webassembly/src/core/kvs/store.rs
2025-04-22 13:00:10 +04:00

317 lines
9.0 KiB
Rust

//! 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<JsValue> 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<Mutex<HashMap<String, String>>>,
#[cfg(target_arch = "wasm32")]
db: Arc<Database>,
#[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<Self> {
// 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<Self> {
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<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)?;
let mut data = self.data.lock().unwrap();
data.insert(key_str, serialized);
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn set<K, V>(&self, key: K, value: &V) -> Result<()>
where
K: ToString + Into<JsValue>,
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<K, V>(&self, key: K) -> Result<V>
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<K, V>(&self, key: K) -> Result<V>
where
K: ToString + Into<JsValue>,
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<K>(&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<K>(&self, key: K) -> Result<()>
where
K: ToString + Into<JsValue>,
{
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<K>(&self, key: K) -> Result<bool>
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<K>(&self, key: K) -> Result<bool>
where
K: ToString + Into<JsValue>,
{
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<Vec<String>> {
let data = self.data.lock().unwrap();
Ok(data.keys().cloned().collect())
}
#[cfg(target_arch = "wasm32")]
pub async fn keys(&self) -> Result<Vec<String>> {
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(())
}
}