This commit is contained in:
2025-04-22 13:00:10 +04:00
parent 6573a01d75
commit 3e49f48f60
12 changed files with 1416 additions and 188 deletions

225
src/core/kvs/README.md Normal file
View File

@@ -0,0 +1,225 @@
# Key-Value Store (KVS) Module
This module provides a simple key-value store implementation with dual backends:
- IndexedDB for WebAssembly applications running in browsers
- In-memory storage for testing and non-browser environments
## Overview
The KVS module provides a simple, yet powerful interface for storing and retrieving data. In a browser environment, it uses IndexedDB as the underlying storage mechanism, which provides a robust, persistent storage solution that works offline and can handle large amounts of data. In non-browser environments, it uses an in-memory store for testing purposes.
## Features
- **Simple API**: Easy-to-use methods for common operations like get, set, delete
- **Type Safety**: Generic methods that preserve your data types through serialization/deserialization
- **Error Handling**: Comprehensive error types for robust error handling
- **Async/Await**: Modern async interface for all operations
- **Serialization**: Automatic serialization/deserialization of complex data types
## Core Components
### KvsStore
The main struct that provides access to the key-value store, with different implementations based on the environment:
```rust
// In WebAssembly environments (browsers)
pub struct KvsStore {
db: Arc<Database>,
store_name: String,
}
// In non-WebAssembly environments (for testing)
pub struct KvsStore {
data: Arc<Mutex<HashMap<String, String>>>,
}
```
### Error Types
The module defines several error types to handle different failure scenarios:
```rust
pub enum KvsError {
Idb(String),
KeyNotFound(String),
Serialization(String),
Deserialization(String),
Other(String),
}
```
## Usage Examples
### Opening a Store
```rust
let store = KvsStore::open("my_database", "my_store").await?;
```
### Storing Values
```rust
// Store a simple string
store.set("string_key", &"Hello, world!").await?;
// Store a complex object
let user = User {
id: 1,
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
};
store.set("user_1", &user).await?;
```
### Retrieving Values
```rust
// Get a string
let value: String = store.get("string_key").await?;
// Get a complex object
let user: User = store.get("user_1").await?;
```
### Checking if a Key Exists
```rust
if store.contains("user_1").await? {
// Key exists
}
```
### Deleting Values
```rust
store.delete("user_1").await?;
```
### Listing All Keys
```rust
let keys = store.keys().await?;
for key in keys {
println!("Found key: {}", key);
}
```
### Clearing the Store
```rust
store.clear().await?;
```
## Error Handling
The module uses a custom `Result` type that wraps `KvsError`:
```rust
type Result<T> = std::result::Result<T, KvsError>;
```
Example of error handling:
```rust
match store.get::<User>("nonexistent_key").await {
Ok(user) => {
// Process user
},
Err(KvsError::KeyNotFound(key)) => {
println!("Key not found: {}", key);
},
Err(e) => {
println!("An error occurred: {}", e);
}
}
```
## Implementation Details
The KVS module uses:
- **Dual backend architecture**:
- IndexedDB for browser environments via the `idb` crate (direct Rust implementation)
- In-memory HashMap for testing and non-browser environments
- **Conditional compilation** with `#[cfg(target_arch = "wasm32")]` to select the appropriate implementation
- **Serde** for serialization/deserialization
- **Wasm-bindgen** for JavaScript interop in browser environments
- **Async/await** for non-blocking operations
- **Arc and Mutex** for thread-safe access to the in-memory store
Note: This implementation uses the `idb` crate to interact with IndexedDB directly from Rust, eliminating the need for a JavaScript bridge file.
## Testing
The module includes comprehensive tests in `src/tests/kvs_tests.rs` that verify all functionality works as expected.
### Running the Tests
Thanks to the dual implementation, tests can be run in two ways:
#### Standard Rust Tests
The in-memory implementation allows tests to run in a standard Rust environment without requiring a browser:
```bash
cargo test
```
This runs all tests using the in-memory implementation, which is perfect for CI/CD pipelines and quick development testing.
#### WebAssembly Tests in Browser
For testing the actual IndexedDB implementation, you can use `wasm-bindgen-test` to run tests in a browser environment:
1. **Install wasm-pack if you haven't already**:
```bash
cargo install wasm-pack
```
2. **Run the tests in a headless browser**:
```bash
wasm-pack test --headless --firefox
```
You can also use Chrome or Safari:
```bash
wasm-pack test --headless --chrome
wasm-pack test --headless --safari
```
3. **Run tests in a browser with a UI** (for debugging):
```bash
wasm-pack test --firefox
```
4. **Run specific tests**:
```bash
wasm-pack test --firefox -- --filter kvs_tests
```
### Test Structure
The tests are organized to test each functionality of the KVS module:
1. **Basic Operations**: Tests for opening a store, setting/getting values
2. **Complex Data**: Tests for storing and retrieving complex objects
3. **Error Handling**: Tests for handling nonexistent keys and errors
4. **Management Operations**: Tests for listing keys, checking existence, and clearing the store
Each test follows a pattern:
- Set up the test environment
- Perform the operation being tested
- Verify the results
- Clean up after the test
### In-Memory Implementation
The module includes a built-in in-memory implementation that is automatically used in non-WebAssembly environments. This implementation:
- Uses a `HashMap<String, String>` wrapped in `Arc<Mutex<>>` for thread safety
- Provides the same API as the IndexedDB implementation
- Automatically serializes/deserializes values using serde_json
- Makes testing much easier by eliminating the need for a browser environment
This dual implementation approach means you don't need to create separate mocks for testing - the module handles this automatically through conditional compilation.

47
src/core/kvs/error.rs Normal file
View File

@@ -0,0 +1,47 @@
//! Error types for the key-value store.
use std::fmt;
/// Errors that can occur when using the key-value store.
#[derive(Debug)]
pub enum KvsError {
/// Error from the idb crate
Idb(String),
/// Key not found
KeyNotFound(String),
/// Serialization error
Serialization(String),
/// Deserialization error
Deserialization(String),
/// Other error
Other(String),
}
impl fmt::Display for KvsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KvsError::Idb(msg) => write!(f, "IndexedDB error: {}", msg),
KvsError::KeyNotFound(key) => write!(f, "Key not found: {}", key),
KvsError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
KvsError::Deserialization(msg) => write!(f, "Deserialization error: {}", msg),
KvsError::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
impl std::error::Error for KvsError {}
impl From<idb::Error> for KvsError {
fn from(err: idb::Error) -> Self {
KvsError::Idb(err.to_string())
}
}
impl From<serde_json::Error> for KvsError {
fn from(err: serde_json::Error) -> Self {
KvsError::Serialization(err.to_string())
}
}
/// Result type for key-value store operations.
pub type Result<T> = std::result::Result<T, KvsError>;

7
src/core/kvs/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
//! A simple key-value store implementation using IndexedDB.
pub mod error;
pub mod store;
pub use error::{KvsError, Result};
pub use store::KvsStore;

317
src/core/kvs/store.rs Normal file
View File

@@ -0,0 +1,317 @@
//! 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(())
}
}

View File

@@ -4,8 +4,10 @@ pub mod error;
pub mod keypair;
pub mod symmetric;
pub mod ethereum;
pub mod kvs;
// Re-export commonly used items for internal use
// (Keeping this even though it's currently unused, as it's good practice for internal modules)
#[allow(unused_imports)]
pub use error::CryptoError;
pub use error::CryptoError;
pub use kvs::{KvsStore as KvStore, KvsError as KvError, Result as KvResult};