diff --git a/Cargo.lock b/Cargo.lock index be45acb55..b5a490a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "accessory" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e416a3ab45838bac2ab2d81b1088d738d7b2d2c5272a54d39366565a29bd80" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "addr2line" version = "0.25.1" @@ -2211,6 +2223,20 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "delegate-display" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9926686c832494164c33a36bf65118f4bd6e704000b58c94681bf62e9ad67a74" +dependencies = [ + "impartial-ord", + "itoa", + "macroific", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "der" version = "0.7.10" @@ -2461,6 +2487,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy_constructor" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a27643a5d05f3a22f5afd6e0d0e6e354f92d37907006f97b84b9cb79082198" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -3241,6 +3279,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impartial-ord" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -3267,6 +3316,40 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[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", + "fancy_constructor", + "indexed_db_futures_macros_internal", + "js-sys", + "sealed", + "smallvec", + "thiserror 2.0.18", + "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.114", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -3499,6 +3582,54 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "macroific" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9" +dependencies = [ + "proc-macro2", + "quote", + "sealed", + "syn 2.0.114", +] + +[[package]] +name = "macroific_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3" +dependencies = [ + "proc-macro2", + "quote", + "sealed", + "syn 2.0.114", +] + +[[package]] +name = "macroific_macro" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "matchers" version = "0.2.0" @@ -4639,6 +4770,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "sec1" version = "0.7.3" @@ -5196,6 +5338,22 @@ dependencies = [ "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.18", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6366,6 +6524,7 @@ dependencies = [ "cc", "hex", "sqlite-wasm-rs", + "sqlite-wasm-vfs", "tempfile", "zeroize", ] diff --git a/walletkit-db/Cargo.toml b/walletkit-db/Cargo.toml index 5c9df6935..624465479 100644 --- a/walletkit-db/Cargo.toml +++ b/walletkit-db/Cargo.toml @@ -19,8 +19,14 @@ categories.workspace = true hex = "0.4" zeroize = "1" +[features] +default = [] +# Enable persistent storage backends (OPFS / IndexedDB) for WASM builds. +wasm-storage = ["sqlite-wasm-vfs"] + [target.'cfg(target_arch = "wasm32")'.dependencies] sqlite-wasm-rs = { version = "0.5", features = ["sqlite3mc"] } +sqlite-wasm-vfs = { version = "0.2", optional = true } [build-dependencies] cc = "1" diff --git a/walletkit-db/src/lib.rs b/walletkit-db/src/lib.rs index b34162730..a36eb38dc 100644 --- a/walletkit-db/src/lib.rs +++ b/walletkit-db/src/lib.rs @@ -8,12 +8,21 @@ //! * **WASM** (`wasm32`): delegated to `sqlite-wasm-rs` (with the `sqlite3mc` //! feature) which ships its own WASM-compiled `sqlite3mc`. //! +//! ## Persistent WASM storage +//! +//! On WASM the default VFS is in-memory (volatile). Enable the `wasm-storage` +//! feature to gain access to the [`wasm_storage`] module which can install +//! OPFS or IndexedDB backed VFS implementations before opening a database. +//! //! Consumer code (vault, cache, cipher config) uses only the safe types //! defined here and never touches raw FFI directly. The `ffi` module is the //! **only** file that contains `unsafe` code or C types. mod ffi; +#[cfg(all(target_arch = "wasm32", feature = "wasm-storage"))] +pub mod wasm_storage; + mod connection; pub mod error; mod statement; diff --git a/walletkit-db/src/wasm_storage.rs b/walletkit-db/src/wasm_storage.rs new file mode 100644 index 000000000..c765211c6 --- /dev/null +++ b/walletkit-db/src/wasm_storage.rs @@ -0,0 +1,112 @@ +//! Persistent storage backends for WASM builds. +//! +//! By default, `sqlite-wasm-rs` uses an in-memory VFS that loses all data on +//! page reload. This module provides two persistent alternatives: +//! +//! * **OPFS** (`sahpool`) — uses the Origin Private File System via +//! `FileSystemSyncAccessHandle`. Requires a **Dedicated Worker** context. +//! Offers full durability. +//! +//! * **IndexedDB** (`relaxed-idb`) — stores database blocks in IndexedDB. +//! Works in **any** browsing context (main thread, worker, etc.). +//! Provides relaxed durability (data is flushed asynchronously). +//! +//! # Usage +//! +//! Call one of the `install_*` functions **once** before opening any database +//! connection. The chosen VFS will be registered as the default so that +//! [`Connection::open`](crate::Connection::open) uses it transparently. +//! +//! ```rust,ignore +//! use std::path::Path; +//! use walletkit_db::Connection; +//! +//! // In a Dedicated Worker — OPFS-backed (full durability): +//! walletkit_db::wasm_storage::install_opfs_sahpool(None).await?; +//! +//! // — or — in any context — IndexedDB-backed (relaxed durability): +//! walletkit_db::wasm_storage::install_relaxed_idb(None).await?; +//! +//! // Then open databases as usual — they will be persisted. +//! let conn = Connection::open(Path::new("app.db"), false)?; +//! ``` + +pub use sqlite_wasm_vfs::sahpool::{ + install as install_sahpool_inner, OpfsSAHPoolCfg, OpfsSAHError, +}; +pub use sqlite_wasm_vfs::relaxed_idb::{ + install as install_idb_inner, RelaxedIdbCfg, RelaxedIdbError, +}; + +/// Errors that can occur during persistent VFS installation. +#[derive(Debug)] +pub enum WasmStorageError { + /// OPFS sahpool VFS installation failed. + Opfs(OpfsSAHError), + /// IndexedDB relaxed-idb VFS installation failed. + Idb(RelaxedIdbError), +} + +impl std::fmt::Display for WasmStorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Opfs(e) => write!(f, "OPFS sahpool VFS install failed: {e}"), + Self::Idb(e) => write!(f, "IndexedDB relaxed-idb VFS install failed: {e}"), + } + } +} + +impl std::error::Error for WasmStorageError {} + +impl From for WasmStorageError { + fn from(e: OpfsSAHError) -> Self { + Self::Opfs(e) + } +} + +impl From for WasmStorageError { + fn from(e: RelaxedIdbError) -> Self { + Self::Idb(e) + } +} + +/// Install the **OPFS `sahpool`** VFS as the default SQLite VFS. +/// +/// This must be called **once** from a **Dedicated Worker** context before +/// opening any database. If `cfg` is `None`, sensible defaults are used. +/// +/// After this call, all databases opened via [`Connection::open`](crate::Connection::open) +/// will be persisted to the Origin Private File System. +/// +/// # Errors +/// +/// Returns [`WasmStorageError::Opfs`] if VFS registration fails (e.g. called +/// outside a Dedicated Worker, or OPFS is not available). +pub async fn install_opfs_sahpool( + cfg: Option, +) -> Result<(), WasmStorageError> { + let cfg = cfg.unwrap_or_default(); + install_sahpool_inner::(&cfg, true).await?; + Ok(()) +} + +/// Install the **IndexedDB `relaxed-idb`** VFS as the default SQLite VFS. +/// +/// This must be called **once** before opening any database. Works in any +/// browsing context (main thread, worker, etc.). If `cfg` is `None`, sensible +/// defaults are used. +/// +/// After this call, all databases opened via [`Connection::open`](crate::Connection::open) +/// will be persisted to IndexedDB with relaxed durability guarantees. +/// +/// # Errors +/// +/// Returns [`WasmStorageError::Idb`] if VFS registration fails. +pub async fn install_relaxed_idb( + cfg: Option, +) -> Result<(), WasmStorageError> { + let cfg = cfg.unwrap_or_default(); + install_idb_inner::(&cfg, true).await?; + Ok(()) +} +