app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 30312865604c202a6d12db2c8bd66bad72b95c2b
parent 305aa2e78fde7063976aa525ac86c54e18560afa
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 19:47:37 +0000

core: add datastore batch set support

- add set_entries api with default fallback
- add idb batch helper for wasm transactions
- implement web datastore override
- cover non-wasm idb batch behavior

Diffstat:
Mcrates/core/src/datastore/types.rs | 16++++++++++++++++
Mcrates/core/src/datastore/web.rs | 40++++++++++++++++++++++++++++++++++++++++
Mcrates/core/src/idb/keyval.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/core/src/idb/mod.rs | 2+-
4 files changed, 135 insertions(+), 3 deletions(-)

diff --git a/crates/core/src/datastore/types.rs b/crates/core/src/datastore/types.rs @@ -35,6 +35,22 @@ pub trait RadrootsClientDatastore { async fn init(&self) -> RadrootsClientDatastoreResult<()>; async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String>; async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String>; + async fn set_entries( + &self, + entries: &[RadrootsClientDatastoreEntry], + ) -> RadrootsClientDatastoreResult<()> { + for entry in entries { + match entry.value.as_deref() { + Some(value) => { + let _ = self.set(&entry.key, value).await?; + } + None => { + let _ = self.del(&entry.key).await?; + } + } + } + Ok(()) + } async fn set_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T> where T: Serialize + DeserializeOwned + Clone; diff --git a/crates/core/src/datastore/web.rs b/crates/core/src/datastore/web.rs @@ -9,9 +9,12 @@ use crate::idb::{IDB_CONFIG_DATASTORE, RadrootsClientIdbConfig}; #[cfg(target_arch = "wasm32")] use crate::idb::RadrootsClientIdbStoreError; use crate::idb::{RadrootsClientWebEncryptedStore, RadrootsClientWebEncryptedStoreConfig}; +#[cfg(target_arch = "wasm32")] +use crate::idb::idb_set_entries; use super::{ RadrootsClientDatastore, + RadrootsClientDatastoreEntry, RadrootsClientDatastoreEntries, RadrootsClientDatastoreError, RadrootsClientDatastoreResult, @@ -163,6 +166,43 @@ impl RadrootsClientDatastore for RadrootsClientWebDatastore { } } + async fn set_entries( + &self, + entries: &[RadrootsClientDatastoreEntry], + ) -> RadrootsClientDatastoreResult<()> { + #[cfg(not(target_arch = "wasm32"))] + { + let _ = entries; + return Err(RadrootsClientDatastoreError::IdbUndefined); + } + #[cfg(target_arch = "wasm32")] + { + let mut encrypted_entries = Vec::with_capacity(entries.len()); + for entry in entries { + let value = match entry.value.as_deref() { + Some(value) => { + let encrypted = self + .encrypted_store + .encrypt_bytes(value.as_bytes()) + .await + .map_err(map_crypto_error)?; + Some(js_sys::Uint8Array::from(&encrypted[..]).into()) + } + None => None, + }; + encrypted_entries.push((entry.key.clone(), value)); + } + idb_set_entries( + self.encrypted_store.get_config().database, + self.encrypted_store.get_config().store, + &encrypted_entries, + ) + .await + .map_err(map_idb_error)?; + Ok(()) + } + } + async fn set_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T> where T: Serialize + DeserializeOwned + Clone, diff --git a/crates/core/src/idb/keyval.rs b/crates/core/src/idb/keyval.rs @@ -9,7 +9,7 @@ use wasm_bindgen::{JsCast, JsValue}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_futures::JsFuture; #[cfg(target_arch = "wasm32")] -use web_sys::{IdbRequest, IdbTransactionMode}; +use web_sys::{IdbRequest, IdbTransaction, IdbTransactionMode}; #[cfg(target_arch = "wasm32")] use super::store::idb_open; @@ -102,6 +102,65 @@ pub async fn idb_set( } #[cfg(target_arch = "wasm32")] +pub async fn idb_set_entries( + database: &str, + store: &str, + entries: &[(String, Option<RadrootsClientIdbValue>)], +) -> Result<(), RadrootsClientIdbStoreError> { + let db = idb_open(database, None, &[]).await?; + let transaction = db + .transaction_with_str_and_mode(store, IdbTransactionMode::Readwrite) + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; + let object_store = transaction + .object_store(store) + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; + let promise = idb_transaction_complete(transaction.clone())?; + for (key, value) in entries { + let key = JsValue::from_str(key); + match value { + Some(value) => { + object_store + .put_with_key(value, &key) + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; + } + None => { + object_store + .delete(&key) + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; + } + } + } + JsFuture::from(promise) + .await + .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?; + db.close(); + Ok(()) +} + +#[cfg(target_arch = "wasm32")] +fn idb_transaction_complete( + transaction: IdbTransaction, +) -> Result<Promise, RadrootsClientIdbStoreError> { + let promise = Promise::new(&mut |resolve, reject| { + let resolve = resolve.clone(); + let on_complete = Closure::wrap(Box::new(move |_event: web_sys::Event| { + let _ = resolve.call0(&JsValue::UNDEFINED); + }) as Box<dyn FnMut(_)>); + transaction.set_oncomplete(Some(on_complete.as_ref().unchecked_ref())); + on_complete.forget(); + + let reject = reject.clone(); + let on_error = Closure::wrap(Box::new(move |_event: web_sys::Event| { + let _ = reject.call1(&JsValue::UNDEFINED, &JsValue::from_str("idb_tx_failed")); + }) as Box<dyn FnMut(_)>); + transaction.set_onerror(Some(on_error.as_ref().unchecked_ref())); + transaction.set_onabort(Some(on_error.as_ref().unchecked_ref())); + on_error.forget(); + }); + Ok(promise) +} + +#[cfg(target_arch = "wasm32")] pub async fn idb_del( database: &str, store: &str, @@ -165,6 +224,15 @@ pub async fn idb_set( } #[cfg(not(target_arch = "wasm32"))] +pub async fn idb_set_entries( + _database: &str, + _store: &str, + _entries: &[(String, Option<RadrootsClientIdbValue>)], +) -> Result<(), RadrootsClientIdbStoreError> { + Err(RadrootsClientIdbStoreError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] pub async fn idb_del( _database: &str, _store: &str, @@ -191,7 +259,7 @@ pub async fn idb_keys( #[cfg(test)] mod tests { - use super::{idb_clear, idb_del, idb_get, idb_keys, idb_set}; + use super::{idb_clear, idb_del, idb_get, idb_keys, idb_set, idb_set_entries}; use crate::idb::RadrootsClientIdbStoreError; #[test] @@ -202,6 +270,14 @@ mod tests { } #[test] + fn non_wasm_keyval_batch_returns_idb_undefined() { + let entries = Vec::new(); + let err = futures::executor::block_on(idb_set_entries("db", "store", &entries)) + .expect_err("idb undefined"); + assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined); + } + + #[test] fn non_wasm_keyval_mutations_return_idb_undefined() { let value = (); let err = futures::executor::block_on(idb_set("db", "store", "key", &value)) diff --git a/crates/core/src/idb/mod.rs b/crates/core/src/idb/mod.rs @@ -33,7 +33,7 @@ pub use config::{ pub use types::RadrootsClientIdbConfig; pub use value::{idb_value_as_bytes, RadrootsClientIdbValue}; pub use error::{RadrootsClientIdbStoreError, RadrootsClientIdbStoreErrorMessage}; -pub use keyval::{idb_clear, idb_del, idb_get, idb_keys, idb_set}; +pub use keyval::{idb_clear, idb_del, idb_get, idb_keys, idb_set, idb_set_entries}; pub use encrypted_store::{ RadrootsClientWebEncryptedStore, RadrootsClientWebEncryptedStoreConfig,