//! Implementation of a simple key-value store using IndexedDB for WebAssembly //! and an in-memory store for testing. use crate::core::kvs::error::{KvsError, Result}; use serde::{de::DeserializeOwned, Serialize}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; #[cfg(target_arch = "wasm32")] use { idb::{Database, DatabaseEvent, Factory, TransactionMode}, js_sys::Promise, wasm_bindgen::prelude::*, wasm_bindgen_futures::JsFuture, }; #[cfg(target_arch = "wasm32")] impl From for KvsError { fn from(err: JsValue) -> Self { KvsError::Other(format!("JavaScript error: {:?}", err)) } } /// A simple key-value store. /// /// In WebAssembly environments, this uses IndexedDB. /// In non-WebAssembly environments, this uses an in-memory store for testing. #[derive(Clone)] pub struct KvsStore { #[cfg(not(target_arch = "wasm32"))] data: Arc>>, #[cfg(target_arch = "wasm32")] db: Arc, #[cfg(target_arch = "wasm32")] store_name: String, } impl KvsStore { /// Opens a new key-value store with the given name. /// /// # Arguments /// /// * `db_name` - The name of the database /// * `store_name` - The name of the object store /// /// # Returns /// /// A new `KvsStore` instance #[cfg(not(target_arch = "wasm32"))] pub async fn open(_db_name: &str, _store_name: &str) -> Result { // In non-WASM environments, use an in-memory store for testing Ok(Self { data: Arc::new(Mutex::new(HashMap::new())), }) } #[cfg(target_arch = "wasm32")] pub async fn open(db_name: &str, store_name: &str) -> Result { let factory = Factory::new()?; let mut db_req = factory.open(db_name, Some(1))?; db_req.on_upgrade_needed(|event| { let db = event.database()?; if !db.object_store_names().includes(&JsValue::from_str(store_name)) { db.create_object_store(store_name, None)?; } Ok(()) })?; let db = Arc::new(db_req.await?); Ok(Self { db, store_name: store_name.to_string(), }) } /// Stores a value with the given key. /// /// # Arguments /// /// * `key` - The key to store the value under /// * `value` - The value to store /// /// # Returns /// /// `Ok(())` if the operation was successful #[cfg(not(target_arch = "wasm32"))] pub async fn set(&self, key: K, value: &V) -> Result<()> where K: ToString, V: Serialize, { let key_str = key.to_string(); let serialized = serde_json::to_string(value)?; let mut data = self.data.lock().unwrap(); data.insert(key_str, serialized); Ok(()) } #[cfg(target_arch = "wasm32")] pub async fn set(&self, key: K, value: &V) -> Result<()> where K: ToString + Into, V: Serialize, { let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?; let store = tx.object_store(&self.store_name)?; let serialized = serde_json::to_string(value)?; store.put_with_key(&JsValue::from_str(&serialized), &key.into())?; JsFuture::from(tx.done()).await?; Ok(()) } /// Retrieves a value for the given key. /// /// # Arguments /// /// * `key` - The key to retrieve the value for /// /// # Returns /// /// The value if found, or `Err(KvsError::KeyNotFound)` if not found #[cfg(not(target_arch = "wasm32"))] pub async fn get(&self, key: K) -> Result where K: ToString, V: DeserializeOwned, { let key_str = key.to_string(); let data = self.data.lock().unwrap(); match data.get(&key_str) { Some(serialized) => { let value = serde_json::from_str(serialized)?; Ok(value) }, None => Err(KvsError::KeyNotFound(key_str)), } } #[cfg(target_arch = "wasm32")] pub async fn get(&self, key: K) -> Result where K: ToString + Into, V: DeserializeOwned, { let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?; let store = tx.object_store(&self.store_name)?; let request = store.get(&key.into())?; let promise = Promise::from(request); let result = JsFuture::from(promise).await?; if result.is_undefined() { return Err(KvsError::KeyNotFound(key.to_string())); } let value_str = result.as_string().ok_or_else(|| { KvsError::Deserialization("Failed to convert value to string".to_string()) })?; let value = serde_json::from_str(&value_str)?; Ok(value) } /// Deletes a value for the given key. /// /// # Arguments /// /// * `key` - The key to delete /// /// # Returns /// /// `Ok(())` if the operation was successful #[cfg(not(target_arch = "wasm32"))] pub async fn delete(&self, key: K) -> Result<()> where K: ToString, { let key_str = key.to_string(); let mut data = self.data.lock().unwrap(); if data.remove(&key_str).is_some() { Ok(()) } else { Err(KvsError::KeyNotFound(key_str)) } } #[cfg(target_arch = "wasm32")] pub async fn delete(&self, key: K) -> Result<()> where K: ToString + Into, { let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?; let store = tx.object_store(&self.store_name)?; // First check if the key exists let request = store.count_with_key(&key.into())?; let promise = Promise::from(request); let result = JsFuture::from(promise).await?; let count = result.as_f64().unwrap_or(0.0); if count <= 0.0 { return Err(KvsError::KeyNotFound(key.to_string())); } store.delete(&key.into())?; JsFuture::from(tx.done()).await?; Ok(()) } /// Checks if a key exists in the store. /// /// # Arguments /// /// * `key` - The key to check /// /// # Returns /// /// `true` if the key exists, `false` otherwise #[cfg(not(target_arch = "wasm32"))] pub async fn contains(&self, key: K) -> Result where K: ToString, { let key_str = key.to_string(); let data = self.data.lock().unwrap(); Ok(data.contains_key(&key_str)) } #[cfg(target_arch = "wasm32")] pub async fn contains(&self, key: K) -> Result where K: ToString + Into, { let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?; let store = tx.object_store(&self.store_name)?; let request = store.count_with_key(&key.into())?; let promise = Promise::from(request); let result = JsFuture::from(promise).await?; let count = result.as_f64().unwrap_or(0.0); Ok(count > 0.0) } /// Lists all keys in the store. /// /// # Returns /// /// A vector of keys as strings #[cfg(not(target_arch = "wasm32"))] pub async fn keys(&self) -> Result> { let data = self.data.lock().unwrap(); Ok(data.keys().cloned().collect()) } #[cfg(target_arch = "wasm32")] pub async fn keys(&self) -> Result> { let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?; let store = tx.object_store(&self.store_name)?; let request = store.get_all_keys(None)?; let promise = Promise::from(request); let result = JsFuture::from(promise).await?; let keys_array = js_sys::Array::from(&result); 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); } else { // Try to convert non-string keys to string keys.push(format!("{:?}", key)); } } Ok(keys) } /// Clears all key-value pairs from the store. /// /// # Returns /// /// `Ok(())` if the operation was successful #[cfg(not(target_arch = "wasm32"))] pub async fn clear(&self) -> Result<()> { let mut data = self.data.lock().unwrap(); data.clear(); Ok(()) } #[cfg(target_arch = "wasm32")] pub async fn clear(&self) -> Result<()> { let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?; let store = tx.object_store(&self.store_name)?; store.clear()?; JsFuture::from(tx.done()).await?; Ok(()) } }