diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2292dc440d..b12227cacb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -256,6 +256,15 @@ jobs: - name: '[m], indexeddb stores, no crypto' cmd: matrix-sdk-indexeddb-stores-no-crypto + - name: '[m]-sqlite' + cmd: sqlite + + - name: '[m], sqlite stores' + cmd: matrix-sdk-sqlite-stores + + - name: '[m], sqlite stores, no crypto' + cmd: matrix-sdk-sqlite-stores-no-crypto + steps: - name: Checkout the repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 diff --git a/Cargo.lock b/Cargo.lock index bc6352d7636..a927a9994c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -488,6 +488,26 @@ dependencies = [ "wiremock", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.101", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -677,9 +697,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -699,6 +719,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -798,6 +827,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.53" @@ -1884,9 +1924,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "findshlibs" @@ -1922,6 +1962,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2233,11 +2279,11 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2245,14 +2291,17 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] @@ -2722,6 +2771,40 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexed_db_futures" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ff41758cbd104e91033bb53bc449bec7eea65652960c81eddf3fc146ecea19" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "derive_more 2.0.1", + "fancy_constructor", + "indexed_db_futures_macros_internal", + "js-sys", + "sealed", + "smallvec", + "thiserror 2.0.17", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexed_db_futures_macros_internal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caeba94923b68f254abef921cea7e7698bf4675fdd89d7c58bf1ed885b49a27d" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -2965,6 +3048,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.15" @@ -2983,10 +3076,11 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ + "bindgen", "cc", "pkg-config", "vcpkg", @@ -3582,6 +3676,7 @@ dependencies = [ "deadpool 0.13.0", "deadpool-sync", "glob", + "gloo-timers", "itertools 0.14.0", "matrix-sdk-base", "matrix-sdk-common", @@ -3597,11 +3692,17 @@ dependencies = [ "serde_json", "serde_path_to_error", "similar-asserts", + "sqlite-wasm-rs", + "sqlite-wasm-vfs", "tempfile", "thiserror 2.0.17", "tokio", "tracing", + "uri_encode", + "uuid", "vodozemac", + "wasm-bindgen-test", + "web-time", "zeroize", ] @@ -4789,6 +4890,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.17", +] + [[package]] name = "rtoolbox" version = "0.0.2" @@ -4969,9 +5080,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ "bitflags", "fallible-iterator", @@ -4979,6 +5090,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -4999,9 +5111,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -5601,6 +5713,35 @@ dependencies = [ "der", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "bindgen", + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "sqlite-wasm-vfs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7a5c9ac229421d577bb5a9bb59048838509958b218dd4e0b3c1214a87c361e" +dependencies = [ + "indexed_db_futures", + "js-sys", + "rsqlite-vfs", + "thiserror 2.0.17", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -6649,6 +6790,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "uri_encode" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9666edb2c10ee91354ec062e9e2d84896c2437378484c566d3523f9b8daf0129" + [[package]] name = "url" version = "2.5.7" @@ -6990,7 +7137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", - "hashbrown 0.15.2", + "hashbrown 0.15.5", "indexmap", "semver", ] diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 0f5e121b6ea..83207c17e7b 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -1603,26 +1603,26 @@ macro_rules! cryptostore_integration_tests_time { assert!(acquired5.is_none()); // That's a nice test we got here, go take a little nap. - tokio::time::sleep(Duration::from_millis(50)).await; + matrix_sdk_common::sleep::sleep(Duration::from_millis(50)).await; // Still too early. let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); assert!(acquired55.is_none()); // not acquired // Ok you can take another nap then. - tokio::time::sleep(Duration::from_millis(250)).await; + matrix_sdk_common::sleep::sleep(Duration::from_millis(250)).await; // At some point, we do get the lock. let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap(); assert_eq!(acquired6, Some(2)); // new lock generation! - tokio::time::sleep(Duration::from_millis(1)).await; + matrix_sdk_common::sleep::sleep(Duration::from_millis(1)).await; // The other gets it almost immediately too. let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); assert_eq!(acquired7, Some(3)); // new lock generation! - tokio::time::sleep(Duration::from_millis(1)).await; + matrix_sdk_common::sleep::sleep(Duration::from_millis(1)).await; // But when we take a longer lease… let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); diff --git a/crates/matrix-sdk-sqlite/CHANGELOG.md b/crates/matrix-sdk-sqlite/CHANGELOG.md index 69f548aacbe..b7e55a2a30f 100644 --- a/crates/matrix-sdk-sqlite/CHANGELOG.md +++ b/crates/matrix-sdk-sqlite/CHANGELOG.md @@ -19,6 +19,8 @@ All notable changes to this project will be documented in this file. - [**breaking**] In `EventCacheStore::handle_linked_chunk_updates`, new chunks may no longer reference chunk identifiers which do not yet exist in the store ([#6061](https://github.com/matrix-org/matrix-rust-sdk/pull/6061)) +- Support compiling to `wasm32-unknown-unknown` target + bump [rusqlite](https://github.com/rusqlite/rusqlite/releases/tag/v0.38.0) version to `0.38` + ([#6329](https://github.com/matrix-org/matrix-rust-sdk/pull/6329)) ### Bug Fixes diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index e11cf2ad873..e2d0130bc0f 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -26,37 +26,57 @@ experimental-push-secrets = [ "matrix-sdk-crypto?/experimental-push-secrets" ] +js = ["matrix-sdk-common/js", "matrix-sdk-base?/js", "matrix-sdk-crypto?/js", "matrix-sdk-store-encryption/js"] + [dependencies] as_variant.workspace = true async-trait.workspace = true deadpool = { version = "0.13", default-features = false, features = ["managed", "rt_tokio_1"] } -deadpool-sync = { version = "0.2", default-features = false } itertools.workspace = true matrix-sdk-base = { workspace = true, optional = true } +matrix-sdk-common.workspace = true matrix-sdk-crypto = { workspace = true, optional = true } matrix-sdk-store-encryption.workspace = true num_cpus = { version = "1.17.0", default-features = false } rmp-serde.workspace = true ruma.workspace = true -rusqlite = { version = "0.37.0", default-features = false, features = ["limits"] } serde.workspace = true serde_json.workspace = true serde_path_to_error = { version = "0.1.20", default-features = false } thiserror.workspace = true -tokio = { workspace = true, features = ["fs"] } tracing.workspace = true vodozemac.workspace = true zeroize.workspace = true +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true } +rusqlite = { version = "0.38.0", default-features = false, features = ["limits", "cache", "fallible_uint", "buildtime_bindgen", "wasm32-wasi-vfs"] } +sqlite-wasm-rs = "0.5.2" +sqlite-wasm-vfs = "0.2.0" +web-time = { version = "1.1.0", features = ["serde"] } +uri_encode = "1.0.4" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +deadpool-sync = { version = "0.2", default-features = false } +tokio = { workspace = true, features = ["fs"] } +rusqlite = { version = "0.38.0", default-features = false, features = ["limits", "cache", "fallible_uint"] } + [dev-dependencies] assert_matches.workspace = true glob = "0.3.3" matrix-sdk-base = { workspace = true, features = ["testing"] } -matrix-sdk-common.workspace = true matrix-sdk-crypto = { workspace = true, features = ["testing"] } matrix-sdk-test.workspace = true matrix-sdk-test-utils.workspace = true similar-asserts.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test.workspace = true +tokio = { workspace = true, features = ["rt", "macros"] } +gloo-timers = { workspace = true, features = ["futures"] } +uuid = { workspace = true, features = ["js", "v4"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tempfile.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/matrix-sdk-sqlite/src/connection.rs b/crates/matrix-sdk-sqlite/src/connection/default.rs similarity index 89% rename from crates/matrix-sdk-sqlite/src/connection.rs rename to crates/matrix-sdk-sqlite/src/connection/default.rs index 78468dde536..3a587e7fe44 100644 --- a/crates/matrix-sdk-sqlite/src/connection.rs +++ b/crates/matrix-sdk-sqlite/src/connection/default.rs @@ -64,25 +64,13 @@ //! [spawn_blocking]: https://github.com/deadpool-rs/deadpool/blob/d6f7d58756f0cc7bdd1f3d54d820c1332d67e4d5/crates/deadpool-sync/src/lib.rs#L113-L131 //! [WAL]: https://www.sqlite.org/wal.html -use std::{convert::Infallible, path::PathBuf}; +use std::path::{Path, PathBuf}; -pub use deadpool::managed::reexports::*; use deadpool::managed::{self, Metrics, RecycleError}; use deadpool_sync::SyncWrapper; +use tokio::fs; -/// The default runtime used by `matrix-sdk-sqlite` for `deadpool`. -pub const RUNTIME: Runtime = Runtime::Tokio1; - -deadpool::managed_reexports!( - "matrix-sdk-sqlite", - Manager, - managed::Object, - rusqlite::Error, - Infallible -); - -/// Type representing a connection to SQLite from the [`Pool`]. -pub type Connection = Object; +use crate::{OpenStoreError, connection::RUNTIME}; /// [`Manager`][managed::Manager] for creating and recycling SQLite /// [`Connection`]s. @@ -93,9 +81,10 @@ pub struct Manager { impl Manager { /// Creates a new [`Manager`] for a database. - #[must_use] - pub fn new(database_path: PathBuf) -> Self { - Self { database_path } + pub async fn new(path: &Path, database_name: &str) -> Result { + fs::create_dir_all(path).await.map_err(OpenStoreError::CreateDir)?; + + Ok(Self { database_path: path.join(database_name) }) } } diff --git a/crates/matrix-sdk-sqlite/src/connection/mod.rs b/crates/matrix-sdk-sqlite/src/connection/mod.rs new file mode 100644 index 00000000000..f86acef5059 --- /dev/null +++ b/crates/matrix-sdk-sqlite/src/connection/mod.rs @@ -0,0 +1,42 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::convert::Infallible; + +use deadpool::managed; +pub use deadpool::managed::reexports::*; + +#[cfg(not(target_family = "wasm"))] +mod default; +#[cfg(target_family = "wasm")] +mod wasm; + +#[cfg(not(target_family = "wasm"))] +pub use default::*; +#[cfg(target_family = "wasm")] +pub use wasm::*; + +/// The default runtime used by `matrix-sdk-sqlite` for `deadpool`. +pub const RUNTIME: Runtime = Runtime::Tokio1; + +deadpool::managed_reexports!( + "matrix-sdk-sqlite", + Manager, + managed::Object, + rusqlite::Error, + Infallible +); + +/// Type representing a connection to SQLite from the [`Pool`]. +pub type Connection = Object; diff --git a/crates/matrix-sdk-sqlite/src/connection/wasm.rs b/crates/matrix-sdk-sqlite/src/connection/wasm.rs new file mode 100644 index 00000000000..c2250961714 --- /dev/null +++ b/crates/matrix-sdk-sqlite/src/connection/wasm.rs @@ -0,0 +1,147 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! An implementation of `deadpool::managed::Manager` for `rusqlite` +//! for usage in WASM environments. +//! +//! Similar to the one implemented in `crate::connection::default`, +//! we do not implement connection recycling here. Mostly due to +//! [`managed::Manager::recycle`] method expecting a future with `Send` +//! bound which is not available in WASM environment. + +use std::{ + cell::RefCell, + convert::Infallible, + ops::DerefMut, + path::{Path, PathBuf}, +}; + +use deadpool::managed::{self, Metrics}; +use rusqlite::OpenFlags; +use sqlite_wasm_vfs::sahpool::{OpfsSAHPoolCfgBuilder, OpfsSAHPoolUtil, install}; + +use crate::OpenStoreError; + +/// [`Manager`][managed::Manager] for creating and recycling SQLite +/// [`Connection`]s. +#[derive(Debug)] +pub struct Manager { + database_path: PathBuf, + + /// VFS used by this database connection in WASM environment. + vfs: String, +} + +impl Manager { + /// Creates a new [`Manager`] for a database. + pub async fn new(path: &Path, database_name: &str) -> Result { + setup_vfs(path).await?; + + // We don't need full path for database path as the parent + // directories are managed by VFS. + let database_path = PathBuf::from(database_name); + + Ok(Self { database_path, vfs: get_vfs_name(path) }) + } +} + +impl managed::Manager for Manager { + type Type = ConnectionWrapper; + type Error = rusqlite::Error; + + async fn create(&self) -> Result { + let path = self.database_path.clone(); + + let conn = rusqlite::Connection::open_with_flags_and_vfs( + path, + OpenFlags::default(), + self.vfs.as_str(), + )?; + Ok(ConnectionWrapper::new(conn)) + } + + async fn recycle( + &self, + _conn: &mut Self::Type, + _: &Metrics, + ) -> managed::RecycleResult { + // We cannot implement connection recycling + // at the moment, due to + // `managed::Manager::recycle` expecting + // a future with `Send` bound which is not + // available in WASM environments. + Ok(()) + } +} + +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +#[derive(Debug)] +/// Wrapper object for providing interior mutability similar to [`SyncWrapper`] +/// without `Send` requirement. +/// +/// Like [`SyncWrapper`], access to the wrapped object is provided via the +/// [`ConnectionWrapper::interact()`] method. +/// +/// [`SyncWrapper`]: deadpool_sync::SyncWrapper +pub struct ConnectionWrapper(RefCell); + +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +impl ConnectionWrapper { + /// Creates a new wrapped object. + pub fn new(value: T) -> Self { + Self(RefCell::new(value)) + } + + /// Interacts with the underlying object. + /// + /// Expects a closure that takes the object as its parameter. + pub async fn interact(&self, f: F) -> Result + where + F: FnOnce(&mut T) -> R, + { + // This async block here is to maintain API compatibility with `SyncWrapper` + // without triggering clippy warning for `async fn` without a call to + // `await` inside + async { + let mut value = self.0.borrow_mut(); + + Ok(f(value.deref_mut())) + } + .await + } +} + +/// Configure VFS name using provided path. +pub fn get_vfs_name(path: &Path) -> String { + format!( + "matrix-opfs-sahpool+{}", + uri_encode::encode_uri_component(path.to_string_lossy().as_ref()) + ) +} + +/// Setup VFS for SQLite database using provided path and return management +/// tool. +/// +/// Subsequence call to this function will simply return the management tool +/// without installing vfs. +pub async fn setup_vfs(path: &Path) -> Result { + let cfg = OpfsSAHPoolCfgBuilder::new() + .vfs_name(&get_vfs_name(path)) + .directory(path.to_string_lossy().as_ref()) + .build(); + // Avoid global installation, due to being harder to test. + let util = install::(&cfg, false).await?; + + Ok(util) +} diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 35308b2b5a2..923afec8982 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -43,10 +43,7 @@ use ruma::{ events::secret::request::SecretName, }; use rusqlite::{OptionalExtension, named_params, params_from_iter}; -use tokio::{ - fs, - sync::{Mutex, OwnedMutexGuard}, -}; +use tokio::sync::{Mutex, OwnedMutexGuard}; use tracing::{debug, instrument, warn}; use vodozemac::Curve25519PublicKey; use zeroize::Zeroizing; @@ -147,9 +144,7 @@ impl SqliteCryptoStore { /// Open the SQLite-based crypto store with the config open config. pub async fn open_with_config(config: SqliteStoreConfig) -> Result { - fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?; - - let pool = config.build_pool_of_connections(DATABASE_NAME)?; + let pool = config.build_pool_of_connections(DATABASE_NAME).await?; let this = Self::open_with_pool(pool, config.secret).await?; this.pool.get().await?.apply_runtime_config(config.runtime_config).await?; @@ -708,7 +703,8 @@ impl SqliteConnectionExt for rusqlite::Connection { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] trait SqliteObjectCryptoStoreExt: SqliteAsyncConnExt { async fn get_sessions_for_sender_key(&self, sender_key: Key) -> Result>> { Ok(self @@ -1042,10 +1038,12 @@ trait SqliteObjectCryptoStoreExt: SqliteAsyncConnExt { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl SqliteObjectCryptoStoreExt for SqliteAsyncConn {} -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl CryptoStore for SqliteCryptoStore { type Error = Error; @@ -1818,7 +1816,12 @@ impl CryptoStore for SqliteCryptoStore { #[cfg(test)] mod tests { - use std::{path::Path, sync::LazyLock}; + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + + use std::sync::LazyLock; + #[cfg(target_family = "wasm")] + use std::sync::atomic::{AtomicU32, Ordering::SeqCst}; use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_crypto::{ @@ -1828,38 +1831,63 @@ mod tests { use matrix_sdk_test::async_test; use ruma::{device_id, room_id, user_id}; use similar_asserts::assert_eq; - use tempfile::{TempDir, tempdir}; + #[cfg(not(target_family = "wasm"))] + use tempfile::tempdir; + #[cfg(not(target_family = "wasm"))] use tokio::fs; use super::SqliteCryptoStore; - use crate::SqliteStoreConfig; + #[cfg(target_family = "wasm")] + use crate::connection::setup_vfs; + use crate::{ + SqliteStoreConfig, + test_utils::{TempDirWrapper, create_tmp_dir}, + }; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); + #[cfg(target_family = "wasm")] + static NUM: AtomicU32 = AtomicU32::new(0); struct TestDb { // Needs to be kept alive because the Drop implementation for TempDir deletes the // directory. - _dir: TempDir, + _dir: TempDirWrapper, + database: SqliteCryptoStore, } - fn copy_db(data_path: &str) -> TempDir { + #[cfg(not(target_family = "wasm"))] + async fn copy_db(db_source: &[u8]) -> TempDirWrapper { let db_name = super::DATABASE_NAME; - let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let database_path = manifest_path.join(data_path).join(db_name); - let tmpdir = tempdir().unwrap(); let destination = tmpdir.path().join(db_name); // Copy the test database to the tempdir so our test runs are idempotent. - std::fs::copy(&database_path, destination).unwrap(); + fs::write(destination, db_source).await.unwrap(); tmpdir } - async fn get_test_db(data_path: &str, passphrase: Option<&str>) -> TestDb { - let tmpdir = copy_db(data_path); + #[cfg(target_family = "wasm")] + async fn copy_db(db_source: &[u8]) -> TempDirWrapper { + let name = NUM.fetch_add(1, SeqCst).to_string(); + let tmpdir = TMP_DIR.path().join(name); + + // Get tool used to manipulate database inside VFS. + let tool = setup_vfs(&tmpdir).await.unwrap(); + + // Import our test fixture into destination database. + tool.import_db(super::DATABASE_NAME, db_source).unwrap(); + + // Make sure that we successfully imported the database. + assert!(tool.exists(super::DATABASE_NAME).unwrap(), "imported db must exists"); + + TempDirWrapper::new_with_path(tmpdir) + } + + async fn get_test_db(db_source: &[u8], passphrase: Option<&str>) -> TestDb { + let tmpdir = copy_db(db_source).await; let database = SqliteCryptoStore::open(tmpdir.path(), passphrase) .await @@ -1882,7 +1910,13 @@ mod tests { /// pre-filled database, or in other words use a test vector for this. #[async_test] async fn test_open_test_vector_store() { - let TestDb { _dir: _, database } = get_test_db("testing/data/storage", None).await; + // Load test database source during compile-time, since we do not have + // access to file system during runtime for WASM targets. + let TestDb { _dir: _, database } = get_test_db( + include_bytes!("../../../testing/data/storage/matrix-sdk-crypto.wasm.db"), + None, + ) + .await; let account = database .load_account() @@ -1948,8 +1982,10 @@ mod tests { /// pre-filled database, or in other words use a test vector for this. #[async_test] async fn test_open_test_vector_encrypted_store() { + // Load test database source during compile-time, since we do not have + // access to file system during runtime for WASM targets. let TestDb { _dir: _, database } = get_test_db( - "testing/data/storage/alice", + include_bytes!("../../../testing/data/storage/alice/matrix-sdk-crypto.wasm.db"), Some(concat!( "/rCia2fYAJ+twCZ1Xm2mxFCYcmJdyzkdJjwtgXsziWpYS/UeNxnixuSieuwZXm+x1VsJHmWpl", "H+QIQBZpEGZtC9/S/l8xK+WOCesmET0o6yJ/KP73ofDtjBlnNpPwuHLKFpyTbyicpCgQ4UT+5E", @@ -2238,9 +2274,12 @@ mod tests { use crate::utils::{EncryptableStore, SqliteAsyncConnExt}; // Create a database with version 16 + #[cfg(not(target_family = "wasm"))] let tmpdir = tempdir().unwrap(); + #[cfg(target_family = "wasm")] + let tmpdir = TempDirWrapper::new(); let config = SqliteStoreConfig::new(tmpdir.path()); - let pool = config.build_pool_of_connections(super::DATABASE_NAME).unwrap(); + let pool = config.build_pool_of_connections(super::DATABASE_NAME).await.unwrap(); let conn = pool.get().await.unwrap(); let version = super::initialize_store(&conn, 0).await.unwrap(); let old_data_store = @@ -2296,9 +2335,15 @@ mod tests { ) -> SqliteCryptoStore { let tmpdir_path = TMP_DIR.path().join(name); + #[cfg(not(target_family = "wasm"))] if clear_data { let _ = fs::remove_dir_all(&tmpdir_path).await; } + #[cfg(target_family = "wasm")] + if clear_data { + let tool = setup_vfs(&tmpdir_path).await.unwrap(); + tool.delete_db(super::DATABASE_NAME).unwrap(); + } SqliteCryptoStore::open(tmpdir_path.to_str().unwrap(), passphrase) .await @@ -2314,12 +2359,15 @@ mod encrypted_tests { use std::sync::LazyLock; use matrix_sdk_crypto::{cryptostore_integration_tests, cryptostore_integration_tests_time}; - use tempfile::{TempDir, tempdir}; + #[cfg(not(target_family = "wasm"))] use tokio::fs; use super::SqliteCryptoStore; + #[cfg(target_family = "wasm")] + use crate::connection::setup_vfs; + use crate::test_utils::{TempDirWrapper, create_tmp_dir}; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); async fn get_store( name: &str, @@ -2327,11 +2375,18 @@ mod encrypted_tests { clear_data: bool, ) -> SqliteCryptoStore { let tmpdir_path = TMP_DIR.path().join(name); + let pass = passphrase.unwrap_or("default_test_password"); + #[cfg(not(target_family = "wasm"))] if clear_data { let _ = fs::remove_dir_all(&tmpdir_path).await; } + #[cfg(target_family = "wasm")] + if clear_data { + let tool = setup_vfs(&tmpdir_path).await.unwrap(); + tool.delete_db(super::DATABASE_NAME).unwrap(); + } SqliteCryptoStore::open(tmpdir_path.to_str().unwrap(), Some(pass)) .await diff --git a/crates/matrix-sdk-sqlite/src/error.rs b/crates/matrix-sdk-sqlite/src/error.rs index f590ef1be35..c9ae3532b12 100644 --- a/crates/matrix-sdk-sqlite/src/error.rs +++ b/crates/matrix-sdk-sqlite/src/error.rs @@ -23,7 +23,10 @@ use matrix_sdk_base::media::store::MediaStoreError; use matrix_sdk_base::store::StoreError as StateStoreError; #[cfg(feature = "crypto-store")] use matrix_sdk_crypto::CryptoStoreError; +#[cfg(target_family = "wasm")] +use sqlite_wasm_vfs::sahpool::OpfsSAHError; use thiserror::Error; +#[cfg(not(target_family = "wasm"))] use tokio::io; use crate::connection::{CreatePoolError, PoolError}; @@ -32,6 +35,7 @@ use crate::connection::{CreatePoolError, PoolError}; #[derive(Error, Debug)] #[non_exhaustive] pub enum OpenStoreError { + #[cfg(not(target_family = "wasm"))] /// Failed to create the DB's parent directory. #[error("Failed to create the database's parent directory: {0}")] CreateDir(#[source] io::Error), @@ -71,6 +75,11 @@ pub enum OpenStoreError { /// Failed to save the store cipher to the DB. #[error("Failed to save the store cipher to the DB: {0}")] SaveCipher(#[source] rusqlite::Error), + + #[cfg(target_family = "wasm")] + /// Failed to setup vfs for wasm environment + #[error("Failed to setup vfs for WASM environment: {0}")] + SetupOpfs(#[from] OpfsSAHError), } #[derive(Debug, Error)] diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index 794f9323e41..442fd9ea10d 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -18,6 +18,7 @@ use std::{collections::HashMap, fmt, iter::once, path::Path, sync::Arc}; use async_trait::async_trait; use matrix_sdk_base::{ + SendOutsideWasm, cross_process_lock::CrossProcessLockGeneration, deserialized_responses::TimelineEvent, event_cache::{ @@ -37,10 +38,7 @@ use ruma::{ use rusqlite::{ OptionalExtension, ToSql, Transaction, TransactionBehavior, params, params_from_iter, }; -use tokio::{ - fs, - sync::{Mutex, OwnedMutexGuard}, -}; +use tokio::sync::{Mutex, OwnedMutexGuard}; use tracing::{debug, error, instrument, trace}; use crate::{ @@ -123,9 +121,7 @@ impl SqliteEventCacheStore { let _timer = timer!("open_with_config"); - fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?; - - let pool = config.build_pool_of_connections(DATABASE_NAME)?; + let pool = config.build_pool_of_connections(DATABASE_NAME).await?; let this = Self::open_with_pool(pool, config.secret).await?; this.write().await?.apply_runtime_config(config.runtime_config).await?; @@ -508,7 +504,8 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { Ok(()) } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl EventCacheStore for SqliteEventCacheStore { type Error = Error; @@ -1553,8 +1550,8 @@ fn find_event_relations_transaction( /// of the kind SQLITE_BUSY from happening, for transactions that may involve /// both reads and writes, and start with a write. async fn with_immediate_transaction< - T: Send + 'static, - F: FnOnce(&Transaction<'_>) -> Result + Send + 'static, + T: SendOutsideWasm + 'static, + F: FnOnce(&Transaction<'_>) -> Result + SendOutsideWasm + 'static, >( this: &SqliteEventCacheStore, f: F, @@ -1645,6 +1642,9 @@ fn insert_chunk( #[cfg(test)] mod tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::{ path::PathBuf, sync::{ @@ -1663,16 +1663,16 @@ mod tests { linked_chunk::{ChunkIdentifier, LinkedChunkId, Update}, }; use matrix_sdk_test::{DEFAULT_TEST_ROOM_ID, async_test}; - use tempfile::{TempDir, tempdir}; use super::SqliteEventCacheStore; use crate::{ SqliteStoreConfig, event_cache_store::keys, + test_utils::{TempDirWrapper, create_tmp_dir}, utils::{EncryptableStore as _, SqliteAsyncConnExt}, }; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); fn new_event_cache_store_workspace() -> PathBuf { @@ -1830,6 +1830,9 @@ mod tests { #[cfg(test)] mod encrypted_tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::sync::{ LazyLock, atomic::{AtomicU32, Ordering::SeqCst}, @@ -1845,11 +1848,11 @@ mod encrypted_tests { events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation}, room_id, user_id, }; - use tempfile::{TempDir, tempdir}; use super::SqliteEventCacheStore; + use crate::test_utils::{TempDirWrapper, create_tmp_dir}; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); async fn get_event_cache_store() -> Result { diff --git a/crates/matrix-sdk-sqlite/src/lib.rs b/crates/matrix-sdk-sqlite/src/lib.rs index 03321f2589c..dc312c6e4e1 100644 --- a/crates/matrix-sdk-sqlite/src/lib.rs +++ b/crates/matrix-sdk-sqlite/src/lib.rs @@ -27,6 +27,9 @@ mod event_cache_store; mod media_store; #[cfg(feature = "state-store")] mod state_store; +#[cfg(test)] +#[allow(dead_code)] +mod test_utils; mod utils; use std::{ cmp::max, @@ -209,18 +212,19 @@ impl SqliteStoreConfig { } /// Build a pool of active connections to a particular database. - pub fn build_pool_of_connections( + pub async fn build_pool_of_connections( &self, database_name: &str, - ) -> Result { - let path = self.path.join(database_name); - let manager = connection::Manager::new(path); + ) -> Result { + let manager = connection::Manager::new(&self.path, database_name).await?; - connection::Pool::builder(manager) + let pool = connection::Pool::builder(manager) .config(self.pool_config) .runtime(connection::RUNTIME) .build() - .map_err(connection::CreatePoolError::Build) + .map_err(connection::CreatePoolError::Build)?; + + Ok(pool) } } diff --git a/crates/matrix-sdk-sqlite/src/media_store.rs b/crates/matrix-sdk-sqlite/src/media_store.rs index a1c364df5c7..46b7d7a4786 100644 --- a/crates/matrix-sdk-sqlite/src/media_store.rs +++ b/crates/matrix-sdk-sqlite/src/media_store.rs @@ -31,10 +31,7 @@ use matrix_sdk_base::{ use matrix_sdk_store_encryption::StoreCipher; use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, time::SystemTime}; use rusqlite::{OptionalExtension, params_from_iter}; -use tokio::{ - fs, - sync::{Mutex, OwnedMutexGuard}, -}; +use tokio::sync::{Mutex, OwnedMutexGuard}; use tracing::{debug, instrument}; use crate::{ @@ -115,9 +112,7 @@ impl SqliteMediaStore { let _timer = timer!("open_with_config"); - fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?; - - let pool = config.build_pool_of_connections(DATABASE_NAME)?; + let pool = config.build_pool_of_connections(DATABASE_NAME).await?; let this = Self::open_with_pool(pool, config.secret).await?; this.write().await?.apply_runtime_config(config.runtime_config).await?; @@ -225,7 +220,8 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { Ok(()) } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl MediaStore for SqliteMediaStore { type Error = Error; @@ -661,6 +657,9 @@ impl MediaStoreInner for SqliteMediaStore { #[cfg(test)] mod tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::{ path::PathBuf, sync::{ @@ -680,12 +679,15 @@ mod tests { }; use matrix_sdk_test::async_test; use ruma::{events::room::MediaSource, media::Method, mxc_uri, uint}; - use tempfile::{TempDir, tempdir}; use super::SqliteMediaStore; - use crate::{SqliteStoreConfig, utils::SqliteAsyncConnExt}; + use crate::{ + SqliteStoreConfig, + test_utils::{TempDirWrapper, create_tmp_dir}, + utils::SqliteAsyncConnExt, + }; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); fn new_media_store_workspace() -> PathBuf { @@ -755,7 +757,7 @@ mod tests { // Since the precision of the timestamp is in seconds, wait so the timestamps // differ. - tokio::time::sleep(Duration::from_secs(3)).await; + matrix_sdk_common::sleep::sleep(Duration::from_secs(3)).await; media_store .add_media_content( @@ -775,7 +777,7 @@ mod tests { // Since the precision of the timestamp is in seconds, wait so the timestamps // differ. - tokio::time::sleep(Duration::from_secs(3)).await; + matrix_sdk_common::sleep::sleep(Duration::from_secs(3)).await; // Access the file so its last access is more recent. let _ = media_store @@ -795,6 +797,9 @@ mod tests { #[cfg(test)] mod encrypted_tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::sync::{ LazyLock, atomic::{AtomicU32, Ordering::SeqCst}, @@ -804,11 +809,11 @@ mod encrypted_tests { media::store::MediaStoreError, media_store_inner_integration_tests, media_store_integration_tests, media_store_integration_tests_time, }; - use tempfile::{TempDir, tempdir}; use super::SqliteMediaStore; + use crate::test_utils::{TempDirWrapper, create_tmp_dir}; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); async fn get_media_store() -> Result { diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index e378742d512..614a54af811 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -37,10 +37,7 @@ use ruma::{ }; use rusqlite::{OptionalExtension, Transaction}; use serde::{Deserialize, Serialize}; -use tokio::{ - fs, - sync::{Mutex, OwnedMutexGuard}, -}; +use tokio::sync::{Mutex, OwnedMutexGuard}; use tracing::{debug, instrument, warn}; use crate::{ @@ -115,9 +112,7 @@ impl SqliteStateStore { /// Open the SQLite-based state store with the config open config. pub async fn open_with_config(config: SqliteStoreConfig) -> Result { - fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?; - - let pool = config.build_pool_of_connections(DATABASE_NAME)?; + let pool = config.build_pool_of_connections(DATABASE_NAME).await?; let this = Self::open_with_pool(pool, config.secret).await?; this.pool.get().await?.apply_runtime_config(config.runtime_config).await?; @@ -874,7 +869,8 @@ impl SqliteConnectionStateStoreExt for rusqlite::Connection { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] trait SqliteObjectStateStoreExt: SqliteAsyncConnExt { async fn get_kv_blob(&self, key: Key) -> Result>> { Ok(self @@ -1111,14 +1107,16 @@ trait SqliteObjectStateStoreExt: SqliteAsyncConnExt { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl SqliteObjectStateStoreExt for SqliteAsyncConn { async fn set_kv_blob(&self, key: Key, value: Vec) -> Result<()> { Ok(self.interact(move |conn| conn.set_kv_blob(&key, &value)).await.unwrap()?) } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl StateStore for SqliteStateStore { type Error = Error; @@ -2379,17 +2377,20 @@ struct ReceiptData { #[cfg(test)] mod tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::sync::{ LazyLock, atomic::{AtomicU32, Ordering::SeqCst}, }; use matrix_sdk_base::{StateStore, StoreError, statestore_integration_tests}; - use tempfile::{TempDir, tempdir}; use super::SqliteStateStore; + use crate::test_utils::{TempDirWrapper, create_tmp_dir}; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); async fn get_store() -> Result { @@ -2406,6 +2407,9 @@ mod tests { #[cfg(test)] mod encrypted_tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::{ path::PathBuf, sync::{ @@ -2416,12 +2420,15 @@ mod encrypted_tests { use matrix_sdk_base::{StateStore, StoreError, statestore_integration_tests}; use matrix_sdk_test::async_test; - use tempfile::{TempDir, tempdir}; use super::SqliteStateStore; - use crate::{SqliteStoreConfig, utils::SqliteAsyncConnExt}; + use crate::{ + SqliteStoreConfig, + test_utils::{TempDirWrapper, create_tmp_dir}, + utils::SqliteAsyncConnExt, + }; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); fn new_state_store_workspace() -> PathBuf { @@ -2489,6 +2496,9 @@ mod encrypted_tests { #[cfg(test)] mod migration_tests { + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); + use std::{ path::{Path, PathBuf}, sync::{ @@ -2519,18 +2529,18 @@ mod migration_tests { use rusqlite::Transaction; use serde::{Deserialize, Serialize}; use serde_json::json; - use tempfile::{TempDir, tempdir}; - use tokio::{fs, sync::Mutex}; + use tokio::sync::Mutex; use zeroize::Zeroizing; use super::{DATABASE_NAME, SqliteStateStore, init, keys}; use crate::{ - OpenStoreError, Secret, SqliteStoreConfig, + Secret, SqliteStoreConfig, error::{Error, Result}, + test_utils::{TempDirWrapper, create_tmp_dir}, utils::{EncryptableStore as _, SqliteAsyncConnExt, SqliteKeyValueStoreAsyncConnExt}, }; - static TMP_DIR: LazyLock = LazyLock::new(|| tempdir().unwrap()); + static TMP_DIR: LazyLock = create_tmp_dir(); static NUM: AtomicU32 = AtomicU32::new(0); const SECRET: &str = "secret"; @@ -2542,9 +2552,7 @@ mod migration_tests { async fn create_fake_db(path: &Path, version: u8) -> Result { let config = SqliteStoreConfig::new(path); - fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir).unwrap(); - - let pool = config.build_pool_of_connections(DATABASE_NAME).unwrap(); + let pool = config.build_pool_of_connections(DATABASE_NAME).await.unwrap(); let conn = pool.get().await?; init(&conn).await?; diff --git a/crates/matrix-sdk-sqlite/src/test_utils.rs b/crates/matrix-sdk-sqlite/src/test_utils.rs new file mode 100644 index 00000000000..544f01c4f21 --- /dev/null +++ b/crates/matrix-sdk-sqlite/src/test_utils.rs @@ -0,0 +1,38 @@ +#[cfg(target_family = "wasm")] +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; + +#[cfg(not(target_family = "wasm"))] +use tempfile::{TempDir, tempdir}; + +#[cfg(not(target_family = "wasm"))] +pub const fn create_tmp_dir() -> LazyLock { + LazyLock::new(|| tempdir().unwrap()) +} + +#[cfg(target_family = "wasm")] +pub const fn create_tmp_dir() -> LazyLock { + LazyLock::new(|| TempDirWrapper::new()) +} + +#[cfg(not(target_family = "wasm"))] +pub type TempDirWrapper = TempDir; +#[cfg(target_family = "wasm")] +/// Wrapper type to keep interface compatibility with `TempDir` +/// for wasm environments. +pub struct TempDirWrapper(PathBuf); + +#[cfg(target_family = "wasm")] +impl TempDirWrapper { + pub fn new() -> Self { + Self(PathBuf::from(uuid::Uuid::new_v4().to_string())) + } + + pub fn new_with_path(path: PathBuf) -> Self { + Self(path) + } + + pub fn path(&self) -> &Path { + &self.0 + } +} diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index 80933bab710..e7928e56230 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -13,6 +13,8 @@ // limitations under the License. use core::fmt; +#[cfg(target_family = "wasm")] +use std::convert::Infallible; use std::{ borrow::{Borrow, Cow}, cmp::min, @@ -21,8 +23,10 @@ use std::{ }; use async_trait::async_trait; +#[cfg(not(target_family = "wasm"))] use deadpool_sync::InteractError; use itertools::Itertools; +use matrix_sdk_base::SendOutsideWasm; use matrix_sdk_store_encryption::StoreCipher; use ruma::{OwnedEventId, OwnedRoomId, serde::Raw, time::SystemTime}; use rusqlite::{OptionalExtension, Params, Row, Statement, Transaction, limits::Limit}; @@ -65,55 +69,62 @@ impl rusqlite::ToSql for Key { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] pub(crate) trait SqliteAsyncConnExt { async fn execute

( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, params: P, ) -> rusqlite::Result where - P: Params + Send + 'static; + P: Params + SendOutsideWasm + 'static; - async fn execute_batch(&self, sql: impl AsRef + Send + 'static) -> rusqlite::Result<()>; + async fn execute_batch( + &self, + sql: impl AsRef + SendOutsideWasm + 'static, + ) -> rusqlite::Result<()>; + #[allow(dead_code)] async fn prepare( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, f: F, ) -> rusqlite::Result where - T: Send + 'static, - F: FnOnce(Statement<'_>) -> rusqlite::Result + Send + 'static; + T: SendOutsideWasm + 'static, + F: FnOnce(Statement<'_>) -> rusqlite::Result + SendOutsideWasm + 'static; async fn query_row( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, params: P, f: F, ) -> rusqlite::Result where - T: Send + 'static, - P: Params + Send + 'static, - F: FnOnce(&Row<'_>) -> rusqlite::Result + Send + 'static; + T: SendOutsideWasm + 'static, + P: Params + SendOutsideWasm + 'static, + F: FnOnce(&Row<'_>) -> rusqlite::Result + SendOutsideWasm + 'static; + #[allow(dead_code)] async fn query_many( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, params: P, f: F, ) -> rusqlite::Result> where - T: Send + 'static, - P: Params + Send + 'static, - F: FnMut(&Row<'_>) -> rusqlite::Result + Send + 'static; + T: SendOutsideWasm + 'static, + P: Params + SendOutsideWasm + 'static, + F: FnMut(&Row<'_>) -> rusqlite::Result + SendOutsideWasm + 'static; async fn with_transaction(&self, f: F) -> Result where - T: Send + 'static, - E: From + Send + 'static, - F: FnOnce(&Transaction<'_>) -> Result + Send + 'static; + T: SendOutsideWasm + 'static, + E: From + SendOutsideWasm + 'static, + F: FnOnce(&Transaction<'_>) -> Result + SendOutsideWasm + 'static; + #[allow(dead_code)] async fn chunk_large_query_over( &self, mut keys_to_chunk: Vec, @@ -121,8 +132,8 @@ pub(crate) trait SqliteAsyncConnExt { do_query: Query, ) -> Result> where - Res: Send + 'static, - Query: Fn(&Transaction<'_>, Vec) -> Result> + Send + 'static; + Res: SendOutsideWasm + 'static, + Query: Fn(&Transaction<'_>, Vec) -> Result> + SendOutsideWasm + 'static; /// Apply the [`RuntimeConfig`]. /// @@ -240,22 +251,26 @@ pub(crate) trait SqliteAsyncConnExt { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl SqliteAsyncConnExt for SqliteAsyncConn { async fn execute

( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, params: P, ) -> rusqlite::Result where - P: Params + Send + 'static, + P: Params + SendOutsideWasm + 'static, { self.interact(move |conn| conn.execute(sql.as_ref(), params)) .await .map_err(map_interact_err)? } - async fn execute_batch(&self, sql: impl AsRef + Send + 'static) -> rusqlite::Result<()> { + async fn execute_batch( + &self, + sql: impl AsRef + SendOutsideWasm + 'static, + ) -> rusqlite::Result<()> { self.interact(move |conn| conn.execute_batch(sql.as_ref())) .await .map_err(map_interact_err)? @@ -263,26 +278,26 @@ impl SqliteAsyncConnExt for SqliteAsyncConn { async fn prepare( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, f: F, ) -> rusqlite::Result where - T: Send + 'static, - F: FnOnce(Statement<'_>) -> rusqlite::Result + Send + 'static, + T: SendOutsideWasm + 'static, + F: FnOnce(Statement<'_>) -> rusqlite::Result + SendOutsideWasm + 'static, { self.interact(move |conn| f(conn.prepare(sql.as_ref())?)).await.map_err(map_interact_err)? } async fn query_row( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, params: P, f: F, ) -> rusqlite::Result where - T: Send + 'static, - P: Params + Send + 'static, - F: FnOnce(&Row<'_>) -> rusqlite::Result + Send + 'static, + T: SendOutsideWasm + 'static, + P: Params + SendOutsideWasm + 'static, + F: FnOnce(&Row<'_>) -> rusqlite::Result + SendOutsideWasm + 'static, { self.interact(move |conn| conn.query_row(sql.as_ref(), params, f)) .await @@ -291,14 +306,14 @@ impl SqliteAsyncConnExt for SqliteAsyncConn { async fn query_many( &self, - sql: impl AsRef + Send + 'static, + sql: impl AsRef + SendOutsideWasm + 'static, params: P, f: F, ) -> rusqlite::Result> where - T: Send + 'static, - P: Params + Send + 'static, - F: FnMut(&Row<'_>) -> rusqlite::Result + Send + 'static, + T: SendOutsideWasm + 'static, + P: Params + SendOutsideWasm + 'static, + F: FnMut(&Row<'_>) -> rusqlite::Result + SendOutsideWasm + 'static, { self.interact(move |conn| { let mut stmt = conn.prepare(sql.as_ref())?; @@ -310,9 +325,9 @@ impl SqliteAsyncConnExt for SqliteAsyncConn { async fn with_transaction(&self, f: F) -> Result where - T: Send + 'static, - E: From + Send + 'static, - F: FnOnce(&Transaction<'_>) -> Result + Send + 'static, + T: SendOutsideWasm + 'static, + E: From + SendOutsideWasm + 'static, + F: FnOnce(&Transaction<'_>) -> Result + SendOutsideWasm + 'static, { self.interact(move |conn| { let txn = conn.transaction()?; @@ -338,8 +353,8 @@ impl SqliteAsyncConnExt for SqliteAsyncConn { do_query: Query, ) -> Result> where - Res: Send + 'static, - Query: Fn(&Transaction<'_>, Vec) -> Result> + Send + 'static, + Res: SendOutsideWasm + 'static, + Query: Fn(&Transaction<'_>, Vec) -> Result> + SendOutsideWasm + 'static, { self.with_transaction(move |txn| { txn.chunk_large_query_over(keys_to_chunk, result_capacity, do_query) @@ -348,6 +363,7 @@ impl SqliteAsyncConnExt for SqliteAsyncConn { } } +#[cfg(not(target_family = "wasm"))] /// Map an [`InteractError`] into a [`rusqlite::Error`]. /// /// An [`InteractError::Panic`] will panic. An [`InteractError::Cancelled`] will @@ -363,6 +379,16 @@ fn map_interact_err(error: InteractError) -> rusqlite::Error { } } +#[cfg(target_family = "wasm")] +/// An unreachable function to avoid having to put conditional compilation +/// everywhere we want to use [`ConnectionWrapper::interact()`]. +/// +/// This function should not be reachable under normal circumstance since +/// [`ConnectionWrapper::interact()`] return [`Infallible`] as an error. +fn map_interact_err(_error: Infallible) -> rusqlite::Error { + unreachable!() +} + pub(crate) trait SqliteTransactionExt { fn chunk_large_query_over( &self, @@ -371,8 +397,8 @@ pub(crate) trait SqliteTransactionExt { do_query: Query, ) -> Result> where - Res: Send + 'static, - Query: Fn(&Transaction<'_>, Vec) -> Result> + Send + 'static; + Res: SendOutsideWasm + 'static, + Query: Fn(&Transaction<'_>, Vec) -> Result> + SendOutsideWasm + 'static; } impl SqliteTransactionExt for Transaction<'_> { @@ -383,8 +409,8 @@ impl SqliteTransactionExt for Transaction<'_> { do_query: Query, ) -> Result> where - Res: Send + 'static, - Query: Fn(&Transaction<'_>, Vec) -> Result> + Send + 'static, + Res: SendOutsideWasm + 'static, + Query: Fn(&Transaction<'_>, Vec) -> Result> + SendOutsideWasm + 'static, { // Divide by 2 to allow space for more static parameters (not part of // `keys_to_chunk`). @@ -434,14 +460,16 @@ pub(crate) trait SqliteKeyValueStoreConnExt { /// Store the given value for the given key. fn set_kv(&self, key: &str, value: &[u8]) -> rusqlite::Result<()>; + #[allow(dead_code)] /// Store the given value for the given key by serializing it. - fn set_serialized_kv(&self, key: &str, value: T) -> Result<()> { + fn set_serialized_kv(&self, key: &str, value: T) -> Result<()> { let serialized_value = rmp_serde::to_vec_named(&value)?; self.set_kv(key, &serialized_value)?; Ok(()) } + #[allow(dead_code)] /// Removes the current key and value if exists. fn clear_kv(&self, key: &str) -> rusqlite::Result<()>; @@ -477,7 +505,8 @@ impl SqliteKeyValueStoreConnExt for rusqlite::Connection { /// "value" BLOB NOT NULL /// ); /// ``` -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] pub(crate) trait SqliteKeyValueStoreAsyncConnExt: SqliteAsyncConnExt { /// Whether the `kv` table exists in this database. async fn kv_table_exists(&self) -> rusqlite::Result { @@ -497,6 +526,7 @@ pub(crate) trait SqliteKeyValueStoreAsyncConnExt: SqliteAsyncConnExt { .optional() } + #[allow(dead_code)] /// Get the stored serialized value for the given key. async fn get_serialized_kv(&self, key: &str) -> Result> { let Some(bytes) = self.get_kv(key).await? else { @@ -509,13 +539,15 @@ pub(crate) trait SqliteKeyValueStoreAsyncConnExt: SqliteAsyncConnExt { /// Store the given value for the given key. async fn set_kv(&self, key: &str, value: Vec) -> rusqlite::Result<()>; + #[allow(dead_code)] /// Store the given value for the given key by serializing it. - async fn set_serialized_kv( + async fn set_serialized_kv( &self, key: &str, value: T, ) -> Result<()>; + #[allow(dead_code)] /// Clears the given value for the given key. async fn clear_kv(&self, key: &str) -> rusqlite::Result<()>; @@ -569,7 +601,8 @@ pub(crate) trait SqliteKeyValueStoreAsyncConnExt: SqliteAsyncConnExt { } } -#[async_trait] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] impl SqliteKeyValueStoreAsyncConnExt for SqliteAsyncConn { async fn set_kv(&self, key: &str, value: Vec) -> rusqlite::Result<()> { let key = key.to_owned(); @@ -578,7 +611,7 @@ impl SqliteKeyValueStoreAsyncConnExt for SqliteAsyncConn { Ok(()) } - async fn set_serialized_kv( + async fn set_serialized_kv( &self, key: &str, value: T, @@ -604,6 +637,7 @@ pub(crate) fn repeat_vars(count: usize) -> impl fmt::Display { iter::repeat_n("?", count).format(",") } +#[allow(dead_code)] /// Convert the given `SystemTime` to a timestamp, as the number of seconds /// since Unix Epoch. /// @@ -660,21 +694,25 @@ pub(crate) trait EncryptableStore { } } + #[allow(dead_code)] fn serialize_value(&self, value: &impl Serialize) -> Result> { let serialized = rmp_serde::to_vec_named(value)?; self.encode_value(serialized) } + #[allow(dead_code)] fn deserialize_value(&self, value: &[u8]) -> Result { let decoded = self.decode_value(value)?; Ok(rmp_serde::from_slice(&decoded)?) } + #[allow(dead_code)] fn serialize_json(&self, value: &impl Serialize) -> Result> { let serialized = serde_json::to_vec(value)?; self.encode_value(serialized) } + #[allow(dead_code)] fn deserialize_json(&self, data: &[u8]) -> Result { let decoded = self.decode_value(data)?; diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 82eee9a18d4..a4b483bfecd 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -107,6 +107,8 @@ All notable changes to this project will be documented in this file. to true will now trigger a download of all historical keys for the room in question from the client's key backup. ([#6017](https://github.com/matrix-org/matrix-rust-sdk/pull/6017)) +- Support SQLite backed store when compiling to `wasm32-unknown-unknown` target + ([#6329](https://github.com/matrix-org/matrix-rust-sdk/pull/6329)) - Add widget partial support for MSC4039. Allows widgets to download non-encrypted files from the content repository (like avatars). ([#6354](https://github.com/matrix-org/matrix-rust-sdk/pull/6354)) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index d64fd0a68ea..b7baa427c6c 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -36,6 +36,7 @@ e2e-encryption = [ js = [ "matrix-sdk-common/js", "matrix-sdk-base/js", + "matrix-sdk-sqlite?/js", ] sqlite = [ diff --git a/testing/data/storage/alice/README.md b/testing/data/storage/alice/README.md index 315cddc2163..224d95264b9 100644 --- a/testing/data/storage/alice/README.md +++ b/testing/data/storage/alice/README.md @@ -5,4 +5,11 @@ The database file is exported using the Android Studio `Device Explorer` tool (i In order to get the passphrase a breakpoint must be added in the ClientBuilder to get it. Passphrase: `/rCia2fYAJ+twCZ1Xm2mxFCYcmJdyzkdJjwtgXsziWpYS/UeNxnixuSieuwZXm+x1VsJHmWplH+QIQBZpEGZtC9/S/l8xK+WOCesmET0o6yJ/KP73ofDtjBlnNpPwuHLKFpyTbyicpCgQ4UT+5EUBuJ08TY9Ujdf1D13k5kr5tSZUefDKKCuG1fCRqlU8ByRas1PMQsZxT2W8t7QgBrQiiGmhpo/OTi4hfx97GOxncKcxTzppiYQNoHs/f15+XXQD7/oiCcqRIuUlXNsU6hRpFGmbYx2Pi1eyQViQCtB5dAEiSD0N8U81wXYnpynuTPtnL+hfnOJIn7Sy7mkERQeKg` -Username is @alice:localhost \ No newline at end of file +Username is @alice:localhost + +# Note for WASM environment + +The database file will be prefixed with `*.wasm.db` instead of `*.sqlite3`. +The provided `matrix-sdk-crypto.sqlite3` data format isn't compatible with SQLite compiled to WebAssembly. +The transcoded `matrix-sdk-crypto.wasm.db` was generated by dumping `matrix-sdk-crypto.sqlite3` +and convert them to WASM compatible format via `https://sqlight.dev/` export tool. diff --git a/testing/data/storage/alice/matrix-sdk-crypto.wasm.db b/testing/data/storage/alice/matrix-sdk-crypto.wasm.db new file mode 100644 index 00000000000..482fa6347b4 Binary files /dev/null and b/testing/data/storage/alice/matrix-sdk-crypto.wasm.db differ diff --git a/testing/data/storage/matrix-sdk-crypto.wasm.db b/testing/data/storage/matrix-sdk-crypto.wasm.db new file mode 100644 index 00000000000..9cbd08ed497 Binary files /dev/null and b/testing/data/storage/matrix-sdk-crypto.wasm.db differ diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index aed4a59c819..a97de0289d7 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -154,6 +154,11 @@ enum WasmFeatureSet { MatrixSdkIndexeddbStoresNoCrypto, /// Check `matrix-sdk` crate with `indexeddb` and `e2e-encryption` features MatrixSdkIndexeddbStores, + /// Check `matrix-sdk` crate with `sqlite` feature (but not + /// `e2e-encryption`) + MatrixSdkSqliteStoresNoCrypto, + /// Check `matrix-sdk` crate with `sqlite` and `e2e-encryption` features + MatrixSdkSqliteStores, /// Check `matrix-sdk-indexeddb` crate with all features IndexeddbAllFeatures, /// Check `matrix-sdk-indexeddb` crate with `e2e-encryption` feature @@ -163,6 +168,18 @@ enum WasmFeatureSet { /// Equivalent to `indexeddb-all-features`, `indexeddb-crypto` and /// `indexeddb-state` Indexeddb, + /// Check `matrix-sdk-sqlite` crate with all features + SqliteAllFeatures, + /// Check `matrix-sdk-sqlite` crate with `state-store` and + /// `crypto-store` feature + SqliteCrypto, + /// Check `matrix-sdk-sqlite` crate with `state-store` feature + SqliteState, + /// Check `matrix-sdk-sqlite` crate with `event-cache` feature + SqliteCache, + /// Equivalent to `sqlite-all-features`, `sqlite-crypto`, `sqlite-state`, + /// and `sqlite-cache` + Sqlite, } impl CiArgs { @@ -366,6 +383,14 @@ fn run_wasm_checks(cmd: Option) -> Result<()> { return Ok(()); } + if let Some(WasmFeatureSet::Sqlite) = cmd { + run_wasm_checks(Some(WasmFeatureSet::SqliteAllFeatures))?; + run_wasm_checks(Some(WasmFeatureSet::SqliteCrypto))?; + run_wasm_checks(Some(WasmFeatureSet::SqliteState))?; + run_wasm_checks(Some(WasmFeatureSet::SqliteCache))?; + return Ok(()); + } + let args = BTreeMap::from([ (WasmFeatureSet::MatrixSdkQrcode, "-p matrix-sdk-qrcode --features js"), (WasmFeatureSet::MatrixSdkNoDefault, "-p matrix-sdk --no-default-features --features js"), @@ -380,6 +405,14 @@ fn run_wasm_checks(cmd: Option) -> Result<()> { WasmFeatureSet::MatrixSdkIndexeddbStores, "-p matrix-sdk --no-default-features --features js,indexeddb,e2e-encryption", ), + ( + WasmFeatureSet::MatrixSdkSqliteStoresNoCrypto, + "-p matrix-sdk --no-default-features --features js,sqlite,bundled-sqlite", + ), + ( + WasmFeatureSet::MatrixSdkSqliteStores, + "-p matrix-sdk --no-default-features --features js,sqlite,bundled-sqlite,e2e-encryption", + ), (WasmFeatureSet::IndexeddbAllFeatures, "-p matrix-sdk-indexeddb"), ( WasmFeatureSet::IndexeddbCrypto, @@ -389,6 +422,22 @@ fn run_wasm_checks(cmd: Option) -> Result<()> { WasmFeatureSet::IndexeddbState, "-p matrix-sdk-indexeddb --no-default-features --features state-store", ), + ( + WasmFeatureSet::SqliteAllFeatures, + "-p matrix-sdk-sqlite --no-default-features --features js,crypto-store,experimental-encrypted-state-events,state-store,event-cache", + ), + ( + WasmFeatureSet::SqliteCrypto, + "-p matrix-sdk-sqlite --no-default-features --features js,state-store,crypto-store", + ), + ( + WasmFeatureSet::SqliteState, + "-p matrix-sdk-sqlite --no-default-features --features js,state-store", + ), + ( + WasmFeatureSet::SqliteCache, + "-p matrix-sdk-sqlite --no-default-features --features js,event-cache", + ), ]); let sh = sh(); @@ -422,6 +471,14 @@ fn run_wasm_pack_tests(cmd: Option, runner: WasmTestRunner) -> R return Ok(()); } + if let Some(WasmFeatureSet::Sqlite) = cmd { + run_wasm_pack_tests(Some(WasmFeatureSet::SqliteAllFeatures), runner)?; + run_wasm_pack_tests(Some(WasmFeatureSet::SqliteCache), runner)?; + run_wasm_pack_tests(Some(WasmFeatureSet::SqliteState), runner)?; + run_wasm_pack_tests(Some(WasmFeatureSet::SqliteCrypto), runner)?; + return Ok(()); + } + let args = BTreeMap::from([ (WasmFeatureSet::MatrixSdkQrcode, ("crates/matrix-sdk-qrcode", "--features js")), ( @@ -450,6 +507,52 @@ fn run_wasm_pack_tests(cmd: Option, runner: WasmTestRunner) -> R WasmFeatureSet::IndexeddbState, ("crates/matrix-sdk-indexeddb", "--no-default-features --features state-store"), ), + // SQLite WASM test suites has to be ran in release mode due to + // a harmless debug assertion when closing database. + // + // Ref: https://github.com/Spxg/sqlite-wasm-rs/blob/master/crates/sqlite-wasm-vfs/src/sahpool.rs#L672 + ( + WasmFeatureSet::MatrixSdkSqliteStoresNoCrypto, + ( + "crates/matrix-sdk", + "--no-default-features --features js,sqlite,bundled-sqlite,testing --lib --release", + ), + ), + ( + WasmFeatureSet::MatrixSdkSqliteStores, + ( + "crates/matrix-sdk", + "--no-default-features --features js,sqlite,bundled-sqlite,e2e-encryption,testing --lib --release", + ), + ), + ( + WasmFeatureSet::SqliteAllFeatures, + ( + "crates/matrix-sdk-sqlite", + "--features js,state-store,experimental-encrypted-state-events,crypto-store,event-cache --release", + ), + ), + ( + WasmFeatureSet::SqliteCrypto, + ( + "crates/matrix-sdk-sqlite", + "--no-default-features --features js,state-store,crypto-store --release", + ), + ), + ( + WasmFeatureSet::SqliteState, + ( + "crates/matrix-sdk-sqlite", + "--no-default-features --features js,state-store --release", + ), + ), + ( + WasmFeatureSet::SqliteCache, + ( + "crates/matrix-sdk-sqlite", + "--no-default-features --features js,event-cache --release", + ), + ), ]); let sh = sh();