diff --git a/Cargo.lock b/Cargo.lock index 2585bef8..1883fa09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2136,6 +2136,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -4250,7 +4263,7 @@ checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals", + "serde_derive_internals 0.26.0", "syn 1.0.109", ] @@ -4358,6 +4371,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_cbor" version = "0.11.2" @@ -4390,6 +4414,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "serde_json" version = "1.0.105" @@ -5347,6 +5382,32 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals 0.28.0", + "syn 2.0.31", +] + [[package]] name = "tungstenite" version = "0.11.1" @@ -5614,6 +5675,7 @@ dependencies = [ "send_wrapper 0.6.0", "serde", "serde-big-array", + "serde-wasm-bindgen", "serde_json", "serial_test 2.0.0", "shell-words", @@ -5631,6 +5693,7 @@ dependencies = [ "tracing-subscriber", "tracing-wasm", "trust-dns-resolver", + "tsify", "veilid-bugsalot", "veilid-hashlink", "veilid-igd", @@ -5813,15 +5876,18 @@ dependencies = [ "console_error_panic_hook", "data-encoding", "futures-util", - "gloo-utils", + "gloo-utils 0.2.0", "js-sys", "lazy_static", + "parking_lot 0.12.1", "send_wrapper 0.6.0", "serde", + "serde-wasm-bindgen", "serde_json", "tracing", "tracing-subscriber", "tracing-wasm", + "tsify", "veilid-core", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 941af040..96f2d9aa 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -199,6 +199,8 @@ wasm-bindgen = "0.2.87" js-sys = "0.3.64" wasm-bindgen-futures = "0.4.37" send_wrapper = { version = "0.6.0", features = ["futures"] } +tsify = { version = "0.4.5", features = ["js"] } +serde-wasm-bindgen = "0.5.0" # Network ws_stream_wasm = "0.7.4" diff --git a/veilid-core/src/core_context.rs b/veilid-core/src/core_context.rs index 4624b012..61e84f8a 100644 --- a/veilid-core/src/core_context.rs +++ b/veilid-core/src/core_context.rs @@ -231,7 +231,7 @@ impl VeilidCoreContext { update_callback: UpdateCallback, config_json: String, ) -> VeilidAPIResult { - // Set up config from callback + // Set up config from json trace!("setup config with json"); let mut config = VeilidConfig::new(); config.setup_from_json(config_json, update_callback.clone())?; diff --git a/veilid-core/src/crypto/types/byte_array_types.rs b/veilid-core/src/crypto/types/byte_array_types.rs index 4cdf2300..d47a323a 100644 --- a/veilid-core/src/crypto/types/byte_array_types.rs +++ b/veilid-core/src/crypto/types/byte_array_types.rs @@ -78,6 +78,7 @@ where macro_rules! byte_array_type { ($name:ident, $size:expr, $encoded_size:expr) => { #[derive(Clone, Copy, Hash)] + #[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(into_wasm_abi))] pub struct $name { pub bytes: [u8; $size], } @@ -293,11 +294,17 @@ macro_rules! byte_array_type { byte_array_type!(CryptoKey, CRYPTO_KEY_LENGTH, CRYPTO_KEY_LENGTH_ENCODED); +#[cfg_attr(target_arch = "wasm32", declare)] pub type PublicKey = CryptoKey; +#[cfg_attr(target_arch = "wasm32", declare)] pub type SecretKey = CryptoKey; +#[cfg_attr(target_arch = "wasm32", declare)] pub type HashDigest = CryptoKey; +#[cfg_attr(target_arch = "wasm32", declare)] pub type SharedSecret = CryptoKey; +#[cfg_attr(target_arch = "wasm32", declare)] pub type RouteId = CryptoKey; +#[cfg_attr(target_arch = "wasm32", declare)] pub type CryptoKeyDistance = CryptoKey; byte_array_type!(Signature, SIGNATURE_LENGTH, SIGNATURE_LENGTH_ENCODED); diff --git a/veilid-core/src/crypto/types/crypto_typed.rs b/veilid-core/src/crypto/types/crypto_typed.rs index 740f888c..39f4bd59 100644 --- a/veilid-core/src/crypto/types/crypto_typed.rs +++ b/veilid-core/src/crypto/types/crypto_typed.rs @@ -1,6 +1,7 @@ use super::*; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(into_wasm_abi))] pub struct CryptoTyped where K: Clone diff --git a/veilid-core/src/crypto/types/crypto_typed_group.rs b/veilid-core/src/crypto/types/crypto_typed_group.rs index 80f717bc..77d0974c 100644 --- a/veilid-core/src/crypto/types/crypto_typed_group.rs +++ b/veilid-core/src/crypto/types/crypto_typed_group.rs @@ -1,7 +1,9 @@ use super::*; #[derive(Clone, Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq, Hash, Default)] +#[cfg_attr(target_arch = "wasm32", derive(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-core/src/crypto/types/keypair.rs b/veilid-core/src/crypto/types/keypair.rs index f8e6e6da..333a5d0b 100644 --- a/veilid-core/src/crypto/types/keypair.rs +++ b/veilid-core/src/crypto/types/keypair.rs @@ -1,10 +1,18 @@ use super::*; #[derive(Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq, Hash)] +#[cfg_attr( + target_arch = "wasm32", + derive(Tsify), + tsify(from_wasm_abi, into_wasm_abi) +)] pub struct KeyPair { + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub key: PublicKey, + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub secret: SecretKey, } +from_impl_to_jsvalue!(KeyPair); impl KeyPair { pub fn new(key: PublicKey, secret: SecretKey) -> Self { diff --git a/veilid-core/src/crypto/types/mod.rs b/veilid-core/src/crypto/types/mod.rs index af03453d..e3f24a81 100644 --- a/veilid-core/src/crypto/types/mod.rs +++ b/veilid-core/src/crypto/types/mod.rs @@ -6,6 +6,7 @@ use core::fmt; use core::hash::Hash; /// Cryptography version fourcc code +#[cfg_attr(target_arch = "wasm32", declare)] pub type CryptoKind = FourCC; /// Sort best crypto kinds first @@ -51,14 +52,24 @@ pub use crypto_typed::*; pub use crypto_typed_group::*; pub use keypair::*; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedKey = CryptoTyped; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedSecret = CryptoTyped; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedKeyPair = CryptoTyped; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedSignature = CryptoTyped; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedSharedSecret = CryptoTyped; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedKeyGroup = CryptoTypedGroup; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedSecretGroup = CryptoTypedGroup; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedKeyPairGroup = CryptoTypedGroup; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedSignatureGroup = CryptoTypedGroup; +#[cfg_attr(target_arch = "wasm32", declare)] pub type TypedSharedSecretGroup = CryptoTypedGroup; diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs index 63d7db63..f2cb5df9 100644 --- a/veilid-core/src/lib.rs +++ b/veilid-core/src/lib.rs @@ -56,6 +56,7 @@ mod table_store; mod veilid_api; mod veilid_config; mod veilid_layer_filter; +mod wasm_helpers; pub use self::api_tracing_layer::ApiTracingLayer; pub use self::core_context::{api_startup, api_startup_json, UpdateCallback}; @@ -127,3 +128,4 @@ use stop_token::*; use thiserror::Error as ThisError; use tracing::*; use veilid_tools::*; +use wasm_helpers::*; diff --git a/veilid-core/src/network_manager/wasm/.cargo/config.toml b/veilid-core/src/network_manager/wasm/.cargo/config.toml new file mode 100644 index 00000000..f4e8c002 --- /dev/null +++ b/veilid-core/src/network_manager/wasm/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/veilid-core/src/veilid_api/error.rs b/veilid-core/src/veilid_api/error.rs index 6fcc4861..d6999118 100644 --- a/veilid-core/src/veilid_api/error.rs +++ b/veilid-core/src/veilid_api/error.rs @@ -107,6 +107,7 @@ macro_rules! apibail_already_initialized { #[derive( ThisError, Clone, Debug, PartialOrd, PartialEq, Eq, Ord, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(into_wasm_abi))] #[serde(tag = "kind")] pub enum VeilidAPIError { #[error("Not initialized")] @@ -145,6 +146,7 @@ pub enum VeilidAPIError { #[error("Generic: {message}")] Generic { message: String }, } +from_impl_to_jsvalue!(VeilidAPIError); impl VeilidAPIError { pub fn not_initialized() -> Self { @@ -213,6 +215,7 @@ impl VeilidAPIError { } } +#[cfg_attr(target_arch = "wasm32", declare)] pub type VeilidAPIResult = Result; impl From for VeilidAPIError { diff --git a/veilid-core/src/veilid_api/types/aligned_u64.rs b/veilid-core/src/veilid_api/types/aligned_u64.rs index fbad5696..510cdba3 100644 --- a/veilid-core/src/veilid_api/types/aligned_u64.rs +++ b/veilid-core/src/veilid_api/types/aligned_u64.rs @@ -8,11 +8,13 @@ use super::*; #[derive( Clone, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Hash, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] #[repr(C, align(8))] #[serde(transparent)] pub struct AlignedU64( #[serde(with = "as_human_string")] #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] u64, ); @@ -117,13 +119,17 @@ impl AlignedU64 { ///////////////////////////////////////////////////////////////////////////////////////////////////// /// Microseconds since epoch +#[cfg_attr(target_arch = "wasm32", declare)] pub type Timestamp = AlignedU64; pub fn get_aligned_timestamp() -> Timestamp { get_timestamp().into() } /// Microseconds duration +#[cfg_attr(target_arch = "wasm32", declare)] pub type TimestampDuration = AlignedU64; /// Request/Response matching id +#[cfg_attr(target_arch = "wasm32", declare)] pub type OperationId = AlignedU64; /// Number of bytes +#[cfg_attr(target_arch = "wasm32", declare)] pub type ByteCount = AlignedU64; diff --git a/veilid-core/src/veilid_api/types/app_message_call.rs b/veilid-core/src/veilid_api/types/app_message_call.rs index 500588be..5af5b692 100644 --- a/veilid-core/src/veilid_api/types/app_message_call.rs +++ b/veilid-core/src/veilid_api/types/app_message_call.rs @@ -2,13 +2,16 @@ use super::*; /// Direct statement blob passed to hosting application for processing #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidAppMessage { #[serde(with = "as_human_opt_string")] #[schemars(with = "Option")] + #[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))] sender: Option, #[serde(with = "as_human_base64")] #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] message: Vec, } @@ -30,9 +33,11 @@ impl VeilidAppMessage { /// Direct question blob passed to hosting application for processing to send an eventual AppReply #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidAppCall { #[serde(with = "as_human_opt_string")] #[schemars(with = "Option")] + #[cfg_attr(target_arch = "wasm32", tsify(optional))] sender: Option, #[serde(with = "as_human_base64")] diff --git a/veilid-core/src/veilid_api/types/dht/dht_record_descriptor.rs b/veilid-core/src/veilid_api/types/dht/dht_record_descriptor.rs index 251d1e28..32c93ffd 100644 --- a/veilid-core/src/veilid_api/types/dht/dht_record_descriptor.rs +++ b/veilid-core/src/veilid_api/types/dht/dht_record_descriptor.rs @@ -2,20 +2,29 @@ use super::*; /// DHT Record Descriptor #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr( + target_arch = "wasm32", + derive(Tsify), + tsify(from_wasm_abi, into_wasm_abi) +)] pub struct DHTRecordDescriptor { /// DHT Key = Hash(ownerKeyKind) of: [ ownerKeyValue, schema ] #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] key: TypedKey, /// The public key of the owner #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] owner: PublicKey, /// If this key is being created: Some(the secret key of the owner) /// If this key is just being opened: None #[schemars(with = "Option")] + #[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))] owner_secret: Option, /// The schema in use associated with the key schema: DHTSchema, } +from_impl_to_jsvalue!(DHTRecordDescriptor); impl DHTRecordDescriptor { pub fn new( diff --git a/veilid-core/src/veilid_api/types/dht/mod.rs b/veilid-core/src/veilid_api/types/dht/mod.rs index 8b757907..bd7b93eb 100644 --- a/veilid-core/src/veilid_api/types/dht/mod.rs +++ b/veilid-core/src/veilid_api/types/dht/mod.rs @@ -11,6 +11,8 @@ pub use value_data::*; pub use value_subkey_range_set::*; /// Value subkey +#[cfg_attr(target_arch = "wasm32", declare)] pub type ValueSubkey = u32; /// Value sequence number +#[cfg_attr(target_arch = "wasm32", declare)] pub type ValueSeqNum = u32; diff --git a/veilid-core/src/veilid_api/types/dht/schema/dflt.rs b/veilid-core/src/veilid_api/types/dht/schema/dflt.rs index b2c6b2b7..bb4e7c6e 100644 --- a/veilid-core/src/veilid_api/types/dht/schema/dflt.rs +++ b/veilid-core/src/veilid_api/types/dht/schema/dflt.rs @@ -2,6 +2,7 @@ use super::*; /// Default DHT Schema (DFLT) #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi))] pub struct DHTSchemaDFLT { /// Owner subkey count pub o_cnt: u16, diff --git a/veilid-core/src/veilid_api/types/dht/schema/mod.rs b/veilid-core/src/veilid_api/types/dht/schema/mod.rs index ec54ae30..1b9bf4c3 100644 --- a/veilid-core/src/veilid_api/types/dht/schema/mod.rs +++ b/veilid-core/src/veilid_api/types/dht/schema/mod.rs @@ -9,6 +9,7 @@ pub use smpl::*; /// Enum over all the supported DHT Schemas #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind")] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi))] pub enum DHTSchema { DFLT(DHTSchemaDFLT), SMPL(DHTSchemaSMPL), diff --git a/veilid-core/src/veilid_api/types/dht/schema/smpl.rs b/veilid-core/src/veilid_api/types/dht/schema/smpl.rs index 572b93e8..d6da5ea6 100644 --- a/veilid-core/src/veilid_api/types/dht/schema/smpl.rs +++ b/veilid-core/src/veilid_api/types/dht/schema/smpl.rs @@ -2,9 +2,11 @@ use super::*; /// Simple DHT Schema (SMPL) Member #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi))] pub struct DHTSchemaSMPLMember { /// Member key #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub m_key: PublicKey, /// Member subkey count pub m_cnt: u16, @@ -12,6 +14,7 @@ pub struct DHTSchemaSMPLMember { /// Simple DHT Schema (SMPL) #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi))] pub struct DHTSchemaSMPL { /// Owner subkey count pub o_cnt: u16, diff --git a/veilid-core/src/veilid_api/types/dht/value_data.rs b/veilid-core/src/veilid_api/types/dht/value_data.rs index f5aee32e..34d6d8ac 100644 --- a/veilid-core/src/veilid_api/types/dht/value_data.rs +++ b/veilid-core/src/veilid_api/types/dht/value_data.rs @@ -2,6 +2,7 @@ use super::*; use veilid_api::VeilidAPIResult; #[derive(Clone, Default, PartialOrd, PartialEq, Eq, Ord, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(into_wasm_abi))] pub struct ValueData { /// An increasing sequence number to time-order the DHT record changes seq: ValueSeqNum, @@ -9,12 +10,16 @@ pub struct ValueData { /// The contents of a DHT Record #[serde(with = "as_human_base64")] #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] data: Vec, /// The public identity key of the writer of the data #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] writer: PublicKey, } +from_impl_to_jsvalue!(ValueData); + impl ValueData { pub const MAX_LEN: usize = 32768; diff --git a/veilid-core/src/veilid_api/types/fourcc.rs b/veilid-core/src/veilid_api/types/fourcc.rs index 5995c1b5..21f8db50 100644 --- a/veilid-core/src/veilid_api/types/fourcc.rs +++ b/veilid-core/src/veilid_api/types/fourcc.rs @@ -4,6 +4,7 @@ use super::*; #[derive( Copy, Default, Clone, Hash, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] #[serde(try_from = "String")] #[serde(into = "String")] pub struct FourCC(pub [u8; 4]); diff --git a/veilid-core/src/veilid_api/types/safety.rs b/veilid-core/src/veilid_api/types/safety.rs index 8e49cf5f..18c7b247 100644 --- a/veilid-core/src/veilid_api/types/safety.rs +++ b/veilid-core/src/veilid_api/types/safety.rs @@ -4,6 +4,7 @@ use super::*; #[derive( Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi, namespace))] pub enum Sequencing { NoPreference = 0, PreferOrdered = 1, @@ -20,6 +21,7 @@ impl Default for Sequencing { #[derive( Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi, namespace))] pub enum Stability { LowLatency = 0, Reliable = 1, @@ -35,6 +37,7 @@ impl Default for Stability { #[derive( Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi, namespace))] pub enum SafetySelection { /// Don't use a safety route, only specify the sequencing preference Unsafe(Sequencing), @@ -61,9 +64,11 @@ impl Default for SafetySelection { #[derive( Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct SafetySpec { /// preferred safety route set id if it still exists #[schemars(with = "Option")] + #[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))] pub preferred_route: Option, /// must be greater than 0 pub hop_count: usize, diff --git a/veilid-core/src/veilid_api/types/stats.rs b/veilid-core/src/veilid_api/types/stats.rs index 836cb728..5a6b27a8 100644 --- a/veilid-core/src/veilid_api/types/stats.rs +++ b/veilid-core/src/veilid_api/types/stats.rs @@ -1,6 +1,7 @@ use super::*; #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct LatencyStats { pub fastest: TimestampDuration, // fastest latency in the ROLLING_LATENCIES_SIZE last latencies pub average: TimestampDuration, // average latency over the ROLLING_LATENCIES_SIZE last latencies @@ -8,6 +9,7 @@ pub struct LatencyStats { } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct TransferStats { pub total: ByteCount, // total amount transferred ever pub maximum: ByteCount, // maximum rate over the ROLLING_TRANSFERS_SIZE last amounts @@ -16,12 +18,14 @@ pub struct TransferStats { } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct TransferStatsDownUp { pub down: TransferStats, pub up: TransferStats, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct RPCStats { pub messages_sent: u32, // number of rpcs that have been sent in the total_time range pub messages_rcvd: u32, // number of rpcs that have been received in the total_time range @@ -34,6 +38,7 @@ pub struct RPCStats { } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct PeerStats { pub time_added: Timestamp, // when the peer was added to the routing table pub rpc_stats: RPCStats, // information about RPCs diff --git a/veilid-core/src/veilid_api/types/veilid_log.rs b/veilid-core/src/veilid_api/types/veilid_log.rs index bf9c5963..2e8c9bf6 100644 --- a/veilid-core/src/veilid_api/types/veilid_log.rs +++ b/veilid-core/src/veilid_api/types/veilid_log.rs @@ -4,6 +4,7 @@ use super::*; #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Serialize, Deserialize, JsonSchema, )] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(namespace))] pub enum VeilidLogLevel { Error = 1, Warn = 2, @@ -80,8 +81,10 @@ impl fmt::Display for VeilidLogLevel { } /// A VeilidCore log message with optional backtrace #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidLog { pub log_level: VeilidLogLevel, pub message: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub backtrace: Option, } diff --git a/veilid-core/src/veilid_api/types/veilid_state.rs b/veilid-core/src/veilid_api/types/veilid_state.rs index 88ee35b9..6c3b4e60 100644 --- a/veilid-core/src/veilid_api/types/veilid_state.rs +++ b/veilid-core/src/veilid_api/types/veilid_state.rs @@ -2,6 +2,11 @@ use super::*; /// Attachment abstraction for network 'signal strength' #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, JsonSchema)] +#[cfg_attr( + target_arch = "wasm32", + derive(Tsify), + tsify(namespace, from_wasm_abi, into_wasm_abi) +)] pub enum AttachmentState { Detached = 0, Attaching = 1, @@ -48,6 +53,7 @@ impl TryFrom for AttachmentState { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidStateAttachment { pub state: AttachmentState, pub public_internet_ready: bool, @@ -55,14 +61,17 @@ pub struct VeilidStateAttachment { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct PeerTableData { #[schemars(with = "Vec")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string[]"))] pub node_ids: Vec, pub peer_address: String, pub peer_stats: PeerStats, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidStateNetwork { pub started: bool, pub bps_down: ByteCount, @@ -71,21 +80,27 @@ pub struct VeilidStateNetwork { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidRouteChange { #[schemars(with = "Vec")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub dead_routes: Vec, #[schemars(with = "Vec")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub dead_remote_routes: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidStateConfig { pub config: VeilidConfigInner, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidValueChange { #[schemars(with = "String")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub key: TypedKey, pub subkeys: Vec, pub count: u32, @@ -93,6 +108,7 @@ pub struct VeilidValueChange { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(into_wasm_abi))] #[serde(tag = "kind")] pub enum VeilidUpdate { Log(VeilidLog), @@ -105,10 +121,13 @@ pub enum VeilidUpdate { ValueChange(VeilidValueChange), Shutdown, } +from_impl_to_jsvalue!(VeilidUpdate); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(into_wasm_abi))] pub struct VeilidState { pub attachment: VeilidStateAttachment, pub network: VeilidStateNetwork, pub config: VeilidStateConfig, } +from_impl_to_jsvalue!(VeilidState); diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index 91027978..d8c6d67e 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -1,7 +1,9 @@ use crate::*; //////////////////////////////////////////////////////////////////////////////////////////////// +#[cfg_attr(target_arch = "wasm32", declare)] pub type ConfigCallbackReturn = VeilidAPIResult>; +#[cfg_attr(target_arch = "wasm32", declare)] pub type ConfigCallback = Arc ConfigCallbackReturn + Send + Sync>; /// Enable and configure HTTPS access to the Veilid node @@ -15,10 +17,12 @@ pub type ConfigCallback = Arc ConfigCallbackReturn + Send + Sy /// ``` /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigHTTPS { pub enabled: bool, pub listen_address: String, pub path: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub url: Option, // Fixed URL is not optional for TLS-based protocols and is dynamically validated } @@ -33,10 +37,12 @@ pub struct VeilidConfigHTTPS { /// ``` /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigHTTP { pub enabled: bool, pub listen_address: String, pub path: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub url: Option, } @@ -47,6 +53,7 @@ pub struct VeilidConfigHTTP { /// To be implemented... /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigApplication { pub https: VeilidConfigHTTPS, pub http: VeilidConfigHTTP, @@ -63,10 +70,12 @@ pub struct VeilidConfigApplication { /// ``` /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigUDP { pub enabled: bool, pub socket_pool_size: u32, pub listen_address: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub public_address: Option, } @@ -81,11 +90,13 @@ pub struct VeilidConfigUDP { /// public_address: '' /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigTCP { pub connect: bool, pub listen: bool, pub max_connections: u32, pub listen_address: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub public_address: Option, } @@ -101,6 +112,7 @@ pub struct VeilidConfigTCP { /// url: 'ws://localhost:5150/ws' /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigWS { pub connect: bool, @@ -108,6 +120,7 @@ pub struct VeilidConfigWS { pub max_connections: u32, pub listen_address: String, pub path: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub url: Option, } @@ -123,6 +136,7 @@ pub struct VeilidConfigWS { /// url: '' /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigWSS { pub connect: bool, @@ -130,6 +144,7 @@ pub struct VeilidConfigWSS { pub max_connections: u32, pub listen_address: String, pub path: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub url: Option, // Fixed URL is not optional for TLS-based protocols and is dynamically validated } @@ -141,6 +156,7 @@ pub struct VeilidConfigWSS { /// sort out which protocol is used for each peer connection. /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigProtocol { pub udp: VeilidConfigUDP, @@ -158,6 +174,7 @@ pub struct VeilidConfigProtocol { /// connection_initial_timeout_ms: 2000 /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigTLS { pub certificate_path: String, pub private_key_path: String, @@ -167,6 +184,7 @@ pub struct VeilidConfigTLS { /// Configure the Distributed Hash Table (DHT) /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigDHT { pub max_find_node_count: u32, pub resolve_node_timeout_ms: u32, @@ -192,10 +210,13 @@ pub struct VeilidConfigDHT { /// Configure RPC /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigRPC { pub concurrency: u32, pub queue_size: u32, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub max_timestamp_behind_ms: Option, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub max_timestamp_ahead_ms: Option, pub timeout_ms: u32, pub max_route_hop_count: u8, @@ -205,6 +226,7 @@ pub struct VeilidConfigRPC { /// Configure the network routing table /// #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigRoutingTable { #[schemars(with = "Vec")] pub node_id: TypedKeyGroup, @@ -221,6 +243,7 @@ pub struct VeilidConfigRoutingTable { } #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigNetwork { pub connection_initial_timeout_ms: u32, pub connection_inactivity_timeout_ms: u32, @@ -231,6 +254,7 @@ pub struct VeilidConfigNetwork { pub client_whitelist_timeout_ms: u32, pub reverse_connection_receipt_time_ms: u32, pub hole_punch_receipt_time_ms: u32, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub network_key_password: Option, pub routing_table: VeilidConfigRoutingTable, pub rpc: VeilidConfigRPC, @@ -244,33 +268,40 @@ pub struct VeilidConfigNetwork { } #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigTableStore { pub directory: String, pub delete: bool, } #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigBlockStore { pub directory: String, pub delete: bool, } #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigProtectedStore { pub allow_insecure_fallback: bool, pub always_use_insecure_storage: bool, pub directory: String, pub delete: bool, pub device_encryption_key_password: String, + #[cfg_attr(target_arch = "wasm32", tsify(optional))] pub new_device_encryption_key_password: Option, } #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigCapabilities { pub disable: Vec, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] +#[cfg_attr(target_arch = "wasm32", tsify(namespace, from_wasm_abi))] pub enum VeilidConfigLogLevel { Off, Error, @@ -358,6 +389,7 @@ impl fmt::Display for VeilidConfigLogLevel { } #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidConfigInner { pub program_name: String, pub namespace: String, diff --git a/veilid-core/src/wasm_helpers.rs b/veilid-core/src/wasm_helpers.rs new file mode 100644 index 00000000..c2f8994b --- /dev/null +++ b/veilid-core/src/wasm_helpers.rs @@ -0,0 +1,21 @@ +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + pub use tsify::*; + pub use wasm_bindgen::prelude::*; + + macro_rules! from_impl_to_jsvalue { + ($name: ident) => { + impl From<$name> for JsValue { + fn from(value: $name) -> Self { + serde_wasm_bindgen::to_value(&value).unwrap() + } + } + } + } + } else { + macro_rules! from_impl_to_jsvalue { + ($name: ident) => {} + } + } +} +pub(crate) use from_impl_to_jsvalue; diff --git a/veilid-wasm/.cargo/config.toml b/veilid-wasm/.cargo/config.toml new file mode 100644 index 00000000..f4e8c002 --- /dev/null +++ b/veilid-wasm/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/veilid-wasm/Cargo.toml b/veilid-wasm/Cargo.toml index 129840f9..059cfd70 100644 --- a/veilid-wasm/Cargo.toml +++ b/veilid-wasm/Cargo.toml @@ -34,6 +34,9 @@ send_wrapper = "^0" futures-util = { version = "^0" } data-encoding = { version = "^2" } gloo-utils = { version = "^0", features = ["serde"] } +tsify = { version = "0.4.5", features = ["js"] } +serde-wasm-bindgen = "0.5.0" [dev-dependencies] wasm-bindgen-test = "^0" +parking_lot = "0.12.1" 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 ed6172e7..77767ea5 100644 --- a/veilid-wasm/src/lib.rs +++ b/veilid-wasm/src/lib.rs @@ -19,11 +19,20 @@ use serde::*; use tracing_subscriber::prelude::*; use tracing_subscriber::*; use tracing_wasm::{WASMLayerConfigBuilder, *}; +use tsify::*; use veilid_core::tools::*; use veilid_core::*; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::*; +pub mod veilid_client_js; +pub mod veilid_crypto_js; +pub mod veilid_routing_context_js; +pub mod veilid_table_db_js; + +mod wasm_helpers; +use wasm_helpers::*; + // Allocator extern crate wee_alloc; #[global_allocator] @@ -56,6 +65,17 @@ fn take_veilid_api() -> Result Vec { + data_encoding::BASE64URL_NOPAD + .decode(b64.as_bytes()) + .unwrap() +} + +pub fn marshall(data: &Vec) -> String { + data_encoding::BASE64URL_NOPAD.encode(data) +} + // JSON Helpers for WASM pub fn to_json(val: T) -> JsValue { JsValue::from_str(&serialize_json(val)) @@ -112,6 +132,7 @@ where // WASM-specific #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidWASMConfigLoggingPerformance { pub enabled: bool, pub level: veilid_core::VeilidConfigLogLevel, @@ -120,28 +141,38 @@ pub struct VeilidWASMConfigLoggingPerformance { } #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidWASMConfigLoggingAPI { pub enabled: bool, pub level: veilid_core::VeilidConfigLogLevel, } #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] pub struct VeilidWASMConfigLogging { pub performance: VeilidWASMConfigLoggingPerformance, pub api: VeilidWASMConfigLoggingAPI, } #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify), tsify(from_wasm_abi))] pub struct VeilidWASMConfig { pub logging: VeilidWASMConfigLogging, } #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr( + target_arch = "wasm32", + derive(Tsify), + tsify(from_wasm_abi, into_wasm_abi) +)] pub struct VeilidRouteBlob { pub route_id: veilid_core::RouteId, #[serde(with = "veilid_core::as_human_base64")] + #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] pub blob: Vec, } +from_impl_to_jsvalue!(VeilidRouteBlob); // WASM Bindings @@ -215,6 +246,11 @@ pub fn change_log_level(layer: String, log_level: String) { } } +#[wasm_bindgen(typescript_custom_section)] +const IUPDATE_VEILID_FUNCTION: &'static str = r#" +type UpdateVeilidFunction = (event: VeilidUpdate) => void; +"#; + #[wasm_bindgen()] pub fn startup_veilid_core(update_callback_js: Function, json_config: String) -> Promise { let update_callback_js = SendWrapper::new(update_callback_js); @@ -378,7 +414,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() }; @@ -1454,6 +1490,8 @@ pub fn veilid_version_string() -> String { } #[derive(Serialize)] +#[cfg_attr(target_arch = "wasm32", derive(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..0629adb9 --- /dev/null +++ b/veilid-wasm/src/veilid_client_js.rs @@ -0,0 +1,169 @@ +#![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"); + } + + /// Initialize a Veilid node, with the configuration in JSON format + /// + /// Must be called only once at the start of an application + /// + /// @param {UpdateVeilidFunction} update_callback_js - called when internal state of the Veilid node changes, for example, when app-level messages are received, when private routes die and need to be reallocated, or when routing table states change + /// @param {string} json_config - called at startup to supply a JSON configuration object. + pub async fn startupCore( + update_callback_js: UpdateVeilidFunction, + json_config: String, + ) -> APIResult<()> { + 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 APIResult::Err(veilid_core::VeilidAPIError::AlreadyInitialized); + } + + let veilid_api = veilid_core::api_startup_json(update_callback, json_config).await?; + VEILID_API.replace(Some(veilid_api)); + APIRESULT_UNDEFINED + } + + // 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); + } + } + + /// Shut down Veilid and terminate the API. + pub async fn shutdownCore() -> APIResult<()> { + let veilid_api = take_veilid_api()?; + veilid_api.shutdown().await; + APIRESULT_UNDEFINED + } + + /// Get a full copy of the current state of Veilid. + pub async fn getState() -> APIResult { + let veilid_api = get_veilid_api()?; + let core_state = veilid_api.get_state().await?; + APIResult::Ok(core_state) + } + + /// Connect to the network. + pub async fn attach() -> APIResult<()> { + let veilid_api = get_veilid_api()?; + veilid_api.attach().await?; + APIRESULT_UNDEFINED + } + + /// Disconnect from the network. + pub async fn detach() -> APIResult<()> { + let veilid_api = get_veilid_api()?; + veilid_api.detach().await?; + APIRESULT_UNDEFINED + } + + /// Execute an 'internal debug command'. + pub async fn debug(command: String) -> APIResult { + let veilid_api = get_veilid_api()?; + let out = veilid_api.debug(command).await?; + APIResult::Ok(out) + } + + /// Return the cargo package version of veilid-core, in object format. + pub fn version() -> VeilidVersion { + let (major, minor, patch) = veilid_core::veilid_version(); + let vv = super::VeilidVersion { + major, + minor, + patch, + }; + vv + } + + /// Return the cargo package version of veilid-core, in string format. + pub fn versionString() -> String { + veilid_core::veilid_version_string() + } +} diff --git a/veilid-wasm/src/veilid_crypto_js.rs b/veilid-wasm/src/veilid_crypto_js.rs new file mode 100644 index 00000000..350bac32 --- /dev/null +++ b/veilid-wasm/src/veilid_crypto_js.rs @@ -0,0 +1,470 @@ +#![allow(non_snake_case)] +use super::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "string[]")] + pub type ValidCryptoKinds; +} + +#[wasm_bindgen(js_name = veilidCrypto)] +pub struct VeilidCrypto {} + +// 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 = veilidCrypto)] +impl VeilidCrypto { + pub fn validCryptoKinds() -> StringArray { + let res = veilid_core::VALID_CRYPTO_KINDS + .iter() + .map(|k| (*k).to_string()) + .collect(); + into_unchecked_string_array(res) + } + + pub fn bestCryptoKind() -> String { + veilid_core::best_crypto_kind().to_string() + } + + pub fn cachedDh(kind: String, key: String, secret: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; + let secret: veilid_core::SecretKey = veilid_core::SecretKey::from_str(&secret)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_cached_dh", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.cached_dh(&key, &secret)?; + APIResult::Ok(out.to_string()) + } + + pub fn computeDh(kind: String, key: String, secret: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; + let secret: veilid_core::SecretKey = veilid_core::SecretKey::from_str(&secret)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_compute_dh", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.compute_dh(&key, &secret)?; + APIResult::Ok(out.to_string()) + } + + pub fn randomBytes(kind: String, len: u32) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_random_bytes", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.random_bytes(len); + let out = data_encoding::BASE64URL_NOPAD.encode(&out); + APIResult::Ok(out) + } + + pub fn defaultSaltLength(kind: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_default_salt_length", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.default_salt_length(); + APIResult::Ok(out) + } + + pub fn hashPassword(kind: String, password: String, salt: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + let password: Vec = data_encoding::BASE64URL_NOPAD + .decode(password.as_bytes()) + .unwrap(); + let salt: Vec = data_encoding::BASE64URL_NOPAD + .decode(salt.as_bytes()) + .unwrap(); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_hash_password", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.hash_password(&password, &salt)?; + APIResult::Ok(out) + } + + pub fn verifyPassword( + kind: String, + password: String, + password_hash: String, + ) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + let password: Vec = data_encoding::BASE64URL_NOPAD + .decode(password.as_bytes()) + .unwrap(); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_verify_password", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.verify_password(&password, &password_hash)?; + APIResult::Ok(out) + } + + pub fn deriveSharedSecret(kind: String, password: String, salt: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + let password: Vec = data_encoding::BASE64URL_NOPAD + .decode(password.as_bytes()) + .unwrap(); + let salt: Vec = data_encoding::BASE64URL_NOPAD + .decode(salt.as_bytes()) + .unwrap(); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_derive_shared_secret", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.derive_shared_secret(&password, &salt)?; + APIResult::Ok(out.to_string()) + } + + pub fn randomNonce(kind: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_random_nonce", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.random_nonce(); + APIResult::Ok(out.to_string()) + } + + pub fn randomSharedSecret(kind: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_random_shared_secret", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.random_shared_secret(); + APIResult::Ok(out.to_string()) + } + + pub fn generateKeyPair(kind: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_generate_key_pair", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.generate_keypair(); + APIResult::Ok(out) + } + + pub fn generateHash(kind: String, data: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let data: Vec = data_encoding::BASE64URL_NOPAD + .decode(data.as_bytes()) + .unwrap(); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_generate_hash", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.generate_hash(&data); + APIResult::Ok(out.to_string()) + } + + pub fn validateKeyPair(kind: String, key: String, secret: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; + let secret: veilid_core::SecretKey = veilid_core::SecretKey::from_str(&secret)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_validate_key_pair", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.validate_keypair(&key, &secret); + APIResult::Ok(out) + } + + pub fn validateHash(kind: String, data: String, hash: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let data: Vec = data_encoding::BASE64URL_NOPAD + .decode(data.as_bytes()) + .unwrap(); + + let hash: veilid_core::HashDigest = veilid_core::HashDigest::from_str(&hash)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_validate_hash", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.validate_hash(&data, &hash); + APIResult::Ok(out) + } + + pub fn distance(kind: String, key1: String, key2: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let key1: veilid_core::CryptoKey = veilid_core::CryptoKey::from_str(&key1)?; + let key2: veilid_core::CryptoKey = veilid_core::CryptoKey::from_str(&key2)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_distance", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.distance(&key1, &key2); + APIResult::Ok(out.to_string()) + } + + pub fn sign(kind: String, key: String, secret: String, data: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; + let secret: veilid_core::SecretKey = veilid_core::SecretKey::from_str(&secret)?; + + let data: Vec = data_encoding::BASE64URL_NOPAD + .decode(data.as_bytes()) + .unwrap(); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument("crypto_sign", "kind", kind.to_string()) + })?; + let out = crypto_system.sign(&key, &secret, &data)?; + APIResult::Ok(out.to_string()) + } + + pub fn verify(kind: String, key: String, data: String, signature: String) -> APIResult<()> { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; + let data: Vec = data_encoding::BASE64URL_NOPAD + .decode(data.as_bytes()) + .unwrap(); + let signature: veilid_core::Signature = veilid_core::Signature::from_str(&signature)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument("crypto_verify", "kind", kind.to_string()) + })?; + crypto_system.verify(&key, &data, &signature)?; + APIRESULT_UNDEFINED + } + + pub fn aeadOverhead(kind: String) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_aead_overhead", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.aead_overhead(); + APIResult::Ok(out) + } + + pub fn decryptAead( + kind: String, + body: String, + nonce: String, + shared_secret: String, + associated_data: Option, + ) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let body: Vec = data_encoding::BASE64URL_NOPAD + .decode(body.as_bytes()) + .unwrap(); + + let nonce: veilid_core::Nonce = veilid_core::Nonce::from_str(&nonce)?; + + let shared_secret: veilid_core::SharedSecret = + veilid_core::SharedSecret::from_str(&shared_secret)?; + + let associated_data: Option> = associated_data.map(|ad| { + data_encoding::BASE64URL_NOPAD + .decode(ad.as_bytes()) + .unwrap() + }); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_decrypt_aead", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.decrypt_aead( + &body, + &nonce, + &shared_secret, + match &associated_data { + Some(ad) => Some(ad.as_slice()), + None => None, + }, + )?; + let out = data_encoding::BASE64URL_NOPAD.encode(&out); + APIResult::Ok(out) + } + + pub fn encryptAead( + kind: String, + body: String, + nonce: String, + shared_secret: String, + associated_data: Option, + ) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let body: Vec = data_encoding::BASE64URL_NOPAD + .decode(body.as_bytes()) + .unwrap(); + + let nonce: veilid_core::Nonce = veilid_core::Nonce::from_str(&nonce).unwrap(); + + let shared_secret: veilid_core::SharedSecret = + veilid_core::SharedSecret::from_str(&shared_secret).unwrap(); + + let associated_data: Option> = associated_data.map(|ad| { + data_encoding::BASE64URL_NOPAD + .decode(ad.as_bytes()) + .unwrap() + }); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_encrypt_aead", + "kind", + kind.to_string(), + ) + })?; + let out = crypto_system.encrypt_aead( + &body, + &nonce, + &shared_secret, + match &associated_data { + Some(ad) => Some(ad.as_slice()), + None => None, + }, + )?; + let out = data_encoding::BASE64URL_NOPAD.encode(&out); + APIResult::Ok(out) + } + + pub fn cryptNoAuth( + kind: String, + body: String, + nonce: String, + shared_secret: String, + ) -> APIResult { + let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; + + let mut body: Vec = data_encoding::BASE64URL_NOPAD + .decode(body.as_bytes()) + .unwrap(); + + let nonce: veilid_core::Nonce = veilid_core::Nonce::from_str(&nonce).unwrap(); + + let shared_secret: veilid_core::SharedSecret = + veilid_core::SharedSecret::from_str(&shared_secret).unwrap(); + + let veilid_api = get_veilid_api()?; + let crypto = veilid_api.crypto()?; + let crypto_system = crypto.get(kind).ok_or_else(|| { + veilid_core::VeilidAPIError::invalid_argument( + "crypto_crypt_no_auth", + "kind", + kind.to_string(), + ) + })?; + crypto_system.crypt_in_place_no_auth(&mut body, &nonce, &shared_secret); + let out = data_encoding::BASE64URL_NOPAD.encode(&body); + APIResult::Ok(out) + } +} diff --git a/veilid-wasm/src/veilid_routing_context_js.rs b/veilid-wasm/src/veilid_routing_context_js.rs new file mode 100644 index 00000000..50db6fe7 --- /dev/null +++ b/veilid-wasm/src/veilid_routing_context_js.rs @@ -0,0 +1,333 @@ +#![allow(non_snake_case)] +use super::*; + +#[wasm_bindgen()] +pub struct VeilidRoutingContext { + inner_routing_context: Option, +} + +#[wasm_bindgen()] +impl VeilidRoutingContext { + /// Don't use this constructor directly. + /// Use one of the `VeilidRoutingContext.create___()` factory methods instead. + /// @deprecated + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner_routing_context: None, + } + } + + // -------------------------------- + // Constructor factories + // -------------------------------- + + /// Get a new RoutingContext object to use to send messages over the Veilid network. + pub fn createWithoutPrivacy() -> APIResult { + let veilid_api = get_veilid_api()?; + let routing_context = veilid_api.routing_context(); + Ok(VeilidRoutingContext { + inner_routing_context: Some(routing_context), + }) + } + + /// Turn on sender privacy, enabling the use of safety routes. + /// + /// Default values for hop count, stability and sequencing preferences are used. + /// + /// Hop count default is dependent on config, but is set to 1 extra hop. + /// Stability default is to choose 'low latency' routes, preferring them over long-term reliability. + /// Sequencing default is to have no preference for ordered vs unordered message delivery + /// To modify these defaults, use `VeilidRoutingContext.createWithCustomPrivacy()`. + pub fn createWithPrivacy() -> APIResult { + let veilid_api = get_veilid_api()?; + let routing_context = veilid_api.routing_context().with_privacy()?; + Ok(VeilidRoutingContext { + inner_routing_context: Some(routing_context), + }) + } + + /// Turn on privacy using a custom `SafetySelection` + pub fn createWithCustomPrivacy( + safety_selection: SafetySelection, + ) -> APIResult { + let veilid_api = get_veilid_api()?; + let routing_context = veilid_api + .routing_context() + .with_custom_privacy(safety_selection)?; + Ok(VeilidRoutingContext { + inner_routing_context: Some(routing_context), + }) + } + + /// Use a specified `Sequencing` preference, with or without privacy. + pub fn createWithSequencing(sequencing: Sequencing) -> APIResult { + let veilid_api = get_veilid_api()?; + let routing_context = veilid_api.routing_context().with_sequencing(sequencing); + Ok(VeilidRoutingContext { + inner_routing_context: Some(routing_context), + }) + } + + // -------------------------------- + // Static methods + // -------------------------------- + + /// Allocate a new private route set with default cryptography and network options. + /// Returns a route id and a publishable 'blob' with the route encrypted with each crypto kind. + /// Those nodes importing the blob will have their choice of which crypto kind to use. + /// + /// Returns a route id and 'blob' that can be published over some means (DHT or otherwise) to be imported by another Veilid node. + pub async fn newPrivateRoute() -> APIResult { + let veilid_api = get_veilid_api()?; + + let (route_id, blob) = veilid_api.new_private_route().await?; + + let route_blob = VeilidRouteBlob { route_id, blob }; + APIResult::Ok(route_blob) + } + + /// Allocate a new private route and specify a specific cryptosystem, stability and sequencing preference. + /// Returns a route id and a publishable 'blob' with the route encrypted with each crypto kind. + /// Those nodes importing the blob will have their choice of which crypto kind to use. + /// + /// Returns a route id and 'blob' that can be published over some means (DHT or otherwise) to be imported by another Veilid node. + pub async fn newCustomPrivateRoute( + stability: Stability, + sequencing: Sequencing, + ) -> APIResult { + let veilid_api = get_veilid_api()?; + + let (route_id, blob) = veilid_api + .new_custom_private_route(&veilid_core::VALID_CRYPTO_KINDS, stability, sequencing) + .await?; + + let route_blob = VeilidRouteBlob { route_id, blob }; + APIResult::Ok(route_blob) + } + + /// Release either a locally allocated or remotely imported private route. + /// + /// This will deactivate the route and free its resources and it can no longer be sent to or received from. + pub fn releasePrivateRoute(route_id: String) -> APIResult<()> { + let route_id: veilid_core::RouteId = veilid_core::deserialize_json(&route_id).unwrap(); + let veilid_api = get_veilid_api()?; + veilid_api.release_private_route(route_id)?; + APIRESULT_UNDEFINED + } + + /// Respond to an AppCall received over a VeilidUpdate::AppCall. + /// + /// * `call_id` - specifies which call to reply to, and it comes from a VeilidUpdate::AppCall, specifically the VeilidAppCall::id() value. + /// * `message` - is an answer blob to be returned by the remote node's RoutingContext::app_call() function, and may be up to 32768 bytes + pub async fn appCallReply(call_id: String, message: String) -> APIResult<()> { + let message = unmarshall(message); + let call_id = match call_id.parse() { + Ok(v) => v, + Err(e) => { + return APIResult::Err(veilid_core::VeilidAPIError::invalid_argument( + e, "call_id", call_id, + )) + } + }; + let veilid_api = get_veilid_api()?; + veilid_api.app_call_reply(call_id, message).await?; + APIRESULT_UNDEFINED + } + + // -------------------------------- + // Instance methods + // -------------------------------- + fn getRoutingContext(&self) -> APIResult { + let Some(routing_context) = &self.inner_routing_context else { + return APIResult::Err(veilid_core::VeilidAPIError::generic("Unable to getRoutingContext instance. inner_routing_context is None.")); + }; + APIResult::Ok(routing_context.clone()) + } + + /// App-level unidirectional message that does not expect any value to be returned. + /// + /// Veilid apps may use this for arbitrary message passing. + /// + /// @param {string} target - can be either a direct node id or a private route. + /// @param {string} message - an arbitrary message blob of up to `32768` bytes. + #[wasm_bindgen(skip_jsdoc)] + pub async fn appMessage(&self, target_string: String, message: String) -> APIResult<()> { + let routing_context = self.getRoutingContext()?; + let message = unmarshall(message); + + let veilid_api = get_veilid_api()?; + let target = veilid_api.parse_as_target(target_string).await?; + routing_context.app_message(target, message).await?; + APIRESULT_UNDEFINED + } + + /// App-level bidirectional call that expects a response to be returned. + /// + /// Veilid apps may use this for arbitrary message passing. + /// + /// @param {string} target_string - can be either a direct node id or a private route, base64Url encoded. + /// @param {string} message - an arbitrary message blob of up to `32768` bytes, base64Url encoded. + /// @returns an answer blob of up to `32768` bytes, base64Url encoded. + #[wasm_bindgen(skip_jsdoc)] + pub async fn appCall(&self, target_string: String, request: String) -> APIResult { + let request: Vec = unmarshall(request); + let routing_context = self.getRoutingContext()?; + + let veilid_api = get_veilid_api()?; + let target = veilid_api.parse_as_target(target_string).await?; + let answer = routing_context.app_call(target, request).await?; + let answer = marshall(&answer); + APIResult::Ok(answer) + } + + /// DHT Records Creates a new DHT record a specified crypto kind and schema + /// + /// The record is considered 'open' after the create operation succeeds. + /// + /// @returns the newly allocated DHT record's key if successful. + pub async fn createDhtRecord( + &self, + schema: DHTSchema, + kind: String, + ) -> APIResult { + let crypto_kind = if kind.is_empty() { + None + } else { + Some(veilid_core::FourCC::from_str(&kind)?) + }; + let routing_context = self.getRoutingContext()?; + + let dht_record_descriptor = routing_context + .create_dht_record(schema, crypto_kind) + .await?; + APIResult::Ok(dht_record_descriptor) + } + + /// Opens a DHT record at a specific key. + /// + /// Associates a secret if one is provided to provide writer capability. Records may only be opened or created. To re-open with a different routing context, first close the value. + /// + /// @returns the DHT record descriptor for the opened record if successful. + /// @param {string} writer - Stringified key pair, in the form of `key:secret` where `key` and `secret` are base64Url encoded. + /// @param {string} key - key of the DHT record. + #[wasm_bindgen(skip_jsdoc)] + pub async fn openDhtRecord( + &self, + key: String, + writer: Option, + ) -> APIResult { + let key = TypedKey::from_str(&key).unwrap(); + let writer = match writer { + Some(writer) => Some(KeyPair::from_str(&writer).unwrap()), + _ => None, + }; + + let routing_context = self.getRoutingContext()?; + let dht_record_descriptor = routing_context.open_dht_record(key, writer).await?; + APIResult::Ok(dht_record_descriptor) + } + + /// Closes a DHT record at a specific key that was opened with create_dht_record or open_dht_record. + /// + /// Closing a record allows you to re-open it with a different routing context + pub async fn closeDhtRecord(&self, key: String) -> APIResult<()> { + let key = TypedKey::from_str(&key).unwrap(); + let routing_context = self.getRoutingContext()?; + routing_context.close_dht_record(key).await?; + APIRESULT_UNDEFINED + } + + /// Deletes a DHT record at a specific key + /// + /// If the record is opened, it must be closed before it is deleted. + /// Deleting a record does not delete it from the network, but will remove the storage of the record locally, + /// and will prevent its value from being refreshed on the network by this node. + pub async fn deleteDhtRecord(&self, key: String) -> APIResult<()> { + let key = TypedKey::from_str(&key).unwrap(); + let routing_context = self.getRoutingContext()?; + routing_context.delete_dht_record(key).await?; + APIRESULT_UNDEFINED + } + + /// Gets the latest value of a subkey. + /// + /// May pull the latest value from the network, but by settings 'force_refresh' you can force a network data refresh. + /// + /// Returns `undefined` if the value subkey has not yet been set. + /// Returns base64Url encoded `data` if the value subkey has valid data. + pub async fn getDhtValue( + &self, + key: String, + subKey: u32, + forceRefresh: bool, + ) -> APIResult> { + let key = TypedKey::from_str(&key).unwrap(); + let routing_context = self.getRoutingContext()?; + let res = routing_context + .get_dht_value(key, subKey, forceRefresh) + .await?; + APIResult::Ok(res) + } + + /// Pushes a changed subkey value to the network + /// + /// Returns `undefined` if the value was successfully put. + /// Returns base64Url encoded `data` if the value put was older than the one available on the network. + pub async fn setDhtValue( + &self, + key: String, + subKey: u32, + data: String, + ) -> APIResult> { + let key = TypedKey::from_str(&key).unwrap(); + let data: Vec = data_encoding::BASE64URL_NOPAD + .decode(&data.as_bytes()) + .unwrap(); + + let routing_context = self.getRoutingContext()?; + let res = routing_context.set_dht_value(key, subKey, data).await?; + APIResult::Ok(res) + } + + // pub async fn watchDhtValues( + // &self, + // key: String, + // subKeys: ValueSubkeyRangeSet, + // expiration: Timestamp, + // count: u32, + // ) -> APIResult { + // let key: veilid_core::TypedKey = veilid_core::deserialize_json(&key).unwrap(); + // let subkeys: veilid_core::ValueSubkeyRangeSet = + // veilid_core::deserialize_json(&subkeys).unwrap(); + // let expiration = veilid_core::Timestamp::from_str(&expiration).unwrap(); + + // 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_watch_dht_values", "id", self.id)); + // }; + // routing_context.clone() + // }; + // let res = routing_context + // .watch_dht_values(key, subkeys, expiration, count) + // .await?; + // APIResult::Ok(res.to_string()) + // } + + // pub async fn cancelDhtWatch(id: u32, key: String, subkeys: String) -> Promise { + // let key: veilid_core::TypedKey = veilid_core::deserialize_json(&key).unwrap(); + // let subkeys: veilid_core::ValueSubkeyRangeSet = + // veilid_core::deserialize_json(&subkeys).unwrap(); + + // 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_cancel_dht_watch", "id", self.id)); + // }; + // routing_context.clone() + // }; + // let res = routing_context.cancel_dht_watch(key, subkeys).await?; + // APIResult::Ok(res) + // } +} diff --git a/veilid-wasm/src/veilid_table_db_js.rs b/veilid-wasm/src/veilid_table_db_js.rs new file mode 100644 index 00000000..2a8c235a --- /dev/null +++ b/veilid-wasm/src/veilid_table_db_js.rs @@ -0,0 +1,176 @@ +#![allow(non_snake_case)] +use super::*; + +#[wasm_bindgen()] +pub struct VeilidTableDB { + inner_table_db: Option, + tableName: String, + columnCount: u32, +} + +#[wasm_bindgen()] +impl VeilidTableDB { + /// If the column count is greater than an existing TableDB's column count, + /// the database will be upgraded to add the missing columns. + #[wasm_bindgen(constructor)] + pub fn new(tableName: String, columnCount: u32) -> Self { + Self { + inner_table_db: None, + tableName, + columnCount, + } + } + + fn getTableDB(&self) -> APIResult { + let Some(table_db) = &self.inner_table_db else { + return APIResult::Err(veilid_core::VeilidAPIError::generic("Unable to getTableDB instance. Ensure you've called openTable().")); + }; + APIResult::Ok(table_db.clone()) + } + + /// Get or create the TableDB database table. + /// This is called automatically when performing actions on the TableDB. + pub async fn openTable(&mut self) -> APIResult<()> { + 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)?; + self.inner_table_db = Some(table_db); + APIRESULT_UNDEFINED + } + + /// Delete this TableDB. + pub async fn deleteTable(&mut self) -> APIResult { + self.inner_table_db = None; + + 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.inner_table_db.is_none() { + let _ = self.openTable().await; + } + } + + /// Read a key from a column in the TableDB immediately. + pub async fn load(&mut self, columnId: u32, key: String) -> APIResult> { + self.ensureOpen().await; + let key = unmarshall(key); + let table_db = self.getTableDB()?; + + let out = table_db.load(columnId, &key).await?.unwrap(); + let out = Some(marshall(&out)); + APIResult::Ok(out) + } + + /// Store a key with a value in a column in the TableDB. + /// Performs a single transaction immediately. + pub async fn store(&mut self, columnId: u32, key: String, value: String) -> APIResult<()> { + self.ensureOpen().await; + let key = unmarshall(key); + let value = unmarshall(value); + let table_db = self.getTableDB()?; + + table_db.store(columnId, &key, &value).await?; + APIRESULT_UNDEFINED + } + + /// Delete key with from a column in the TableDB. + pub async fn delete(&mut self, columnId: u32, key: String) -> APIResult> { + self.ensureOpen().await; + let key = unmarshall(key); + let table_db = self.getTableDB()?; + + let out = table_db.delete(columnId, &key).await?.unwrap(); + let out = Some(marshall(&out)); + APIResult::Ok(out) + } + + /// Get the list of keys in a column of the TableDB. + /// + /// Returns an array of base64Url encoded keys. + pub async fn getKeys(&mut self, columnId: u32) -> APIResult { + self.ensureOpen().await; + let table_db = self.getTableDB()?; + + let keys = table_db.clone().get_keys(columnId).await?; + let out: Vec = keys.into_iter().map(|k| marshall(&k)).collect(); + let out = into_unchecked_string_array(out); + + APIResult::Ok(out) + } + + /// Start a TableDB write transaction. + /// The transaction object must be committed or rolled back before dropping. + pub async fn createTransaction(&mut self) -> APIResult { + self.ensureOpen().await; + let table_db = self.getTableDB()?; + + let transaction = table_db.transact(); + APIResult::Ok(VeilidTableDBTransaction { + inner_transaction: Some(transaction), + }) + } +} + +#[wasm_bindgen] +pub struct VeilidTableDBTransaction { + inner_transaction: Option, +} + +#[wasm_bindgen] +impl VeilidTableDBTransaction { + /// Don't use this constructor directly. + /// Use `.createTransaction()` on an instance of `VeilidTableDB` instead. + /// @deprecated + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner_transaction: None, + } + } + + fn getTransaction(&self) -> APIResult { + let Some(transaction) = &self.inner_transaction else { + return APIResult::Err(veilid_core::VeilidAPIError::generic("Unable to getTransaction instance. inner_transaction is None.")); + }; + APIResult::Ok(transaction.clone()) + } + + /// Commit the transaction. Performs all actions atomically. + pub async fn commit(&self) -> APIResult<()> { + let transaction = self.getTransaction()?; + transaction.commit().await + } + + /// Rollback the transaction. Does nothing to the TableDB. + pub fn rollback(&self) -> APIResult<()> { + let transaction = self.getTransaction()?; + transaction.rollback(); + APIRESULT_UNDEFINED + } + + /// Store a key with a value in a column in the TableDB. + /// Does not modify TableDB until `.commit()` is called. + pub fn store(&self, col: u32, key: String, value: String) -> APIResult<()> { + let key = unmarshall(key); + let value = unmarshall(value); + let transaction = self.getTransaction()?; + transaction.store(col, &key, &value) + } + + /// Delete key with from a column in the TableDB + pub fn deleteKey(&self, col: u32, key: String) -> APIResult<()> { + let key = unmarshall(key); + let transaction = self.getTransaction()?; + transaction.delete(col, &key) + } +} diff --git a/veilid-wasm/src/wasm_helpers.rs b/veilid-wasm/src/wasm_helpers.rs new file mode 100644 index 00000000..f8e93468 --- /dev/null +++ b/veilid-wasm/src/wasm_helpers.rs @@ -0,0 +1,38 @@ +use super::*; + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + pub use tsify::*; + pub use wasm_bindgen::prelude::*; + + macro_rules! from_impl_to_jsvalue { + ($name: ident) => { + impl From<$name> for JsValue { + fn from(value: $name) -> Self { + serde_wasm_bindgen::to_value(&value).unwrap() + } + } + } + } + } else { + macro_rules! from_impl_to_jsvalue { + ($name: ident) => {} + } + } +} +pub(crate) use from_impl_to_jsvalue; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "string[]")] + pub type StringArray; +} + +/// Convert a `Vec` into a `js_sys::Array` with the type of `string[]` +pub(crate) fn into_unchecked_string_array(items: Vec) -> StringArray { + items + .iter() + .map(JsValue::from) + .collect::() + .unchecked_into::() // TODO: can I do this a better way? +} diff --git a/veilid-wasm/tests/web.rs b/veilid-wasm/tests/web.rs index 8022f2bb..0c251687 100644 --- a/veilid-wasm/tests/web.rs +++ b/veilid-wasm/tests/web.rs @@ -1,182 +1,218 @@ //! Test suite for the Web and headless browsers. - -//XXXXXXXXXXXXXXX -//XXX DOES NOT WORK. +//! These tests only work with WASM_BINDGEN_USE_NO_MODULE=true env var, +//! as otherwise there's no way to access the generated wasm bindings from inside JS. #![cfg(target_arch = "wasm32")] extern crate alloc; extern crate wasm_bindgen_test; -use core::sync::atomic::AtomicBool; +use js_sys::*; +use parking_lot::Once; use veilid_wasm::*; +use wasm_bindgen::*; +use wasm_bindgen_futures::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); -static SETUP_ONCE: AtomicBool = AtomicBool::new(false); +static SETUP_ONCE: Once = Once::new(); pub fn setup() -> () { - if SETUP_ONCE - .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - console_log("setup()"); + SETUP_ONCE.call_once(|| { + console_log!("setup()"); console_error_panic_hook::set_once(); - wasm_logger::init(wasm_logger::Config::new(Level::Trace)); init_callbacks(); - } + }) } -// xxx needs updating to new keys and veilid_api object + fn init_callbacks() { assert_eq!(js_sys::eval(r#" window.sleep = (milliseconds) => { return new Promise(resolve => setTimeout(resolve, milliseconds)) }; - window.stateChangeCallback = async (stateChange) => { console.log("State change: " + stateChange); }; - window.configCallback = (configKey) => { - switch(configKey) { - case "namespace": return ""; - case "capabilities.disable": return []; - case "tablestore.directory": return ""; - case "network.routing_table.node_id": return []; - case "network.routing_table.node_id_secret": return []; - case "network.routing_table.bootstrap": return []; - case "network.routing_table.limit_over_attached": return 64; - case "network.routing_table.limit_fully_attached": return 32; - case "network.routing_table.limit_attached_strong": return 16; - case "network.routing_table.limit_attached_good": return 8; - case "network.routing_table.limit_attached_weak": return 4; - case "network.rpc.concurrency": return 2; - case "network.rpc.queue_size": return 128; - case "network.rpc.max_timestamp_behind": return 10000000; - case "network.rpc.max_timestamp_ahead": return 10000000; - case "network.rpc.timeout": return 10000000; - case "network.rpc.max_route_hop_count": return 4; - case "network.rpc.default_route_hop_count": return 1; - case "network.dht.max_find_node_count": return 20; - case "network.dht.resolve_node_timeout": return 10000; - case "network.dht.resolve_node_count": return 1; - case "network.dht.resolve_node_fanout": return 4; - case "network.dht.get_value_timeout": return 10000; - case "network.dht.get_value_count": return 3; - case "network.dht.get_value_fanout": return 4; - case "network.dht.set_value_timeout": return 10000; - case "network.dht.set_value_count": return 5; - case "network.dht.set_value_fanout": return 4; - case "network.dht.min_peer_count": return 20; - case "network.dht.min_peer_refresh_time": return 2000000; - case "network.dht.validate_dial_info_receipt_time": return 5000000; - case "network.upnp": return false; - case "network.detect_address_changes": return true; - case "network.address_filter": return true; - case "network.restricted_nat_retries": return 3; - case "network.tls.certificate_path": return ""; - case "network.tls.private_key_path": return ""; - case "network.application.path": return "/app"; - case "network.application.https.enabled": return false; - case "network.application.https.listen_address": return ""; - case "network.application.http.enabled": return false; - case "network.application.http.listen_address": return ""; - case "network.protocol.udp.enabled": return false; - case "network.protocol.udp.socket_pool_size": return 0; - case "network.protocol.udp.listen_address": return ""; - case "network.protocol.udp.public_address": return ""; - case "network.protocol.tcp.connect": return false; - case "network.protocol.tcp.listen": return false; - case "network.protocol.tcp.max_connections": return 32; - case "network.protocol.tcp.listen_address": return ""; - case "network.protocol.tcp.public_address": return ""; - case "network.protocol.ws.connect": return true; - case "network.protocol.ws.listen": return false; - case "network.protocol.ws.max_connections": return 16; - case "network.protocol.ws.listen_address": return ""; - case "network.protocol.ws.path": return "/ws"; - case "network.protocol.ws.public_address": return ""; - case "network.protocol.wss.connect": return true; - case "network.protocol.wss.listen": return false; - case "network.protocol.wss.max_connections": return 16; - case "network.protocol.wss.listen_address": return ""; - case "network.protocol.wss.path": return "/ws"; - case "network.protocol.wss.public_address": return ""; - default: - console.log("config key '" + key +"' doesn't exist"); break; - } + window.stateChangeCallback = async (stateChange) => { + delete stateChange.peers; // makes logs less verbose + console.log("State change: ", JSON.stringify(stateChange, null, 2)); }; + window.veilidCoreInitConfig = { + logging: { + api: { + enabled: true, + level: 'Info', + }, + performance: { + enabled: false, + level: 'Info', + logs_in_timings: false, + logs_in_console: false, + }, + }, + }; + + window.veilidCoreStartupConfig = { + program_name: 'veilid-wasm-test', + namespace: '', + capabilities: { + disable: [], + }, + protected_store: { + allow_insecure_fallback: true, + always_use_insecure_storage: true, + directory: '', + delete: false, + device_encryption_key_password: 'some-user-secret-value', + // "new_device_encryption_key_password": "an-updated-user-secret-value" + }, + table_store: { + directory: '', + delete: false, + }, + block_store: { + directory: '', + delete: false, + }, + network: { + connection_initial_timeout_ms: 2000, + connection_inactivity_timeout_ms: 60000, + max_connections_per_ip4: 32, + max_connections_per_ip6_prefix: 32, + max_connections_per_ip6_prefix_size: 56, + max_connection_frequency_per_min: 128, + client_whitelist_timeout_ms: 300000, + reverse_connection_receipt_time_ms: 5000, + hole_punch_receipt_time_ms: 5000, + network_key_password: '', + disable_capabilites: [], + routing_table: { + node_id: [], + node_id_secret: [], + bootstrap: [ + 'ws://bootstrap.veilid.net:5150/ws', + ], + limit_over_attached: 64, + limit_fully_attached: 32, + limit_attached_strong: 16, + limit_attached_good: 8, + limit_attached_weak: 4, + }, + rpc: { + concurrency: 0, + queue_size: 1024, + max_timestamp_behind_ms: 10000, + max_timestamp_ahead_ms: 10000, + timeout_ms: 5000, + max_route_hop_count: 4, + default_route_hop_count: 1, + }, + dht: { + max_find_node_count: 20, + resolve_node_timeout_ms: 10000, + resolve_node_count: 1, + resolve_node_fanout: 4, + get_value_timeout_ms: 10000, + get_value_count: 3, + get_value_fanout: 4, + set_value_timeout_ms: 10000, + set_value_count: 5, + set_value_fanout: 4, + min_peer_count: 20, + min_peer_refresh_time_ms: 60000, + validate_dial_info_receipt_time_ms: 2000, + local_subkey_cache_size: 128, + local_max_subkey_cache_memory_mb: 256, + remote_subkey_cache_size: 1024, + remote_max_records: 65536, + remote_max_subkey_cache_memory_mb: 256, + remote_max_storage_space_mb: 0, + }, + upnp: true, + detect_address_changes: true, + restricted_nat_retries: 0, + tls: { + certificate_path: '', + private_key_path: '', + connection_initial_timeout_ms: 2000, + }, + application: { + https: { + enabled: false, + listen_address: ':5150', + path: 'app', + }, + http: { + enabled: false, + listen_address: ':5150', + path: 'app', + }, + }, + protocol: { + udp: { + enabled: false, + socket_pool_size: 0, + listen_address: '', + }, + tcp: { + connect: false, + listen: false, + max_connections: 32, + listen_address: '', + }, + ws: { + connect: true, + listen: true, + max_connections: 16, + listen_address: ':5150', + path: 'ws', + }, + wss: { + connect: true, + listen: false, + max_connections: 16, + listen_address: '', + path: 'ws', + }, + }, + }, + }; true "#).expect("failed to eval"), JsValue::TRUE); } -/////////////////////////////////////////////////////////////////////////////////////////////////// -/// - -#[wasm_bindgen_test] -fn test_construct() { - setup(); - - assert_eq!( - js_sys::eval( - r#" - let vc = new VeilidCore(); - true - "# - ) - .expect("failed to eval"), - JsValue::TRUE - ); -} - -#[wasm_bindgen_test(async)] -async fn test_startup_shutdown() { - setup(); - - assert_eq!( - JsFuture::from( - js_sys::eval( - r#" - (async function() { - let vc = new VeilidCore(); - await vc.startup(window.stateChangeCallback, window.configCallback); - await vc.shutdown(); - return true; - })().then(v => { - console.log("finished: " + v); - return v; - }); - "# - ) - .expect("failed to eval") +/// Helper for converting an eval Promise result into a JsValue +async fn eval_promise(source: &str) -> JsValue { + JsFuture::from( + eval(source) + .expect("Failed to eval") .dyn_into::() - .unwrap() - ) - .await, - Ok(JsValue::TRUE) - ); + .unwrap(), + ) + .await + .unwrap() } -#[wasm_bindgen_test(async)] -async fn test_attach_detach() { +// ---------------------------------------------------------------- + +// TODO: now that veilidClient uses a single instance of VeilidAPI, +// subsequent tests fail because veilidCore has already been initialized. +#[wasm_bindgen_test()] +async fn test_kitchen_sink() { setup(); - assert_eq!( - JsFuture::from( - js_sys::eval( - r#" - (async function() { - let vc = new VeilidCore(); - await vc.startup(window.stateChangeCallback, window.configCallback); - await vc.attach(); - await window.sleep(1000); - await vc.detach(); - await vc.shutdown(); - return true; - })().then(v => { - console.log("finished: " + v); - return v; - }); - "# - ) - .expect("failed to eval") - .dyn_into::() - .unwrap() - ) - .await, - Ok(JsValue::TRUE) - ); + let res = eval_promise( + r#" + (async function () { + const { veilidClient } = wasm_bindgen; // only accessible in no_module mode. + veilidClient.initializeCore(window.veilidCoreInitConfig); + await veilidClient.startupCore(window.stateChangeCallback, JSON.stringify(window.veilidCoreStartupConfig)); + + console.log(veilidClient.versionString()); + await veilidClient.attach(); + + await sleep(10000); + await veilidClient.detach(); + await veilidClient.shutdownCore(); + + return true; + })(); + "#, + ).await; + + assert_eq!(res, JsValue::TRUE); } diff --git a/veilid-wasm/wasm_build.sh b/veilid-wasm/wasm_build.sh index 7a6bb0f5..5cf70dc6 100755 --- a/veilid-wasm/wasm_build.sh +++ b/veilid-wasm/wasm_build.sh @@ -37,7 +37,7 @@ if [[ "$1" == "release" ]]; then cargo build --target wasm32-unknown-unknown --release mkdir -p $OUTPUTDIR - wasm-bindgen --out-dir $OUTPUTDIR --target web $INPUTDIR/veilid_wasm.wasm + wasm-bindgen --out-dir $OUTPUTDIR --target web --weak-refs $INPUTDIR/veilid_wasm.wasm wasm-strip $OUTPUTDIR/veilid_wasm_bg.wasm else OUTPUTDIR=../target/wasm32-unknown-unknown/debug/pkg @@ -45,7 +45,7 @@ else RUSTFLAGS="-O -g $RUSTFLAGS" cargo build --target wasm32-unknown-unknown mkdir -p $OUTPUTDIR - wasm-bindgen --out-dir $OUTPUTDIR --target web --keep-debug --debug $INPUTDIR/veilid_wasm.wasm + wasm-bindgen --out-dir $OUTPUTDIR --target web --weak-refs --keep-debug --debug $INPUTDIR/veilid_wasm.wasm ./wasm-sourcemap.py $OUTPUTDIR/veilid_wasm_bg.wasm -o $OUTPUTDIR/veilid_wasm_bg.wasm.map --dwarfdump $DWARFDUMP # wasm-strip $OUTPUTDIR/veilid_wasm_bg.wasm fi diff --git a/veilid-wasm/wasm_test.sh b/veilid-wasm/wasm_test.sh new file mode 100755 index 00000000..27209505 --- /dev/null +++ b/veilid-wasm/wasm_test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eo pipefail + +WASM_BINDGEN_USE_NO_MODULE=true wasm-pack test --firefox "$@"