//! 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>>, /// Cache of key-value pairs to avoid frequent IndexedDB accesses cache: Arc>>, /// 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 { 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::().ok()) .and_then(|request| request.result().ok()) .and_then(|result| result.dyn_into::().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); 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); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); 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::() .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); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); let onsuccess = Closure::wrap(Box::new(move |event: web_sys::Event| { let cursor = event .target() .and_then(|target| target.dyn_into::().ok()) .and_then(|request| request.result().ok()) .and_then(|result| result.dyn_into::().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::() .call0(&JsValue::NULL) .expect("Failed to call success callback"); } }) as Box); 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(&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); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); 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(&self, key: K) -> Result> 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(&self, key: K) -> Result 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::().ok()) .expect("Failed to get request"); resolve.call1(&JsValue::NULL, &request.result().unwrap()) .expect("Failed to resolve promise"); }) as Box); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); 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); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); 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> { // 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::().ok()) .expect("Failed to get request"); resolve.call1(&JsValue::NULL, &request.result().unwrap()) .expect("Failed to resolve promise"); }) as Box); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); 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); let error_callback = Closure::wrap(Box::new(move |_event: web_sys::Event| { reject.call0(&JsValue::NULL) .expect("Failed to reject promise"); }) as Box); 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 { 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(&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(&self, key: K) -> Result 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(&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(&self, key: K) -> Result 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> { // 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::new(name, encrypted).await } #[cfg(target_arch = "wasm32")] pub async fn open_indexeddb_store(name: &str, _password: Option<&str>) -> Result { // 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 }