From 779532b624a7cd201f8906efe033bfd475b7519a Mon Sep 17 00:00:00 2001 From: Brandon Vandegrift <798832-bmv437@users.noreply.gitlab.com> Date: Fri, 18 Aug 2023 18:32:04 -0400 Subject: [PATCH] Add new VeilidTable and VeilidClient to WASM API These new interfaces have been reworked (compared to the original API) to emit (mostly) proper TypeScript types. --- .../src/crypto/types/crypto_typed_group.rs | 1 + veilid-wasm/README.md | 6 + veilid-wasm/src/lib.rs | 8 +- veilid-wasm/src/veilid_client_js.rs | 152 +++++++++++++++ veilid-wasm/src/veilid_table_js.rs | 182 ++++++++++++++++++ 5 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 veilid-wasm/README.md create mode 100644 veilid-wasm/src/veilid_client_js.rs create mode 100644 veilid-wasm/src/veilid_table_js.rs diff --git a/veilid-core/src/crypto/types/crypto_typed_group.rs b/veilid-core/src/crypto/types/crypto_typed_group.rs index 60a34b2e..ff93df00 100644 --- a/veilid-core/src/crypto/types/crypto_typed_group.rs +++ b/veilid-core/src/crypto/types/crypto_typed_group.rs @@ -4,6 +4,7 @@ use super::*; Clone, Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq, Hash, Default, Tsify, )] #[serde(from = "Vec>", into = "Vec>")] +// TODO: figure out hot to TS type this as `string`, since it's converted to string via the JSON API. pub struct CryptoTypedGroup where K: Clone diff --git a/veilid-wasm/README.md b/veilid-wasm/README.md new file mode 100644 index 00000000..c4ac3045 --- /dev/null +++ b/veilid-wasm/README.md @@ -0,0 +1,6 @@ +# veilid-wasm + +## Notes + +- [`wasm_bindgen`](https://rustwasm.github.io/wasm-bindgen/) is used to generate interop code between JavaScript and Rust, as well as basic TypeScript types. +- [`tsify`](https://github.com/madonoharu/tsify) is used to export TypeScript types along-side [`wasm_bindgen`](https://rustwasm.github.io/wasm-bindgen/) and [`serde_wasm_bindgen`](https://github.com/cloudflare/serde-wasm-bindgen), and enables serialization/deserialization. diff --git a/veilid-wasm/src/lib.rs b/veilid-wasm/src/lib.rs index 015fe026..ac4f4aaf 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -25,6 +25,9 @@ use veilid_core::*; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::*; +pub mod veilid_client_js; +pub mod veilid_table_js; + // Allocator extern crate wee_alloc; #[global_allocator] @@ -385,7 +388,7 @@ pub fn routing_context_app_message(id: u32, target_string: String, message: Stri let routing_context = { let rc = (*ROUTING_CONTEXTS).borrow(); let Some(routing_context) = rc.get(&id) else { - return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("routing_context_app_call", "id", id)); + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("routing_context_app_message", "id", id)); }; routing_context.clone() }; @@ -1460,7 +1463,8 @@ pub fn veilid_version_string() -> String { veilid_core::veilid_version_string() } -#[derive(Serialize)] +#[derive(Serialize, Tsify)] +#[tsify(into_wasm_abi)] pub struct VeilidVersion { pub major: u32, pub minor: u32, diff --git a/veilid-wasm/src/veilid_client_js.rs b/veilid-wasm/src/veilid_client_js.rs new file mode 100644 index 00000000..1f2f8f1b --- /dev/null +++ b/veilid-wasm/src/veilid_client_js.rs @@ -0,0 +1,152 @@ +#![allow(non_snake_case)] +use super::*; + +#[wasm_bindgen(typescript_custom_section)] +const IUPDATE_VEILID_FUNCTION: &'static str = r#" +type UpdateVeilidFunction = (event: VeilidUpdate) => void; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = Function, typescript_type = "UpdateVeilidFunction")] + pub type UpdateVeilidFunction; +} + +#[wasm_bindgen(js_name = veilidClient)] +pub struct VeilidClient {} + +// Since this implementation doesn't contain a `new` fn that's marked as a constructor, +// and none of the member fns take a &self arg, +// this is just a namespace/class of static functions. +#[wasm_bindgen(js_class = veilidClient)] +impl VeilidClient { + pub async fn initializeCore(platformConfig: VeilidWASMConfig) { + if INITIALIZED.swap(true, Ordering::Relaxed) { + return; + } + console_error_panic_hook::set_once(); + + // Set up subscriber and layers + let subscriber = Registry::default(); + let mut layers = Vec::new(); + let mut filters = (*FILTERS).borrow_mut(); + + // Performance logger + if platformConfig.logging.performance.enabled { + let filter = + veilid_core::VeilidLayerFilter::new(platformConfig.logging.performance.level, None); + let layer = WASMLayer::new( + WASMLayerConfigBuilder::new() + .set_report_logs_in_timings(platformConfig.logging.performance.logs_in_timings) + .set_console_config(if platformConfig.logging.performance.logs_in_console { + ConsoleConfig::ReportWithConsoleColor + } else { + ConsoleConfig::NoReporting + }) + .build(), + ) + .with_filter(filter.clone()); + filters.insert("performance", filter); + layers.push(layer.boxed()); + }; + + // API logger + if platformConfig.logging.api.enabled { + let filter = + veilid_core::VeilidLayerFilter::new(platformConfig.logging.api.level, None); + let layer = veilid_core::ApiTracingLayer::get().with_filter(filter.clone()); + filters.insert("api", filter); + layers.push(layer.boxed()); + } + + let subscriber = subscriber.with(layers); + subscriber + .try_init() + .map_err(|e| format!("failed to initialize logging: {}", e)) + .expect("failed to initalize WASM platform"); + } + + pub async fn startupCore( + update_callback_js: UpdateVeilidFunction, + json_config: String, + ) -> Result<(), VeilidAPIError> { + let update_callback_js = SendWrapper::new(update_callback_js); + let update_callback = Arc::new(move |update: VeilidUpdate| { + let _ret = match Function::call1( + &update_callback_js, + &JsValue::UNDEFINED, + &to_jsvalue(update), + ) { + Ok(v) => v, + Err(e) => { + console_log(&format!("calling update callback failed: {:?}", e)); + return; + } + }; + }); + + if VEILID_API.borrow().is_some() { + return Err(veilid_core::VeilidAPIError::AlreadyInitialized); + } + + let veilid_api = veilid_core::api_startup_json(update_callback, json_config).await?; + VEILID_API.replace(Some(veilid_api)); + Ok(()) + } + + // TODO: can we refine the TS type of `layer`? + pub fn changeLogLevel(layer: String, log_level: VeilidConfigLogLevel) { + let layer = if layer == "all" { "".to_owned() } else { layer }; + let filters = (*FILTERS).borrow(); + if layer.is_empty() { + // Change all layers + for f in filters.values() { + f.set_max_level(log_level); + } + } else { + // Change a specific layer + let f = filters.get(layer.as_str()).unwrap(); + f.set_max_level(log_level); + } + } + + pub async fn shutdownCore() -> Result<(), VeilidAPIError> { + let veilid_api = take_veilid_api()?; + veilid_api.shutdown().await; + Ok(()) + } + + pub async fn getState() -> Result { + let veilid_api = get_veilid_api()?; + let core_state = veilid_api.get_state().await?; + Ok(core_state) + } + + pub async fn attach() -> Result<(), VeilidAPIError> { + let veilid_api = get_veilid_api()?; + veilid_api.attach().await?; + Ok(()) + } + + pub async fn detach() -> Result<(), VeilidAPIError> { + let veilid_api = get_veilid_api()?; + veilid_api.detach().await?; + Ok(()) + } + + pub async fn debug(command: String) -> Result { + let veilid_api = get_veilid_api()?; + let out = veilid_api.debug(command).await?; + APIResult::Ok(out) + } + + pub fn version() -> VeilidVersion { + let (major, minor, patch) = veilid_core::veilid_version(); + let vv = super::VeilidVersion { + major, + minor, + patch, + }; + vv + } +} diff --git a/veilid-wasm/src/veilid_table_js.rs b/veilid-wasm/src/veilid_table_js.rs new file mode 100644 index 00000000..dd7abe72 --- /dev/null +++ b/veilid-wasm/src/veilid_table_js.rs @@ -0,0 +1,182 @@ +#![allow(non_snake_case)] +use super::*; + +#[wasm_bindgen()] +pub struct VeilidTable { + id: u32, + tableName: String, + columnCount: u32, +} + +#[wasm_bindgen()] +impl VeilidTable { + #[wasm_bindgen(constructor)] + pub fn new(tableName: String, columnCount: u32) -> VeilidTable { + VeilidTable { + id: 0, + tableName, + columnCount, + } + } + + pub async fn openTable(&mut self) -> Result { + let veilid_api = get_veilid_api()?; + let tstore = veilid_api.table_store()?; + let table_db = tstore + .open(&self.tableName, self.columnCount) + .await + .map_err(veilid_core::VeilidAPIError::generic)?; + let new_id = add_table_db(table_db); + self.id = new_id; + APIResult::Ok(new_id) + } + + pub fn releaseTable(&mut self) -> bool { + let mut tdbs = (*TABLE_DBS).borrow_mut(); + let status = tdbs.remove(&self.id); + self.id = 0; + if status.is_none() { + return false; + } + return true; + } + + pub async fn deleteTable(&mut self) -> Result { + self.releaseTable(); + + let veilid_api = get_veilid_api()?; + let tstore = veilid_api.table_store()?; + let deleted = tstore + .delete(&self.tableName) + .await + .map_err(veilid_core::VeilidAPIError::generic)?; + APIResult::Ok(deleted) + } + + async fn ensureOpen(&mut self) { + if self.id == 0 { + let _ = self.openTable().await; + } + } + + pub async fn load( + &mut self, + columnId: u32, + key: String, + ) -> Result, VeilidAPIError> { + self.ensureOpen().await; + + let table_db = { + let table_dbs = (*TABLE_DBS).borrow(); + let Some(table_db) = table_dbs.get(&self.id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("table_db_load", "id", self.id)); + }; + table_db.clone() + }; + + let out = table_db.load(columnId, key.as_bytes()).await?.unwrap(); + let out = Some(str::from_utf8(&out).unwrap().to_owned()); + // let out = serde_wasm_bindgen::to_value(&out) + // .expect("Could not parse using serde_wasm_bindgen"); + APIResult::Ok(out) + } + + pub async fn store( + &mut self, + columnId: u32, + key: String, + value: String, + ) -> Result<(), VeilidAPIError> { + self.ensureOpen().await; + + let table_db = { + let table_dbs = (*TABLE_DBS).borrow(); + let Some(table_db) = table_dbs.get(&self.id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("table_db_store", "id", self.id)); + }; + table_db.clone() + }; + + table_db + .store(columnId, key.as_bytes(), value.as_bytes()) + .await?; + APIRESULT_UNDEFINED + } + + pub async fn delete( + &mut self, + columnId: u32, + key: String, + ) -> Result, VeilidAPIError> { + self.ensureOpen().await; + + let table_db = { + let table_dbs = (*TABLE_DBS).borrow(); + let Some(table_db) = table_dbs.get(&self.id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("table_db_delete", "id", self.id)); + }; + table_db.clone() + }; + + // TODO: will crash when trying to .unwrap() of None (trying to delete key that doesn't exist) + let out = table_db.delete(columnId, key.as_bytes()).await?.unwrap(); + let out = Some(str::from_utf8(&out).unwrap().to_owned()); + APIResult::Ok(out) + } + + // TODO try and figure out how to result a String[], maybe Box<[String]>? + pub async fn getKeys(&mut self, columnId: u32) -> Result { + self.ensureOpen().await; + + let table_db = { + let table_dbs = (*TABLE_DBS).borrow(); + let Some(table_db) = table_dbs.get(&self.id) else { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument("table_db_store", "id", self.id)); + }; + table_db.clone() + }; + + let keys = table_db.clone().get_keys(columnId).await?; + let out: Vec = keys + .into_iter() + .map(|k| str::from_utf8(&k).unwrap().to_owned()) + .collect(); + let out = + serde_wasm_bindgen::to_value(&out).expect("Could not parse using serde_wasm_bindgen"); + + APIResult::Ok(out) + } + + pub async fn transact(&mut self) -> u32 { + self.ensureOpen().await; + + let table_dbs = (*TABLE_DBS).borrow(); + let Some(table_db) = table_dbs.get(&self.id) else { + return 0; + }; + let tdbt = table_db.clone().transact(); + let tdbtid = add_table_db_transaction(tdbt); + return tdbtid; + } + + // TODO: placeholders for transaction functions + // pub async fn releaseTransaction(&mut self) { + // self.ensureOpen().await; + // } + + // pub async fn commitTransaction(&mut self) { + // self.ensureOpen().await; + // } + + // pub async fn rollbackTransaction(&mut self) { + // self.ensureOpen().await; + // } + + // pub async fn storeTransaction(&mut self, tableId: u32, key: String, value: String) { + // self.ensureOpen().await; + // } + + // pub async fn deleteTransaction(&mut self) { + // self.ensureOpen().await; + // } +}