feat: Add basic project structure and initial crates
- Added basic project structure with workspace and crates: `kvstore`, `vault`, `evm_client`, `cli_app`, `web_app`. - Created initial `Cargo.toml` files for each crate. - Added placeholder implementations for key components. - Included initial documentation files (`README.md`, architecture docs, repo structure). - Included initial implementaion for kvstore crate(async API, backend abstraction, separation of concerns, WASM/native support, testability) - Included native and browser tests for the kvstore crate
This commit is contained in:
31
kvstore/Cargo.toml
Normal file
31
kvstore/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "kvstore"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
sled = { version = "0.34", optional = true }
|
||||
idb = { version = "0.4", optional = true }
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = "1"
|
||||
tempfile = "3"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
native = ["sled", "tokio"]
|
||||
web = ["idb"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.45", optional = true, default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
idb = "0.4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
52
kvstore/README.md
Normal file
52
kvstore/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# kvstore: Async Key-Value Store for Native and WASM
|
||||
|
||||
`kvstore` provides a runtime-agnostic, async trait for key-value storage, with robust implementations for both native (using `sled`) and browser/WASM (using IndexedDB via the `idb` crate) environments.
|
||||
|
||||
## Features
|
||||
- **Unified async trait**: Same API for all platforms. Methods: `get`, `set`, `remove`, `contains_key`, `keys`, `clear`.
|
||||
- **Native backend**: Uses `sled` for fast, embedded storage. Blocking I/O is offloaded with `tokio::task::spawn_blocking`.
|
||||
- **WASM backend**: Uses IndexedDB via the `idb` crate for browser storage. Fully async and Promise-based.
|
||||
- **Error handling**: Consistent error types across platforms.
|
||||
- **Conditional compilation**: Uses Cargo features and `cfg` to select the backend.
|
||||
|
||||
## Usage
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
kvstore = { path = "../kvstore" }
|
||||
```
|
||||
|
||||
### Example
|
||||
```rust
|
||||
use kvstore::{KVStore, NativeStore};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let store = NativeStore::open("/tmp/mydb").unwrap();
|
||||
store.set("foo", b"bar").await.unwrap();
|
||||
let val = store.get("foo").await.unwrap();
|
||||
println!("Got: {:?}", val);
|
||||
}
|
||||
```
|
||||
|
||||
For WASM/browser, use `WasmStore` and run in a browser environment.
|
||||
|
||||
## Testing
|
||||
|
||||
### Native
|
||||
```sh
|
||||
cargo test -p kvstore --features native
|
||||
```
|
||||
|
||||
### WASM (browser)
|
||||
```sh
|
||||
cd kvstore
|
||||
wasm-pack test --headless --firefox --features web
|
||||
```
|
||||
|
||||
## Architecture
|
||||
- See `../docs/Architecture.md` for full design details.
|
||||
|
||||
## License
|
||||
MIT OR Apache-2.0
|
24
kvstore/src/error.rs
Normal file
24
kvstore/src/error.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Error types for the kvstore crate
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KVError {
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Key not found: {0}")]
|
||||
KeyNotFound(String),
|
||||
#[error("Store not found: {0}")]
|
||||
StoreNotFound(String),
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
#[error("Encryption error: {0}")]
|
||||
Encryption(String),
|
||||
#[error("Decryption error: {0}")]
|
||||
Decryption(String),
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, KVError>;
|
18
kvstore/src/lib.rs
Normal file
18
kvstore/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
/// Error types for the kvstore crate
|
||||
pub mod error;
|
||||
/// Async trait for key-value storage
|
||||
pub mod traits;
|
||||
/// Native backend (e.g., sled or slatedb)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod native;
|
||||
/// WASM backend (IndexedDB)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
pub use error::{KVError, Result};
|
||||
pub use traits::KVStore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use native::NativeStore;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::WasmStore;
|
106
kvstore/src/native.rs
Normal file
106
kvstore/src/native.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
//! Native backend for kvstore using sled
|
||||
//!
|
||||
//! # Runtime Requirement
|
||||
//!
|
||||
//! **A Tokio runtime must be running to use this backend.**
|
||||
//! This library does not start or manage a runtime; it assumes that all async methods are called from within an existing Tokio runtime context (e.g., via `#[tokio::main]` or `tokio::test`).
|
||||
//!
|
||||
//! All blocking I/O is offloaded using `tokio::task::spawn_blocking`.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
|
||||
use crate::traits::KVStore;
|
||||
use crate::error::{KVError, Result};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sled::Db;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeStore {
|
||||
db: Arc<Db>,
|
||||
}
|
||||
|
||||
impl NativeStore {
|
||||
pub fn open(path: &str) -> Result<Self> {
|
||||
let db = sled::open(path).map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(Self { db: Arc::new(db) })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[async_trait]
|
||||
impl KVStore for NativeStore {
|
||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.get(&key)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))
|
||||
.map(|opt| opt.map(|ivec| ivec.to_vec()))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
let value = value.to_vec();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.insert(&key, value)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
async fn remove(&self, key: &str) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.remove(&key)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
async fn contains_key(&self, key: &str) -> Result<bool> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
Ok(db.contains_key(&key)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
|
||||
async fn keys(&self) -> Result<Vec<String>> {
|
||||
let db = self.db.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut keys = Vec::new();
|
||||
for result in db.iter() {
|
||||
let (key, _) = result.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
keys.push(String::from_utf8_lossy(&key).to_string());
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
|
||||
async fn clear(&self) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.clear().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
}
|
26
kvstore/src/traits.rs
Normal file
26
kvstore/src/traits.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! Async trait for key-value storage
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
/// Async key-value store interface.
|
||||
///
|
||||
/// For native (non-wasm32) backends, implementers should be `Send + Sync` to support runtime-agnostic async usage.
|
||||
/// For WASM (wasm32) backends, `Send + Sync` is not required and types may not implement them.
|
||||
///
|
||||
/// Methods:
|
||||
/// - get
|
||||
/// - set
|
||||
/// - remove (was delete)
|
||||
/// - contains_key (was exists)
|
||||
/// - keys
|
||||
/// - clear
|
||||
pub trait KVStore {
|
||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
|
||||
async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
|
||||
async fn remove(&self, key: &str) -> Result<()>;
|
||||
async fn contains_key(&self, key: &str) -> Result<bool>;
|
||||
async fn keys(&self) -> Result<Vec<String>>;
|
||||
async fn clear(&self) -> Result<()>;
|
||||
}
|
140
kvstore/src/wasm.rs
Normal file
140
kvstore/src/wasm.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
//! WASM backend for kvstore using IndexedDB (idb crate)
|
||||
//!
|
||||
//! # Platform
|
||||
//!
|
||||
//! This backend is only available when compiling for `wasm32` (browser/WebAssembly).
|
||||
//! It uses the `idb` crate for async IndexedDB operations.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! This implementation is designed to run inside a browser environment and is not supported on native targets.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
|
||||
|
||||
use crate::traits::KVStore;
|
||||
use crate::error::{KVError, Result};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use idb::{Database, TransactionMode, Factory};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::JsValue;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use js_sys::Uint8Array;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
const STORE_NAME: &str = "kv";
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WasmStore {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl WasmStore {
|
||||
pub async fn open(name: &str) -> Result<Self> {
|
||||
let factory = Factory::new().map_err(|e| KVError::Other(format!("IndexedDB factory error: {e:?}")))?;
|
||||
let mut open_req = factory.open(name, None)
|
||||
.map_err(|e| KVError::Other(format!("IndexedDB factory open error: {e:?}")))?;
|
||||
open_req.on_upgrade_needed(|event| {
|
||||
let db = event.database().expect("Failed to get database in upgrade event");
|
||||
if !db.store_names().iter().any(|n| n == STORE_NAME) {
|
||||
db.create_object_store(STORE_NAME, Default::default()).unwrap();
|
||||
}
|
||||
});
|
||||
let db = open_req.await.map_err(|e| KVError::Other(format!("IndexedDB open error: {e:?}")))?;
|
||||
Ok(Self { db })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl KVStore for WasmStore {
|
||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
use idb::Query;
|
||||
let val = store.get(Query::from(JsValue::from_str(key))).await
|
||||
.map_err(|e| KVError::Other(format!("idb get await error: {e:?}")))?;
|
||||
if let Some(jsval) = val {
|
||||
let arr = Uint8Array::new(&jsval);
|
||||
Ok(Some(arr.to_vec()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
store.put(&Uint8Array::from(value).into(), Some(&JsValue::from_str(key))).await
|
||||
.map_err(|e| KVError::Other(format!("idb put await error: {e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
async fn remove(&self, key: &str) -> Result<()> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
use idb::Query;
|
||||
store.delete(Query::from(JsValue::from_str(key))).await
|
||||
.map_err(|e| KVError::Other(format!("idb delete await error: {e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
async fn contains_key(&self, key: &str) -> Result<bool> {
|
||||
Ok(self.get(key).await?.is_some())
|
||||
}
|
||||
|
||||
async fn keys(&self) -> Result<Vec<String>> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
let js_keys = store.get_all_keys(None, None).await
|
||||
.map_err(|e| KVError::Other(format!("idb get_all_keys error: {e:?}")))?;
|
||||
let arr = js_sys::Array::from(&JsValue::from(js_keys));
|
||||
let mut keys = Vec::new();
|
||||
for i in 0..arr.length() {
|
||||
if let Some(s) = arr.get(i).as_string() {
|
||||
keys.push(s);
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
async fn clear(&self) -> Result<()> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
store.clear().await
|
||||
.map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct WasmStore;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl KVStore for WasmStore {
|
||||
async fn get(&self, _key: &str) -> Result<Option<Vec<u8>>> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn delete(&self, _key: &str) -> Result<()> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn exists(&self, _key: &str) -> Result<bool> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
}
|
33
kvstore/tests/native.rs
Normal file
33
kvstore/tests/native.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
use kvstore::{NativeStore, KVStore};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_native_store_basic() {
|
||||
let tmp_dir = tempfile::tempdir().unwrap();
|
||||
let path = tmp_dir.path().join("testdb");
|
||||
let store = NativeStore::open(path.to_str().unwrap()).unwrap();
|
||||
|
||||
// Test set/get
|
||||
store.set("foo", b"bar").await.unwrap();
|
||||
let val = store.get("foo").await.unwrap();
|
||||
assert_eq!(val, Some(b"bar".to_vec()));
|
||||
|
||||
// Test exists
|
||||
assert_eq!(store.contains_key("foo").await.unwrap(), true);
|
||||
assert_eq!(store.contains_key("bar").await.unwrap(), false);
|
||||
store.remove("foo").await.unwrap();
|
||||
assert_eq!(store.get("foo").await.unwrap(), None);
|
||||
|
||||
// Test keys
|
||||
store.set("foo", b"bar").await.unwrap();
|
||||
store.set("baz", b"qux").await.unwrap();
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert!(keys.contains(&"foo".to_string()));
|
||||
assert!(keys.contains(&"baz".to_string()));
|
||||
|
||||
// Test clear
|
||||
store.clear().await.unwrap();
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 0);
|
||||
}
|
46
kvstore/tests/web.rs
Normal file
46
kvstore/tests/web.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
//! WASM/browser tests for kvstore using wasm-bindgen-test
|
||||
|
||||
use kvstore::{WasmStore, KVStore};
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_set_and_get() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
let val = store.get("foo").await.expect("get");
|
||||
assert_eq!(val, Some(b"bar".to_vec()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_delete_and_exists() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
assert_eq!(store.contains_key("foo").await.unwrap(), true);
|
||||
assert_eq!(store.contains_key("bar").await.unwrap(), false);
|
||||
store.remove("foo").await.unwrap();
|
||||
assert_eq!(store.get("foo").await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_keys() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
store.set("baz", b"qux").await.expect("set");
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert!(keys.contains(&"foo".to_string()));
|
||||
assert!(keys.contains(&"baz".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_clear() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
store.set("baz", b"qux").await.expect("set");
|
||||
store.clear().await.unwrap();
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 0);
|
||||
}
|
Reference in New Issue
Block a user