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:
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,