diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 556cd0b15ca..8c45e61cf95 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -6,6 +6,27 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features + +- Configuration of the session store has been changed to allow for use of either IndexedDb + or SQLite on relevant platforms. These can be enabled via features, `indexeddb` or `sqlite`. + + Previously the code to configure a sqlite session would look something like this in a host language: + + ```rust + builder + .session_paths("data_path", "cache_path") + .passphrase("foobar") + ``` + + With the new system, a helper object is exposed for either SQLite or IndexedDB to group those settings. + + ```rust + builder + .session_store_sqlite(SqliteSessionStoreBuilder::new("data_path", "cache_path").passphrase("foobar")) + ``` + + ### Refactor - Adjust features in the `matrix-sdk-ffi` crate to expose more platform-specific knobs. diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 117a1204b00..356e2eddc41 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -25,17 +25,22 @@ crate-type = [ [features] default = ["bundled-sqlite", "unstable-msc4274"] -bundled-sqlite = ["matrix-sdk/bundled-sqlite"] +bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"] unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"] +# Use IndexedDB for session storage, only supported on Wasm targets. +indexeddb = ["matrix-sdk/indexeddb"] +# Use SQLite for session storage, not supported on Wasm targets. +sqlite = ["matrix-sdk/sqlite"] # Required when targeting a Javascript environment, like Wasm in a browser. js = ["matrix-sdk-ui/js"] # Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms. native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"] # Use Rustls as the TLS implementation, necessary on Android platforms. rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"] -# Enable sentry error monitoring, not compatible with Wasm platforms. +# Enable sentry error monitoring, not compatible on Wasm platforms. sentry = ["dep:sentry", "dep:sentry-tracing"] + [dependencies] anyhow.workspace = true as_variant.workspace = true @@ -50,7 +55,6 @@ matrix-sdk = { workspace = true, features = [ "experimental-widgets", "markdown", "socks", - "sqlite", "uniffi", ] } matrix-sdk-common.workspace = true diff --git a/bindings/matrix-sdk-ffi/README.md b/bindings/matrix-sdk-ffi/README.md index a514194c818..0f68e8b4d28 100644 --- a/bindings/matrix-sdk-ffi/README.md +++ b/bindings/matrix-sdk-ffi/README.md @@ -12,6 +12,8 @@ Given the number of platforms targeted, we have broken out a number of features ### Functionality - `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms. - `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one. +- `sqlite`: Use SQLite for session storage, not supported on Wasm targets. +- `indexeddb`: Use IndexedDB for session storage, only supported on Wasm targets. ### Unstable specs - `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements. @@ -22,7 +24,7 @@ Each supported target should use features to select the relevant TLS system. He - Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"` - iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"` -- Javascript/Wasm: `"unstable-msc4274,native-tls"` +- Javascript/Wasm: `"indexeddb,unstable-msc4274,native-tls"` ### Swift/iOS sync diff --git a/bindings/matrix-sdk-ffi/build.rs b/bindings/matrix-sdk-ffi/build.rs index d58dc7a1690..eb844e4cbe0 100644 --- a/bindings/matrix-sdk-ffi/build.rs +++ b/bindings/matrix-sdk-ffi/build.rs @@ -2,7 +2,7 @@ use std::{ env, error::Error, path::{Path, PathBuf}, - process::Command, + process::{self, Command}, }; use vergen::EmitBuilder; @@ -56,7 +56,31 @@ fn get_clang_major_version(clang_path: &Path) -> String { clang_version.split('.').next().expect("could not parse clang output").to_owned() } +fn env_is_set(var_name: &str) -> bool { + env::var_os(var_name).is_some() +} + +fn ensure(cond: bool, err: &str) { + if !cond { + eprintln!( + "\n\ + ┏━━━━━━━━{pad}━┓\n\ + ┃ error: {err} ┃\n\ + ┗━━━━━━━━{pad}━┛\n\ + ", + pad = "━".repeat(err.len()), + ); + process::exit(1); + } +} + fn main() -> Result<(), Box> { + let sqlite_set = env_is_set("CARGO_FEATURE_SQLITE"); + let indexeddb_set = env_is_set("CARGO_FEATURE_INDEXEDDB"); + ensure( + sqlite_set ^ indexeddb_set, + "one of the features 'sqlite' or (exclusive) 'indexeddb' must be enabled", + ); setup_x86_64_android_workaround(); uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed"); EmitBuilder::builder().git_sha(true).emit()?; diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index c76fc498298..8ea9f3edc6b 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -1,4 +1,4 @@ -use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration}; +use std::{num::NonZeroUsize, sync::Arc, time::Duration}; use futures_util::StreamExt; use matrix_sdk::{ @@ -12,11 +12,10 @@ use matrix_sdk::{ VersionBuilderError, }, Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError, - RumaApiError, SqliteStoreConfig, + RumaApiError, }; use ruma::api::error::{DeserializationError, FromHttpResponseError}; use tracing::{debug, error}; -use zeroize::Zeroizing; use super::client::Client; use crate::{ @@ -26,6 +25,7 @@ use crate::{ helpers::unwrap_or_clone_arc, qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener}, runtime::get_runtime_handle, + session_store::{SessionStoreBuilder, SessionStoreResult}, task_handle::TaskHandle, }; @@ -108,11 +108,7 @@ impl From for ClientBuildError { #[derive(Clone, uniffi::Object)] pub struct ClientBuilder { - session_paths: Option, - session_passphrase: Zeroizing>, - session_pool_max_size: Option, - session_cache_size: Option, - session_journal_size_limit: Option, + session_store: Option, system_is_memory_constrained: bool, username: Option, homeserver_cfg: Option, @@ -138,11 +134,7 @@ impl ClientBuilder { #[uniffi::constructor] pub fn new() -> Arc { Arc::new(Self { - session_paths: None, - session_passphrase: Zeroizing::new(None), - session_pool_max_size: None, - session_cache_size: None, - session_journal_size_limit: None, + session_store: None, system_is_memory_constrained: false, username: None, homeserver_cfg: None, @@ -193,73 +185,6 @@ impl ClientBuilder { Arc::new(builder) } - /// Sets the paths that the client will use to store its data and caches. - /// Both paths **must** be unique per session as the SDK stores aren't - /// capable of handling multiple users, however it is valid to use the - /// same path for both stores on a single session. - /// - /// Leaving this unset tells the client to use an in-memory data store. - pub fn session_paths(self: Arc, data_path: String, cache_path: String) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.session_paths = Some(SessionPaths { data_path, cache_path }); - Arc::new(builder) - } - - /// Set the passphrase for the stores given to - /// [`ClientBuilder::session_paths`]. - pub fn session_passphrase(self: Arc, passphrase: Option) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.session_passphrase = Zeroizing::new(passphrase); - Arc::new(builder) - } - - /// Set the pool max size for the SQLite stores given to - /// [`ClientBuilder::session_paths`]. - /// - /// Each store exposes an async pool of connections. This method controls - /// the size of the pool. The larger the pool is, the more memory is - /// consumed, but also the more the app is reactive because it doesn't need - /// to wait on a pool to be available to run queries. - /// - /// See [`SqliteStoreConfig::pool_max_size`] to learn more. - pub fn session_pool_max_size(self: Arc, pool_max_size: Option) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.session_pool_max_size = pool_max_size - .map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`")); - Arc::new(builder) - } - - /// Set the cache size for the SQLite stores given to - /// [`ClientBuilder::session_paths`]. - /// - /// Each store exposes a SQLite connection. This method controls the cache - /// size, in **bytes (!)**. - /// - /// The cache represents data SQLite holds in memory at once per open - /// database file. The default cache implementation does not allocate the - /// full amount of cache memory all at once. Cache memory is allocated - /// in smaller chunks on an as-needed basis. - /// - /// See [`SqliteStoreConfig::cache_size`] to learn more. - pub fn session_cache_size(self: Arc, cache_size: Option) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.session_cache_size = cache_size; - Arc::new(builder) - } - - /// Set the size limit for the SQLite WAL files of stores given to - /// [`ClientBuilder::session_paths`]. - /// - /// Each store uses the WAL journal mode. This method controls the size - /// limit of the WAL files, in **bytes (!)**. - /// - /// See [`SqliteStoreConfig::journal_size_limit`] to learn more. - pub fn session_journal_size_limit(self: Arc, limit: Option) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.session_journal_size_limit = limit; - Arc::new(builder) - } - /// Tell the client that the system is memory constrained, like in a push /// notification process for example. /// @@ -425,48 +350,22 @@ impl ClientBuilder { inner_builder.cross_process_store_locks_holder_name(holder_name.clone()); } - let store_path = if let Some(session_paths) = &builder.session_paths { - // This is the path where both the state store and the crypto store will live. - let data_path = Path::new(&session_paths.data_path); - // This is the path where the event cache store will live. - let cache_path = Path::new(&session_paths.cache_path); - - debug!( - data_path = %data_path.to_string_lossy(), - event_cache_path = %cache_path.to_string_lossy(), - "Creating directories for data (state and crypto) and cache stores.", - ); - - fs::create_dir_all(data_path)?; - fs::create_dir_all(cache_path)?; - - let mut sqlite_store_config = if builder.system_is_memory_constrained { - SqliteStoreConfig::with_low_memory_config(data_path) - } else { - SqliteStoreConfig::new(data_path) - }; - - sqlite_store_config = - sqlite_store_config.passphrase(builder.session_passphrase.as_deref()); - - if let Some(size) = builder.session_pool_max_size { - sqlite_store_config = sqlite_store_config.pool_max_size(size); - } - - if let Some(size) = builder.session_cache_size { - sqlite_store_config = sqlite_store_config.cache_size(size); - } - - if let Some(limit) = builder.session_journal_size_limit { - sqlite_store_config = sqlite_store_config.journal_size_limit(limit); + let store_path = if let Some(session_store) = builder.session_store { + match session_store.build()? { + #[cfg(feature = "indexeddb")] + SessionStoreResult::IndexedDb { name, passphrase } => { + inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref()); + None + } + #[cfg(feature = "sqlite")] + SessionStoreResult::Sqlite { config, cache_path, store_path: data_path } => { + inner_builder = inner_builder + .sqlite_store_with_config_and_cache_path(config, Some(cache_path)); + Some(data_path) + } } - - inner_builder = inner_builder - .sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path)); - - Some(data_path.to_owned()) } else { - debug!("Not using a store path."); + debug!("Not using a session store."); None }; @@ -646,14 +545,32 @@ impl ClientBuilder { } } -/// The store paths the client will use when built. -#[derive(Clone)] -struct SessionPaths { - /// The path that the client will use to store its data. - data_path: String, - /// The path that the client will use to store its caches. This path can be - /// the same as the data path if you prefer to keep everything in one place. - cache_path: String, +#[cfg(feature = "sqlite")] +#[matrix_sdk_ffi_macros::export] +impl ClientBuilder { + /// Tell the client to use sqlite to store session data. + pub fn session_store_sqlite( + self: Arc, + config: Arc, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.session_store = Some(SessionStoreBuilder::Sqlite(config.as_ref().clone())); + Arc::new(builder) + } +} + +#[cfg(feature = "indexeddb")] +#[matrix_sdk_ffi_macros::export] +impl ClientBuilder { + /// Tell the client to use IndexedDb to store session data. + pub fn session_store_indexeddb( + self: Arc, + config: Arc, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.session_store = Some(SessionStoreBuilder::IndexedDb(config.as_ref().clone())); + Arc::new(builder) + } } #[derive(Clone, uniffi::Record)] diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index cf64c47b93e..22b947acd10 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -28,6 +28,7 @@ mod room_member; mod room_preview; mod ruma; mod runtime; +mod session_store; mod session_verification; mod sync_service; mod task_handle; diff --git a/bindings/matrix-sdk-ffi/src/session_store.rs b/bindings/matrix-sdk-ffi/src/session_store.rs new file mode 100644 index 00000000000..cada59c65a5 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/session_store.rs @@ -0,0 +1,250 @@ +#[cfg(feature = "sqlite")] +use std::path::PathBuf; + +#[cfg(feature = "sqlite")] +use matrix_sdk::SqliteStoreConfig; + +/// The result of building a [`SessionStoreBuilder`], with data that +/// can be passed directly to a ClientBuilder. +pub enum SessionStoreResult { + #[cfg(feature = "sqlite")] + Sqlite { config: SqliteStoreConfig, cache_path: PathBuf, store_path: PathBuf }, + #[cfg(feature = "indexeddb")] + IndexedDb { name: String, passphrase: Option }, +} + +#[cfg(feature = "sqlite")] +mod sqlite_session_store { + use std::{fs, path::Path, sync::Arc}; + + use matrix_sdk::SqliteStoreConfig; + use tracing::debug; + use zeroize::Zeroizing; + + use super::SessionStoreResult; + use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc}; + + /// The store paths the client will use when built. + #[derive(Clone)] + struct SessionPaths { + /// The path that the client will use to store its data. + data_path: String, + /// The path that the client will use to store its caches. This path can + /// be the same as the data path if you prefer to keep + /// everything in one place. + cache_path: String, + } + + /// A builder for configuring a Sqlite session store. + #[derive(Clone, uniffi::Object)] + pub struct SqliteSessionStoreBuilder { + paths: SessionPaths, + passphrase: Zeroizing>, + pool_max_size: Option, + cache_size: Option, + journal_size_limit: Option, + system_is_memory_constrained: bool, + } + + #[matrix_sdk_ffi_macros::export] + impl SqliteSessionStoreBuilder { + /// Construct a SqliteSessionStoreBuilder and set the paths that the + /// client will use to store its data and caches. + /// + /// Both paths **must** be unique per session as the SDK stores aren't + /// capable of handling multiple users, however it is valid to use the + /// same path for both stores on a single session. + #[uniffi::constructor] + pub fn new(data_path: String, cache_path: String) -> Arc { + Arc::new(Self { + paths: SessionPaths { data_path, cache_path }, + passphrase: Zeroizing::new(None), + pool_max_size: None, + cache_size: None, + journal_size_limit: None, + system_is_memory_constrained: false, + }) + } + + /// Set the passphrase for the stores given to + /// [`ClientBuilder::paths`]. + pub fn passphrase(self: Arc, passphrase: Option) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.passphrase = Zeroizing::new(passphrase); + Arc::new(builder) + } + + /// Set the pool max size for the SQLite stores given to + /// [`ClientBuilder::session_paths`]. + /// + /// Each store exposes an async pool of connections. This method + /// controls the size of the pool. The larger the pool is, the + /// more memory is consumed, but also the more the app is + /// reactive because it doesn't need to wait on a pool to be + /// available to run queries. + /// + /// See [`SqliteStoreConfig::pool_max_size`] to learn more. + pub fn pool_max_size(self: Arc, pool_max_size: Option) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.pool_max_size = pool_max_size.map(|size| { + size.try_into().expect("`pool_max_size` is too large to fit in `usize`") + }); + Arc::new(builder) + } + + /// Set the cache size for the SQLite stores given to + /// [`ClientBuilder::session_paths`]. + /// + /// Each store exposes a SQLite connection. This method controls the + /// cache size, in **bytes (!)**. + /// + /// The cache represents data SQLite holds in memory at once per open + /// database file. The default cache implementation does not allocate + /// the full amount of cache memory all at once. Cache memory is + /// allocated in smaller chunks on an as-needed basis. + /// + /// See [`SqliteStoreConfig::cache_size`] to learn more. + pub fn cache_size(self: Arc, cache_size: Option) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.cache_size = cache_size; + Arc::new(builder) + } + + /// Set the size limit for the SQLite WAL files of stores given to + /// [`ClientBuilder::session_paths`]. + /// + /// Each store uses the WAL journal mode. This method controls the size + /// limit of the WAL files, in **bytes (!)**. + /// + /// See [`SqliteStoreConfig::journal_size_limit`] to learn more. + pub fn journal_size_limit(self: Arc, limit: Option) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.journal_size_limit = limit; + Arc::new(builder) + } + + /// Tell the client that the system is memory constrained, like in a + /// push notification process for example. + /// + /// So far, at the time of writing (2025-04-07), it changes the defaults + /// of [`SqliteStoreConfig`], so one might not need to call + /// [`ClientBuilder::session_cache_size`] and siblings for example. + /// Please check [`SqliteStoreConfig::with_low_memory_config`]. + pub fn system_is_memory_constrained(self: Arc) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.system_is_memory_constrained = true; + Arc::new(builder) + } + } + + impl SqliteSessionStoreBuilder { + pub fn build(&self) -> Result { + let data_path = Path::new(&self.paths.data_path); + let cache_path = Path::new(&self.paths.cache_path); + + debug!( + data_path = %data_path.to_string_lossy(), + cache_path = %cache_path.to_string_lossy(), + "Creating directories for data and cache stores.", + ); + + fs::create_dir_all(data_path)?; + fs::create_dir_all(cache_path)?; + + let mut sqlite_store_config = if self.system_is_memory_constrained { + SqliteStoreConfig::with_low_memory_config(data_path) + } else { + SqliteStoreConfig::new(data_path) + }; + + sqlite_store_config = sqlite_store_config.passphrase(self.passphrase.as_deref()); + + if let Some(size) = self.pool_max_size { + sqlite_store_config = sqlite_store_config.pool_max_size(size); + } + + if let Some(size) = self.cache_size { + sqlite_store_config = sqlite_store_config.cache_size(size); + } + + if let Some(limit) = self.journal_size_limit { + sqlite_store_config = sqlite_store_config.journal_size_limit(limit); + } + + Ok(SessionStoreResult::Sqlite { + config: sqlite_store_config, + store_path: data_path.to_owned(), + cache_path: cache_path.to_owned(), + }) + } + } +} + +#[cfg(feature = "indexeddb")] +mod indexeddb_session_store { + use std::sync::Arc; + + use super::SessionStoreResult; + use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc}; + + #[derive(Clone, uniffi::Object)] + pub struct IndexedDbSessionStoreBuilder { + name: String, + passphrase: Option, + } + + #[matrix_sdk_ffi_macros::export] + impl IndexedDbSessionStoreBuilder { + #[uniffi::constructor] + pub fn new(name: String) -> Arc { + Arc::new(Self { name, passphrase: None }) + } + + /// Set the passphrase for the IndexedDB store. + pub fn passphrase(self: Arc, passphrase: Option) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.passphrase = passphrase; + Arc::new(builder) + } + } + + impl IndexedDbSessionStoreBuilder { + pub fn build(&self) -> Result { + Ok(SessionStoreResult::IndexedDb { + name: self.name.clone(), + passphrase: self.passphrase.clone(), + }) + } + } +} + +#[cfg(feature = "indexeddb")] +pub use indexeddb_session_store::*; +#[cfg(feature = "sqlite")] +pub use sqlite_session_store::*; + +use crate::client_builder::ClientBuildError; + +/// Represent the kind of store the client will configure. +#[derive(Clone)] +pub enum SessionStoreBuilder { + /// Represents the builder for the SQLite store. + #[cfg(feature = "sqlite")] + Sqlite(SqliteSessionStoreBuilder), + + /// Represents the client for the IndexedDB store. + #[cfg(feature = "indexeddb")] + IndexedDb(IndexedDbSessionStoreBuilder), +} + +impl SessionStoreBuilder { + pub(crate) fn build(&self) -> Result { + match self { + #[cfg(feature = "sqlite")] + Self::Sqlite(config) => config.build(), + + #[cfg(feature = "indexeddb")] + Self::IndexedDb(config) => config.build(), + } + } +} diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index edec278912d..551639ecf2a 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -231,7 +231,7 @@ fn check_clippy() -> Result<()> { "rustup run {NIGHTLY} cargo clippy --workspace --all-targets --exclude matrix-sdk-crypto --exclude xtask --no-default-features - --features native-tls,sso-login,testing + --features native-tls,sso-login,sqlite,testing -- -D warnings" ) .run()?; diff --git a/xtask/src/fixup.rs b/xtask/src/fixup.rs index edf753c51d1..b5e32957390 100644 --- a/xtask/src/fixup.rs +++ b/xtask/src/fixup.rs @@ -69,7 +69,7 @@ fn fix_clippy() -> Result<()> { "rustup run {NIGHTLY} cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged --exclude matrix-sdk-crypto --exclude xtask - --no-default-features --features native-tls,sso-login + --no-default-features --features native-tls,sqlite,sso-login -- -D warnings" ) .run()?;