317 lines
9.0 KiB
Rust
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(())
|
|
}
|
|
} |