From 1422a8e246228d21ce0445c362a7b246a7d5eab5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 29 Oct 2025 11:45:47 +0100 Subject: [PATCH 1/4] feat(ffi): Add IndexedDB and in-memory session stores support. This patch introduces the `sqlite` and `indexeddb` feature flag, enabling the use of SQLite or IndexedDB for the stores. This patch also introduces the ability to use non-persistent, in-memory stores. The new `ClientBuilder::in_memory_store`, `ClientBuilder::sqlite_store` and `ClientBuilder::indexeddb_store` methods are introduced to configure the stores. This patch adds new `SqliteStoreBuilder` and `IndexedDbStoreBuilder` structure. --- bindings/matrix-sdk-ffi/Cargo.toml | 8 +- bindings/matrix-sdk-ffi/src/client.rs | 13 +- bindings/matrix-sdk-ffi/src/client_builder.rs | 195 +++++-------- bindings/matrix-sdk-ffi/src/lib.rs | 1 + bindings/matrix-sdk-ffi/src/store.rs | 260 ++++++++++++++++++ xtask/src/ci.rs | 2 +- 6 files changed, 341 insertions(+), 138 deletions(-) create mode 100644 bindings/matrix-sdk-ffi/src/store.rs diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 682fa9148de..ec69c1ddbc0 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -25,7 +25,12 @@ crate-type = [ [features] default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"] -bundled-sqlite = ["matrix-sdk/bundled-sqlite"] +# Use SQLite for the session storage. +sqlite = ["matrix-sdk/sqlite"] +# Use an embedded version of SQLite. +bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"] +# Use IndexedDB for the session storage. +indexeddb = ["matrix-sdk/indexeddb"] unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"] # Required when targeting a Javascript environment, like Wasm in a browser. js = ["matrix-sdk-ui/js"] @@ -51,7 +56,6 @@ matrix-sdk = { workspace = true, features = [ "experimental-widgets", "markdown", "socks", - "sqlite", "uniffi", ] } matrix-sdk-base.workspace = true diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 7d4240af3cf..5ed22d74c03 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -10,6 +10,8 @@ use anyhow::{anyhow, Context as _}; use futures_util::pin_mut; #[cfg(not(target_family = "wasm"))] use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle; +#[cfg(feature = "sqlite")] +use matrix_sdk::STATE_STORE_DATABASE_NAME; use matrix_sdk::{ authentication::oauth::{ AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession, @@ -39,7 +41,6 @@ use matrix_sdk::{ sliding_sync::Version as SdkSlidingSyncVersion, store::RoomLoadSettings as SdkRoomLoadSettings, Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens, - STATE_STORE_DATABASE_NAME, }; use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm}; use matrix_sdk_ui::{ @@ -242,13 +243,18 @@ impl From for TransmissionProgress { #[derive(uniffi::Object)] pub struct Client { pub(crate) inner: AsyncRuntimeDropped, + delegate: OnceLock>, + pub(crate) utd_hook_manager: OnceLock>, + session_verification_controller: Arc>>, + /// The path to the directory where the state store and the crypto store are - /// located, if the `Client` instance has been built with a SQLite store - /// backend. + /// located, if the `Client` instance has been built with a store (either + /// SQLite or IndexedDB). + #[cfg_attr(not(feature = "sqlite"), allow(unused))] store_path: Option, } @@ -1597,6 +1603,7 @@ impl Client { self.inner.event_cache().clear_all_rooms().await?; // Delete the state store file, if it exists. + #[cfg(feature = "sqlite")] if let Some(store_path) = &self.store_path { debug!("Removing the state store: {}", store_path.display()); diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 71c1b7d8586..7abd342f7eb 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}; #[cfg(not(target_family = "wasm"))] use matrix_sdk::reqwest::Certificate; @@ -11,15 +11,21 @@ use matrix_sdk::{ VersionBuilderError, }, Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError, - RumaApiError, SqliteStoreConfig, ThreadingSupport, + RumaApiError, ThreadingSupport, }; use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement}; use ruma::api::error::{DeserializationError, FromHttpResponseError}; use tracing::debug; -use zeroize::Zeroizing; use super::client::Client; -use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc}; +#[cfg(any(feature = "sqlite", feature = "indexeddb"))] +use crate::store; +use crate::{ + client::ClientSessionDelegate, + error::ClientError, + helpers::unwrap_or_clone_arc, + store::{StoreBuilder, StoreBuilderOutcome}, +}; /// A list of bytes containing a certificate in DER or PEM form. pub type CertificateBytes = Vec; @@ -100,11 +106,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, + store: Option, system_is_memory_constrained: bool, username: Option, homeserver_cfg: Option, @@ -143,11 +145,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, + store: None, system_is_memory_constrained: false, username: None, homeserver_cfg: None, @@ -201,80 +199,13 @@ 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. /// /// 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`]. + /// `matrix_sdk::SqliteStoreConfig` (if the `sqlite` feature is enabled). + /// Please check + /// `matrix_sdk::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; @@ -404,6 +335,13 @@ impl ClientBuilder { Arc::new(builder) } + /// Use in-memory session storage. + pub fn in_memory_store(self: Arc) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.store = Some(StoreBuilder::InMemory); + Arc::new(builder) + } + pub async fn build(self: Arc) -> Result, ClientBuildError> { let builder = unwrap_or_clone_arc(self); let mut inner_builder = MatrixClient::builder(); @@ -413,48 +351,26 @@ 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 store_path = if let Some(store) = &builder.store { + match store.build()? { + #[cfg(feature = "sqlite")] + StoreBuilderOutcome::Sqlite { config, cache_path, store_path: data_path } => { + inner_builder = inner_builder + .sqlite_store_with_config_and_cache_path(config, Some(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); - } + Some(data_path) + } + #[cfg(feature = "indexeddb")] + StoreBuilderOutcome::IndexedDb { name, passphrase } => { + inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref()); - if let Some(size) = builder.session_cache_size { - sqlite_store_config = sqlite_store_config.cache_size(size); - } + None + } - if let Some(limit) = builder.session_journal_size_limit { - sqlite_store_config = sqlite_store_config.journal_size_limit(limit); + StoreBuilderOutcome::InMemory => None, } - - 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 }; @@ -601,6 +517,31 @@ impl ClientBuilder { } } +#[cfg(feature = "sqlite")] +#[matrix_sdk_ffi_macros::export] +impl ClientBuilder { + /// Use SQLite as the session storage. + pub fn sqlite_store(self: Arc, config: Arc) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config))); + Arc::new(builder) + } +} + +#[cfg(feature = "indexeddb")] +#[matrix_sdk_ffi_macros::export] +impl ClientBuilder { + /// Use IndexedDB as the session storage. + pub fn indexeddb_store( + self: Arc, + config: Arc, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.store = Some(StoreBuilder::IndexedDb(unwrap_or_clone_arc(config))); + Arc::new(builder) + } +} + #[cfg(not(target_family = "wasm"))] #[matrix_sdk_ffi_macros::export] impl ClientBuilder { @@ -642,18 +583,8 @@ 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, -} - -#[derive(Clone, uniffi::Record)] /// The config to use for HTTP requests by default in this client. +#[derive(Clone, uniffi::Record)] pub struct RequestConfig { /// Max number of retries. retry_limit: Option, diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 09fd4a3657e..992f6b76892 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -26,6 +26,7 @@ mod ruma; mod runtime; mod session_verification; mod spaces; +mod store; mod sync_service; mod task_handle; mod timeline; diff --git a/bindings/matrix-sdk-ffi/src/store.rs b/bindings/matrix-sdk-ffi/src/store.rs new file mode 100644 index 00000000000..18142ba76d1 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/store.rs @@ -0,0 +1,260 @@ +#[cfg(feature = "sqlite")] +use std::path::PathBuf; + +#[cfg(feature = "sqlite")] +use matrix_sdk::SqliteStoreConfig; + +#[cfg(doc)] +use crate::client_builder::ClientBuilder; + +/// The outcome of building a [`StoreBuilder`], with data that can be passed +/// directly to a [`ClientBuilder`]. +pub enum StoreBuilderOutcome { + /// An SQLite store configuration successfully built. + #[cfg(feature = "sqlite")] + Sqlite { config: SqliteStoreConfig, cache_path: PathBuf, store_path: PathBuf }, + + /// An IndexedDB store configuration successfully built. + #[cfg(feature = "indexeddb")] + IndexedDb { name: String, passphrase: Option }, + + /// An in-memory store configuration successfully built. + InMemory, +} + +#[cfg(feature = "sqlite")] +mod sqlite { + use std::{fs, path::Path, sync::Arc}; + + use matrix_sdk::SqliteStoreConfig; + use tracing::debug; + use zeroize::Zeroizing; + + use super::StoreBuilderOutcome; + use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc}; + + /// The store paths the client will use when built. + #[derive(Clone)] + struct StorePaths { + /// 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 SqliteStoreBuilder { + paths: StorePaths, + passphrase: Zeroizing>, + pool_max_size: Option, + cache_size: Option, + journal_size_limit: Option, + system_is_memory_constrained: bool, + } + + #[matrix_sdk_ffi_macros::export] + impl SqliteStoreBuilder { + /// Construct a [`SqliteStoreBuilder`] 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. + 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 stores. + /// + /// 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 stores. + /// + /// 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. + /// + /// 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`]. 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 SqliteStoreBuilder { + 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(StoreBuilderOutcome::Sqlite { + config: sqlite_store_config, + store_path: data_path.to_owned(), + cache_path: cache_path.to_owned(), + }) + } + } +} + +#[cfg(feature = "indexeddb")] +mod indexeddb { + use std::sync::Arc; + + use super::StoreBuilderOutcome; + use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc}; + + #[derive(Clone, uniffi::Object)] + pub struct IndexedDbStoreBuilder { + name: String, + passphrase: Option, + } + + #[matrix_sdk_ffi_macros::export] + impl IndexedDbStoreBuilder { + #[uniffi::constructor] + pub fn new(name: String) -> Arc { + Arc::new(Self { name, passphrase: None }) + } + + /// Set the passphrase for the stores. + pub fn passphrase(self: Arc, passphrase: Option) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.passphrase = passphrase; + Arc::new(builder) + } + } + + impl IndexedDbStoreBuilder { + pub fn build(&self) -> Result { + Ok(StoreBuilderOutcome::IndexedDb { + name: self.name.clone(), + passphrase: self.passphrase.clone(), + }) + } + } +} + +#[cfg(feature = "indexeddb")] +pub use indexeddb::*; +#[cfg(feature = "sqlite")] +pub use sqlite::*; + +use crate::client_builder::ClientBuildError; + +/// Represent the kind of store the client will configure. +#[derive(Clone)] +pub enum StoreBuilder { + /// Represents the builder for the SQLite store. + #[cfg(feature = "sqlite")] + Sqlite(SqliteStoreBuilder), + + /// Represents the builder for the IndexedDB store. + #[cfg(feature = "indexeddb")] + IndexedDb(IndexedDbStoreBuilder), + + /// Represents the builder for in-memory store. + InMemory, +} + +impl StoreBuilder { + #[allow(clippy::result_large_err)] + pub(crate) fn build(&self) -> Result { + match self { + #[cfg(feature = "sqlite")] + Self::Sqlite(config) => config.build(), + + #[cfg(feature = "indexeddb")] + Self::IndexedDb(config) => config.build(), + + Self::InMemory => Ok(StoreBuilderOutcome::InMemory), + } + } +} diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 1769fe628bc..bd70741d3bc 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -234,7 +234,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,experimental-element-recent-emojis + --features native-tls,sso-login,sqlite,testing,experimental-element-recent-emojis -- -D warnings" ) .run()?; From f6afb51d817599e04e52bb512f55e648053cab8c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 29 Oct 2025 11:57:21 +0100 Subject: [PATCH 2/4] doc(ffi): Update `CHANGELOG.md` and `README.md`. --- bindings/matrix-sdk-ffi/CHANGELOG.md | 32 ++++++++++++++++++++++++++-- bindings/matrix-sdk-ffi/README.md | 18 +++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index baa8e09de5e..6a1fd2cf551 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -6,7 +6,35 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate -### Breaking changes: +### Breaking changes + +- Add the `sqlite` feature, along with the `indexeddb` feature, to enable either + the SQLite or IndexedDB store. The `session_paths`, `session_passphrase`, + `session_pool_max_size`, `session_cache_size` and `session_journal_size_limit` + methods on `ClientBuilder` have been removed. New methods are added: + `ClientBuilder::in_memory_store` if one wants non-persistent stores, + `ClientBuilder::sqlite_store` to configure and to use SQLite stores (if + the `sqlite` feature is enabled), and `ClientBuilder::indexeddb_store` to + configure and to use IndexedDB stores (if the `indexeddb` feature is enabled). + ([#5811](https://github.com/matrix-org/matrix-rust-sdk/pull/5811)) + + The code: + + ```rust + client_builder + .session_paths("data_path", "cache_path") + .passphrase("foobar") + ``` + + now becomes: + + ```rust + client_builder + .sqlite_store( + SqliteSessionStoreBuilder::new("data_path", "cache_path") + .passphrase("foobar") + ) + ``` - The `waveform` parameter in `Timeline::send_voice_message` format changed to a list of `f32` between 0 and 1. @@ -32,7 +60,7 @@ All notable changes to this project will be documented in this file. - Add `Client::subscribe_to_send_queue_updates` to observe global send queue updates. ([#5784](https://github.com/matrix-org/matrix-rust-sdk/pull/5784)) -### Features: +### Features - Add `Room::mark_as_fully_read_unchecked` so clients can mark a room as read without needing a `Timeline` instance. Note this method is not recommended as it can potentially cause incorrect read receipts, but it can needed in certain cases. - Add `Timeline::latest_event_id` to be able to fetch the event id of the latest event of the timeline. diff --git a/bindings/matrix-sdk-ffi/README.md b/bindings/matrix-sdk-ffi/README.md index a514194c818..58e6f899fa3 100644 --- a/bindings/matrix-sdk-ffi/README.md +++ b/bindings/matrix-sdk-ffi/README.md @@ -3,31 +3,33 @@ This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform. ## Features + Given the number of platforms targeted, we have broken out a number of features -### Platform specific +### Platform specific + - `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms. - `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms. ### 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 the session storage. +- `bundled-sqlite`: Use an embedded version of SQLite instead of the system provided one. +- `indexeddb`: Use IndexedDB for the session storage. ### Unstable specs + - `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements. ## Platforms -Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms: +Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms: - 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 - - -### Swift/iOS async - TBD From 6ee211c9b9a48fb73a2de28d965f35e886a14cdf Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 29 Oct 2025 13:18:08 +0100 Subject: [PATCH 3/4] chore(ffi): Allow `clippy::result_large_err`. These two methods are used only once, it's fine to get a large error here. --- bindings/matrix-sdk-ffi/src/store.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/matrix-sdk-ffi/src/store.rs b/bindings/matrix-sdk-ffi/src/store.rs index 18142ba76d1..72e2343c115 100644 --- a/bindings/matrix-sdk-ffi/src/store.rs +++ b/bindings/matrix-sdk-ffi/src/store.rs @@ -142,6 +142,7 @@ mod sqlite { } impl SqliteStoreBuilder { + #[allow(clippy::result_large_err)] pub fn build(&self) -> Result { let data_path = Path::new(&self.paths.data_path); let cache_path = Path::new(&self.paths.cache_path); From 1815eb90bff914c59f86f74d117ed9e6df217e38 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 29 Oct 2025 13:30:36 +0100 Subject: [PATCH 4/4] chore(ffi): Restore `ClientBuilder::session_paths` as `#[deprecated]`. This method restores and marks `ClientBuilder::session_paths` as deprecated. --- bindings/matrix-sdk-ffi/src/client_builder.rs | 17 ++++++++++++++ bindings/matrix-sdk-ffi/src/store.rs | 22 ++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 7abd342f7eb..8249c8775a6 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -1,3 +1,6 @@ +// Allow UniFFI to use methods marked as `#[deprecated]`. +#![allow(deprecated)] + use std::{num::NonZeroUsize, sync::Arc, time::Duration}; #[cfg(not(target_family = "wasm"))] @@ -526,6 +529,20 @@ impl ClientBuilder { builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config))); Arc::new(builder) } + + /// Sets the paths that the client will use to store its data and caches + /// with SQLite. + /// + /// 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. + #[deprecated = "Use `ClientBuilder::session_store_with_sqlite` instead"] + pub fn session_paths(self: Arc, data_path: String, cache_path: String) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.store = + Some(StoreBuilder::Sqlite(store::SqliteStoreBuilder::raw_new(data_path, cache_path))); + Arc::new(builder) + } } #[cfg(feature = "indexeddb")] diff --git a/bindings/matrix-sdk-ffi/src/store.rs b/bindings/matrix-sdk-ffi/src/store.rs index 72e2343c115..5ac81a0acb1 100644 --- a/bindings/matrix-sdk-ffi/src/store.rs +++ b/bindings/matrix-sdk-ffi/src/store.rs @@ -56,6 +56,19 @@ mod sqlite { system_is_memory_constrained: bool, } + impl SqliteStoreBuilder { + pub(crate) fn raw_new(data_path: String, cache_path: String) -> Self { + Self { + paths: StorePaths { data_path, cache_path }, + passphrase: Zeroizing::new(None), + pool_max_size: None, + cache_size: None, + journal_size_limit: None, + system_is_memory_constrained: false, + } + } + } + #[matrix_sdk_ffi_macros::export] impl SqliteStoreBuilder { /// Construct a [`SqliteStoreBuilder`] and set the paths that the client @@ -66,14 +79,7 @@ mod sqlite { /// 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, - }) + Arc::new(Self::raw_new(data_path, cache_path)) } /// Set the passphrase for the stores.