feat: implement browser extension UI with WebAssembly integration
This commit is contained in:
		| @@ -9,8 +9,9 @@ path = "src/lib.rs" | ||||
| [dependencies] | ||||
| tokio = { version = "1.37", features = ["rt", "macros"] } | ||||
| async-trait = "0.1" | ||||
| js-sys = "0.3" | ||||
| wasm-bindgen = "0.2" | ||||
|  | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||||
| wasm-bindgen-futures = "0.4" | ||||
| thiserror = "1" | ||||
|  | ||||
| @@ -22,7 +23,9 @@ tempfile = "3" | ||||
| tokio = { version = "1", features = ["rt-multi-thread", "macros"] } | ||||
|  | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
| idb = { version = "0.4" } | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] } | ||||
| idb = { version = "0.6" } | ||||
| wasm-bindgen-test = "0.3" | ||||
|  | ||||
| [features] | ||||
|   | ||||
| @@ -22,3 +22,12 @@ pub enum KVError { | ||||
| } | ||||
|  | ||||
| pub type Result<T> = std::result::Result<T, KVError>; | ||||
|  | ||||
| // Allow automatic conversion from idb::Error to KVError | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl From<idb::Error> for KVError { | ||||
|     fn from(e: idb::Error) -> Self { | ||||
|         KVError::Other(format!("idb error: {e:?}")) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -26,8 +26,7 @@ use async_trait::async_trait; | ||||
| use idb::{Database, TransactionMode, Factory}; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use wasm_bindgen::JsValue; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use js_sys::Uint8Array; | ||||
| // use wasm-bindgen directly for Uint8Array if needed | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use std::rc::Rc; | ||||
|  | ||||
| @@ -47,6 +46,7 @@ impl WasmStore { | ||||
|         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| { | ||||
|             use idb::DatabaseEvent; | ||||
|             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(); | ||||
| @@ -66,11 +66,13 @@ impl KVStore for WasmStore { | ||||
|         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:?}")))?; | ||||
|         let val = store.get(Query::from(JsValue::from_str(key)))?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb get error: {e:?}")))?; | ||||
|         if let Some(jsval) = val { | ||||
|             let arr = Uint8Array::new(&jsval); | ||||
|             Ok(Some(arr.to_vec())) | ||||
|             match jsval.into_serde::<Vec<u8>>() { | ||||
|     Ok(bytes) => Ok(Some(bytes)), | ||||
|     Err(_) => Ok(None), | ||||
| } | ||||
|         } else { | ||||
|             Ok(None) | ||||
|         } | ||||
| @@ -80,8 +82,9 @@ impl KVStore for WasmStore { | ||||
|             .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:?}")))?; | ||||
|         let js_value = JsValue::from_serde(&value).map_err(|e| KVError::Other(format!("serde error: {e:?}")))?; | ||||
| store.put(&js_value, Some(&JsValue::from_str(key)))?.await | ||||
|     .map_err(|e| KVError::Other(format!("idb put error: {e:?}")))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|     async fn remove(&self, key: &str) -> Result<()> { | ||||
| @@ -90,8 +93,8 @@ impl KVStore for WasmStore { | ||||
|         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:?}")))?; | ||||
|         store.delete(Query::from(JsValue::from_str(key)))?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb delete error: {e:?}")))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|     async fn contains_key(&self, key: &str) -> Result<bool> { | ||||
| @@ -103,12 +106,11 @@ impl KVStore for WasmStore { | ||||
|             .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 | ||||
|         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() { | ||||
|         for key in js_keys.iter() { | ||||
|             if let Some(s) = key.as_string() { | ||||
|                 keys.push(s); | ||||
|             } | ||||
|         } | ||||
| @@ -120,7 +122,7 @@ impl KVStore for WasmStore { | ||||
|             .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 | ||||
|         store.clear()?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|   | ||||
| @@ -31,3 +31,22 @@ async fn test_native_store_basic() { | ||||
|     let keys = store.keys().await.unwrap(); | ||||
|     assert_eq!(keys.len(), 0); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_native_store_persistence() { | ||||
|     let tmp_dir = tempfile::tempdir().unwrap(); | ||||
|     let path = tmp_dir.path().join("persistdb"); | ||||
|     let db_path = path.to_str().unwrap(); | ||||
|     // First open, set value | ||||
|     { | ||||
|         let store = NativeStore::open(db_path).unwrap(); | ||||
|         store.set("persist", b"value").await.unwrap(); | ||||
|     } | ||||
|     // Reopen and check value | ||||
|     { | ||||
|         let store = NativeStore::open(db_path).unwrap(); | ||||
|         let val = store.get("persist").await.unwrap(); | ||||
|         assert_eq!(val, Some(b"value".to_vec())); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ 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"); | ||||
|     let store = WasmStore::open("vault").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())); | ||||
| @@ -16,7 +16,7 @@ async fn test_set_and_get() { | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_delete_and_exists() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").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); | ||||
| @@ -26,7 +26,7 @@ async fn test_delete_and_exists() { | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_keys() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").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(); | ||||
| @@ -35,9 +35,26 @@ async fn test_keys() { | ||||
|     assert!(keys.contains(&"baz".to_string())); | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_wasm_store_persistence() { | ||||
|     // Use a unique store name to avoid collisions | ||||
|     let store_name = "persist_test_store"; | ||||
|     // First open, set value | ||||
|     { | ||||
|         let store = WasmStore::open(store_name).await.expect("open"); | ||||
|         store.set("persist", b"value").await.expect("set"); | ||||
|     } | ||||
|     // Reopen and check value | ||||
|     { | ||||
|         let store = WasmStore::open(store_name).await.expect("open"); | ||||
|         let val = store.get("persist").await.expect("get"); | ||||
|         assert_eq!(val, Some(b"value".to_vec())); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_clear() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").await.expect("open"); | ||||
|     store.set("foo", b"bar").await.expect("set"); | ||||
|     store.set("baz", b"qux").await.expect("set"); | ||||
|     store.clear().await.unwrap(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user