- Add WebAssembly target support to build for web browsers. - Implement IndexedDB-backed key-value store for WASM. - Add new WASM dependencies to Cargo.toml. - Improve large number handling in Rhai example script. - Refactor key-value store to handle both SlateDB and IndexedDB.
701 lines
31 KiB
Rust
701 lines
31 KiB
Rust
//! IndexedDB-backed key-value store implementation for WebAssembly.
|
|
|
|
use crate::hero_vault::kvs::error::{KvsError, Result};
|
|
use serde::{de::DeserializeOwned, Serialize};
|
|
use std::sync::{Arc, Mutex};
|
|
use cfg_if::cfg_if;
|
|
|
|
// This implementation is only available for WebAssembly
|
|
cfg_if! {
|
|
if #[cfg(target_arch = "wasm32")] {
|
|
use wasm_bindgen::prelude::*;
|
|
use js_sys::{Promise, Object, Reflect, Array};
|
|
use web_sys::{
|
|
IdbDatabase, IdbOpenDbRequest, IdbFactory,
|
|
IdbTransaction, IdbObjectStore, IdbKeyRange,
|
|
window
|
|
};
|
|
use std::collections::HashMap;
|
|
use wasm_bindgen_futures::JsFuture;
|
|
|
|
/// A key-value store backed by IndexedDB for WebAssembly environments.
|
|
#[derive(Clone)]
|
|
pub struct IndexedDbStore {
|
|
/// The name of the store
|
|
name: String,
|
|
/// The IndexedDB database
|
|
db: Arc<Mutex<Option<IdbDatabase>>>,
|
|
/// Cache of key-value pairs to avoid frequent IndexedDB accesses
|
|
cache: Arc<Mutex<HashMap<String, String>>>,
|
|
/// Whether the store is encrypted
|
|
encrypted: bool,
|
|
/// Object store name within IndexedDB
|
|
store_name: String,
|
|
}
|
|
|
|
impl IndexedDbStore {
|
|
/// Creates a new IndexedDbStore.
|
|
///
|
|
/// Note: In WebAssembly, this function must be called in an async context.
|
|
pub async fn new(name: &str, encrypted: bool) -> Result<Self> {
|
|
let window = window().ok_or_else(|| KvsError::Other("No window object available".to_string()))?;
|
|
let indexed_db = window.indexed_db()
|
|
.map_err(|_| KvsError::Other("Failed to get IndexedDB factory".to_string()))?
|
|
.ok_or_else(|| KvsError::Other("IndexedDB not available".to_string()))?;
|
|
|
|
// The store name in IndexedDB
|
|
let store_name = "kvs-data";
|
|
|
|
// Open the database
|
|
let db_name = format!("hero-vault-{}", name);
|
|
let open_request = indexed_db.open_with_u32(&db_name, 1)
|
|
.map_err(|_| KvsError::Other("Failed to open IndexedDB database".to_string()))?;
|
|
|
|
// Set up database schema on upgrade needed
|
|
let store_name_clone = store_name.clone();
|
|
let upgrade_needed_closure = Closure::wrap(Box::new(move |event: web_sys::IdbVersionChangeEvent| {
|
|
let db = event.target()
|
|
.and_then(|target| target.dyn_into::<IdbOpenDbRequest>().ok())
|
|
.and_then(|request| request.result().ok())
|
|
.and_then(|result| result.dyn_into::<IdbDatabase>().ok());
|
|
|
|
if let Some(db) = db {
|
|
// Create the object store if it doesn't exist
|
|
if !Array::from(&db.object_store_names()).includes(&JsValue::from_str(&store_name_clone)) {
|
|
db.create_object_store(&store_name_clone)
|
|
.expect("Failed to create object store");
|
|
}
|
|
}
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
open_request.set_onupgradeneeded(Some(upgrade_needed_closure.as_ref().unchecked_ref()));
|
|
upgrade_needed_closure.forget();
|
|
|
|
// Wait for the database to open
|
|
let request_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
resolve.call0(&JsValue::NULL)
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
open_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
|
open_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
success_callback.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
JsFuture::from(request_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to open IndexedDB database".to_string()))?;
|
|
|
|
// Get the database object
|
|
let db = open_request.result()
|
|
.map_err(|_| KvsError::Other("Failed to get IndexedDB database".to_string()))?
|
|
.dyn_into::<IdbDatabase>()
|
|
.map_err(|_| KvsError::Other("Invalid database object".to_string()))?;
|
|
|
|
// Initialize the cache by loading all keys and values
|
|
let cache = Arc::new(Mutex::new(HashMap::new()));
|
|
|
|
// Create the store
|
|
let store = IndexedDbStore {
|
|
name: name.to_string(),
|
|
db: Arc::new(Mutex::new(Some(db))),
|
|
cache,
|
|
encrypted,
|
|
store_name: store_name.to_string(),
|
|
};
|
|
|
|
// Initialize the cache
|
|
store.initialize_cache().await?;
|
|
|
|
Ok(store)
|
|
}
|
|
|
|
/// Initializes the cache by loading all keys and values from IndexedDB.
|
|
async fn initialize_cache(&self) -> Result<()> {
|
|
// Get the database
|
|
let db_guard = self.db.lock().unwrap();
|
|
let db = db_guard.as_ref()
|
|
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
|
|
|
// Create a transaction
|
|
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readonly")
|
|
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
|
|
|
// Get the object store
|
|
let store = transaction.object_store(&self.store_name)
|
|
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
|
|
|
// Open a cursor to iterate through all entries
|
|
let cursor_request = store.open_cursor()
|
|
.map_err(|_| KvsError::Other("Failed to open cursor".to_string()))?;
|
|
|
|
// Load all entries into the cache
|
|
let cache = Arc::clone(&self.cache);
|
|
let load_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
resolve.call0(&JsValue::NULL)
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let onsuccess = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
let cursor = event
|
|
.target()
|
|
.and_then(|target| target.dyn_into::<web_sys::IdbRequest>().ok())
|
|
.and_then(|request| request.result().ok())
|
|
.and_then(|result| result.dyn_into::<web_sys::IdbCursorWithValue>().ok());
|
|
|
|
if let Some(cursor) = cursor {
|
|
// Get the key and value
|
|
let key = cursor.key().as_string()
|
|
.expect("Failed to get key as string");
|
|
|
|
let value = cursor.value()
|
|
.as_string()
|
|
.expect("Failed to get value as string");
|
|
|
|
// Add to cache
|
|
let mut cache_lock = cache.lock().unwrap();
|
|
cache_lock.insert(key, value);
|
|
|
|
// Continue to next entry
|
|
cursor.continue_()
|
|
.expect("Failed to continue cursor");
|
|
} else {
|
|
// No more entries, resolve the promise
|
|
success_callback.as_ref().unchecked_ref::<js_sys::Function>()
|
|
.call0(&JsValue::NULL)
|
|
.expect("Failed to call success callback");
|
|
}
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
cursor_request.set_onsuccess(Some(onsuccess.as_ref().unchecked_ref()));
|
|
cursor_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
onsuccess.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
JsFuture::from(load_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to load cache".to_string()))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets a value in IndexedDB and updates the cache.
|
|
async fn set_in_db<K, V>(&self, key: K, value: &V) -> Result<()>
|
|
where
|
|
K: ToString,
|
|
V: Serialize,
|
|
{
|
|
let key_str = key.to_string();
|
|
let serialized = serde_json::to_string(value)?;
|
|
|
|
// Get the database
|
|
let db_guard = self.db.lock().unwrap();
|
|
let db = db_guard.as_ref()
|
|
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
|
|
|
// Create a transaction
|
|
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readwrite")
|
|
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
|
|
|
// Get the object store
|
|
let store = transaction.object_store(&self.store_name)
|
|
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
|
|
|
// Put the value in the store
|
|
let put_request = store.put_with_key(&JsValue::from_str(&serialized), &JsValue::from_str(&key_str))
|
|
.map_err(|_| KvsError::Other("Failed to put value in store".to_string()))?;
|
|
|
|
// Wait for the request to complete
|
|
let put_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
resolve.call0(&JsValue::NULL)
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
put_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
|
put_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
success_callback.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
JsFuture::from(put_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to put value in store".to_string()))?;
|
|
|
|
// Update the cache
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.insert(key_str, serialized);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Gets a value from the cache or IndexedDB.
|
|
async fn get_from_db<K>(&self, key: K) -> Result<Option<String>>
|
|
where
|
|
K: ToString,
|
|
{
|
|
let key_str = key.to_string();
|
|
|
|
// Check the cache first
|
|
{
|
|
let cache_lock = self.cache.lock().unwrap();
|
|
if let Some(value) = cache_lock.get(&key_str) {
|
|
return Ok(Some(value.clone()));
|
|
}
|
|
}
|
|
|
|
// If not in cache, get from IndexedDB
|
|
let db_guard = self.db.lock().unwrap();
|
|
let db = db_guard.as_ref()
|
|
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
|
|
|
// Create a transaction
|
|
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readonly")
|
|
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
|
|
|
// Get the object store
|
|
let store = transaction.object_store(&self.store_name)
|
|
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
|
|
|
// Get the value from the store
|
|
let get_request = store.get(&JsValue::from_str(&key_str))
|
|
.map_err(|_| KvsError::Other("Failed to get value from store".to_string()))?;
|
|
|
|
// Wait for the request to complete
|
|
let value = JsFuture::from(get_request.into())
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to get value from store".to_string()))?;
|
|
|
|
if value.is_undefined() || value.is_null() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let value_str = value.as_string()
|
|
.ok_or_else(|| KvsError::Deserialization("Failed to convert value to string".to_string()))?;
|
|
|
|
// Update the cache
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.insert(key_str, value_str.clone());
|
|
|
|
Ok(Some(value_str))
|
|
}
|
|
|
|
/// Deletes a value from IndexedDB and the cache.
|
|
async fn delete_from_db<K>(&self, key: K) -> Result<bool>
|
|
where
|
|
K: ToString,
|
|
{
|
|
let key_str = key.to_string();
|
|
|
|
// Check if the key exists in cache
|
|
let exists_in_cache = {
|
|
let cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.contains_key(&key_str)
|
|
};
|
|
|
|
// Get the database
|
|
let db_guard = self.db.lock().unwrap();
|
|
let db = db_guard.as_ref()
|
|
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
|
|
|
// Create a transaction
|
|
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readwrite")
|
|
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
|
|
|
// Get the object store
|
|
let store = transaction.object_store(&self.store_name)
|
|
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
|
|
|
// Check if the key exists in IndexedDB
|
|
let key_range = IdbKeyRange::only(&JsValue::from_str(&key_str))
|
|
.map_err(|_| KvsError::Other("Failed to create key range".to_string()))?;
|
|
|
|
let count_request = store.count_with_key(&key_range)
|
|
.map_err(|_| KvsError::Other("Failed to count key".to_string()))?;
|
|
|
|
let count_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
let request = event
|
|
.target()
|
|
.and_then(|target| target.dyn_into::<web_sys::IdbRequest>().ok())
|
|
.expect("Failed to get request");
|
|
|
|
resolve.call1(&JsValue::NULL, &request.result().unwrap())
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
count_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
|
count_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
success_callback.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
let count = JsFuture::from(count_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to count key".to_string()))?;
|
|
|
|
let exists_in_db = count.as_f64().unwrap_or(0.0) > 0.0;
|
|
|
|
if !exists_in_cache && !exists_in_db {
|
|
return Ok(false);
|
|
}
|
|
|
|
// Delete the key from IndexedDB
|
|
let delete_request = store.delete(&JsValue::from_str(&key_str))
|
|
.map_err(|_| KvsError::Other("Failed to delete key".to_string()))?;
|
|
|
|
let delete_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
resolve.call0(&JsValue::NULL)
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
delete_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
|
delete_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
success_callback.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
JsFuture::from(delete_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to delete key".to_string()))?;
|
|
|
|
// Remove from cache
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.remove(&key_str);
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Gets all keys from IndexedDB.
|
|
async fn get_all_keys(&self) -> Result<Vec<String>> {
|
|
// Try to get keys from cache first
|
|
{
|
|
let cache_lock = self.cache.lock().unwrap();
|
|
if !cache_lock.is_empty() {
|
|
return Ok(cache_lock.keys().cloned().collect());
|
|
}
|
|
}
|
|
|
|
// Get the database
|
|
let db_guard = self.db.lock().unwrap();
|
|
let db = db_guard.as_ref()
|
|
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
|
|
|
// Create a transaction
|
|
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readonly")
|
|
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
|
|
|
// Get the object store
|
|
let store = transaction.object_store(&self.store_name)
|
|
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
|
|
|
// Get all keys
|
|
let keys_request = store.get_all_keys()
|
|
.map_err(|_| KvsError::Other("Failed to get keys".to_string()))?;
|
|
|
|
let keys_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
let request = event
|
|
.target()
|
|
.and_then(|target| target.dyn_into::<web_sys::IdbRequest>().ok())
|
|
.expect("Failed to get request");
|
|
|
|
resolve.call1(&JsValue::NULL, &request.result().unwrap())
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
keys_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
|
keys_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
success_callback.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
let keys_array = JsFuture::from(keys_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to get keys".to_string()))?;
|
|
|
|
let keys_array = Array::from(&keys_array);
|
|
let mut keys = Vec::new();
|
|
|
|
for i in 0..keys_array.length() {
|
|
let key = keys_array.get(i);
|
|
if let Some(key_str) = key.as_string() {
|
|
keys.push(key_str);
|
|
}
|
|
}
|
|
|
|
Ok(keys)
|
|
}
|
|
|
|
/// Clears all key-value pairs from the store.
|
|
async fn clear_db(&self) -> Result<()> {
|
|
// Get the database
|
|
let db_guard = self.db.lock().unwrap();
|
|
let db = db_guard.as_ref()
|
|
.ok_or_else(|| KvsError::Other("Database not initialized".to_string()))?;
|
|
|
|
// Create a transaction
|
|
let transaction = db.transaction_with_str_and_mode(&self.store_name, "readwrite")
|
|
.map_err(|_| KvsError::Other("Failed to create transaction".to_string()))?;
|
|
|
|
// Get the object store
|
|
let store = transaction.object_store(&self.store_name)
|
|
.map_err(|_| KvsError::Other("Failed to get object store".to_string()))?;
|
|
|
|
// Clear the store
|
|
let clear_request = store.clear()
|
|
.map_err(|_| KvsError::Other("Failed to clear store".to_string()))?;
|
|
|
|
let clear_promise = Promise::new(&mut |resolve, reject| {
|
|
let success_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
resolve.call0(&JsValue::NULL)
|
|
.expect("Failed to resolve promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| {
|
|
reject.call0(&JsValue::NULL)
|
|
.expect("Failed to reject promise");
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
clear_request.set_onsuccess(Some(success_callback.as_ref().unchecked_ref()));
|
|
clear_request.set_onerror(Some(error_callback.as_ref().unchecked_ref()));
|
|
|
|
success_callback.forget();
|
|
error_callback.forget();
|
|
});
|
|
|
|
JsFuture::from(clear_promise)
|
|
.await
|
|
.map_err(|_| KvsError::Other("Failed to clear store".to_string()))?;
|
|
|
|
// Clear the cache
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.clear();
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen(js_namespace = console)]
|
|
fn log(s: &str);
|
|
}
|
|
} else {
|
|
// For non-WebAssembly targets, provide a placeholder implementation
|
|
use std::fmt;
|
|
|
|
/// A placeholder struct for IndexedDbStore on non-WebAssembly platforms.
|
|
#[derive(Clone)]
|
|
pub struct IndexedDbStore {
|
|
name: String,
|
|
}
|
|
|
|
impl IndexedDbStore {
|
|
/// Creates a new IndexedDbStore.
|
|
pub fn new(_name: &str, _encrypted: bool) -> Result<Self> {
|
|
Err(KvsError::Other("IndexedDbStore is only available in WebAssembly".to_string()))
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for IndexedDbStore {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("IndexedDbStore")
|
|
.field("name", &self.name)
|
|
.field("note", &"Placeholder for non-WebAssembly platforms")
|
|
.finish()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only provide the full KVStore implementation for WebAssembly
|
|
#[cfg(target_arch = "wasm32")]
|
|
impl KVStore for IndexedDbStore {
|
|
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
|
where
|
|
K: ToString,
|
|
V: Serialize,
|
|
{
|
|
// For WebAssembly, we need to use the async version but in a synchronous context
|
|
let key_str = key.to_string();
|
|
let serialized = serde_json::to_string(value)?;
|
|
|
|
// Update the cache immediately
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.insert(key_str.clone(), serialized.clone());
|
|
|
|
// Start the async operation but don't wait for it
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let db_guard = self.db.lock().unwrap();
|
|
if let Some(db) = db_guard.as_ref() {
|
|
// Create a transaction
|
|
if let Ok(transaction) = db.transaction_with_str_and_mode(&self.store_name, "readwrite") {
|
|
// Get the object store
|
|
if let Ok(store) = transaction.object_store(&self.store_name) {
|
|
// Put the value in the store
|
|
let _ = store.put_with_key(
|
|
&JsValue::from_str(&serialized),
|
|
&JsValue::from_str(&key_str)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get<K, V>(&self, key: K) -> Result<V>
|
|
where
|
|
K: ToString,
|
|
V: DeserializeOwned,
|
|
{
|
|
// For WebAssembly, we need to use the cache for synchronous operations
|
|
let key_str = key.to_string();
|
|
|
|
// Check the cache first
|
|
let cache_lock = self.cache.lock().unwrap();
|
|
if let Some(value) = cache_lock.get(&key_str) {
|
|
let value = serde_json::from_str(value)?;
|
|
return Ok(value);
|
|
}
|
|
|
|
// If not in cache, we can't do a synchronous IndexedDB request
|
|
// This is a limitation of WebAssembly integration
|
|
Err(KvsError::KeyNotFound(key_str))
|
|
}
|
|
|
|
fn delete<K>(&self, key: K) -> Result<()>
|
|
where
|
|
K: ToString,
|
|
{
|
|
let key_str = key.to_string();
|
|
|
|
// Remove from cache immediately
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
if cache_lock.remove(&key_str).is_none() {
|
|
return Err(KvsError::KeyNotFound(key_str.clone()));
|
|
}
|
|
|
|
// Start the async operation but don't wait for it
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let db_guard = self.db.lock().unwrap();
|
|
if let Some(db) = db_guard.as_ref() {
|
|
// Create a transaction
|
|
if let Ok(transaction) = db.transaction_with_str_and_mode(&self.store_name, "readwrite") {
|
|
// Get the object store
|
|
if let Ok(store) = transaction.object_store(&self.store_name) {
|
|
// Delete the key
|
|
let _ = store.delete(&JsValue::from_str(&key_str));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn contains<K>(&self, key: K) -> Result<bool>
|
|
where
|
|
K: ToString,
|
|
{
|
|
let key_str = key.to_string();
|
|
|
|
// Check the cache first
|
|
let cache_lock = self.cache.lock().unwrap();
|
|
Ok(cache_lock.contains_key(&key_str))
|
|
}
|
|
|
|
fn keys(&self) -> Result<Vec<String>> {
|
|
// Return keys from cache
|
|
let cache_lock = self.cache.lock().unwrap();
|
|
Ok(cache_lock.keys().cloned().collect())
|
|
}
|
|
|
|
fn clear(&self) -> Result<()> {
|
|
// Clear the cache immediately
|
|
let mut cache_lock = self.cache.lock().unwrap();
|
|
cache_lock.clear();
|
|
|
|
// Start the async operation but don't wait for it
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let db_guard = self.db.lock().unwrap();
|
|
if let Some(db) = db_guard.as_ref() {
|
|
// Create a transaction
|
|
if let Ok(transaction) = db.transaction_with_str_and_mode(&self.store_name, "readwrite") {
|
|
// Get the object store
|
|
if let Ok(store) = transaction.object_store(&self.store_name) {
|
|
// Clear the store
|
|
let _ = store.clear();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
fn is_encrypted(&self) -> bool {
|
|
self.encrypted
|
|
}
|
|
}
|
|
|
|
// For creating and managing IndexedDbStore instances
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub async fn create_indexeddb_store(name: &str, encrypted: bool) -> Result<IndexedDbStore> {
|
|
IndexedDbStore::new(name, encrypted).await
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub async fn open_indexeddb_store(name: &str, _password: Option<&str>) -> Result<IndexedDbStore> {
|
|
// For IndexedDB we don't use the password parameter since encryption is handled differently
|
|
// We just open the store with the given name
|
|
IndexedDbStore::new(name, false).await
|
|
}
|