Merge branch 'tsify-typescript-wasm' into 'main'

[WASM] New APIs: Enhanced JS interop and TypeScript type generation

See merge request veilid/veilid!130
This commit is contained in:
Christien Rioux 2023-09-06 12:59:08 +00:00
commit d6c3d8fddd
No known key found for this signature in database
38 changed files with 1656 additions and 159 deletions

70
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -231,7 +231,7 @@ impl VeilidCoreContext {
update_callback: UpdateCallback,
config_json: String,
) -> VeilidAPIResult<VeilidCoreContext> {
// 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())?;

View File

@ -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);

View File

@ -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<K>
where
K: Clone

View File

@ -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<CryptoTyped<K>>", into = "Vec<CryptoTyped<K>>")]
// TODO: figure out hot to TS type this as `string`, since it's converted to string via the JSON API.
pub struct CryptoTypedGroup<K = PublicKey>
where
K: Clone

View File

@ -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 {

View File

@ -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<PublicKey>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedSecret = CryptoTyped<SecretKey>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedKeyPair = CryptoTyped<KeyPair>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedSignature = CryptoTyped<Signature>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedSharedSecret = CryptoTyped<SharedSecret>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedKeyGroup = CryptoTypedGroup<PublicKey>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedSecretGroup = CryptoTypedGroup<SecretKey>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedKeyPairGroup = CryptoTypedGroup<KeyPair>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedSignatureGroup = CryptoTypedGroup<Signature>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type TypedSharedSecretGroup = CryptoTypedGroup<SharedSecret>;

View File

@ -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::*;

View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

View File

@ -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<T> = Result<T, VeilidAPIError>;
impl From<std::io::Error> for VeilidAPIError {

View File

@ -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;

View File

@ -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<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))]
sender: Option<TypedKey>,
#[serde(with = "as_human_base64")]
#[schemars(with = "String")]
#[cfg_attr(target_arch = "wasm32", tsify(type = "string"))]
message: Vec<u8>,
}
@ -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<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(optional))]
sender: Option<TypedKey>,
#[serde(with = "as_human_base64")]

View File

@ -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<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))]
owner_secret: Option<SecretKey>,
/// The schema in use associated with the key
schema: DHTSchema,
}
from_impl_to_jsvalue!(DHTRecordDescriptor);
impl DHTRecordDescriptor {
pub fn new(

View File

@ -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;

View File

@ -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,

View File

@ -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),

View File

@ -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,

View File

@ -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<u8>,
/// 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;

View File

@ -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]);

View File

@ -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<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))]
pub preferred_route: Option<RouteId>,
/// must be greater than 0
pub hop_count: usize,

View File

@ -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

View File

@ -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<String>,
}

View File

@ -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<String> 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<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(type = "string[]"))]
pub node_ids: Vec<TypedKey>,
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<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(type = "string"))]
pub dead_routes: Vec<RouteId>,
#[schemars(with = "Vec<String>")]
#[cfg_attr(target_arch = "wasm32", tsify(type = "string"))]
pub dead_remote_routes: Vec<RouteId>,
}
#[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<ValueSubkey>,
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);

View File

@ -1,7 +1,9 @@
use crate::*;
////////////////////////////////////////////////////////////////////////////////////////////////
#[cfg_attr(target_arch = "wasm32", declare)]
pub type ConfigCallbackReturn = VeilidAPIResult<Box<dyn core::any::Any + Send>>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type ConfigCallback = Arc<dyn Fn(String) -> ConfigCallbackReturn + Send + Sync>;
/// Enable and configure HTTPS access to the Veilid node
@ -15,10 +17,12 @@ pub type ConfigCallback = Arc<dyn Fn(String) -> 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<String>, // 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<String>,
}
@ -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<String>,
}
@ -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<String>,
}
@ -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<String>,
}
@ -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<String>, // 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<u32>,
#[cfg_attr(target_arch = "wasm32", tsify(optional))]
pub max_timestamp_ahead_ms: Option<u32>,
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<String>")]
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<String>,
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<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(target_arch = "wasm32", derive(Tsify))]
pub struct VeilidConfigCapabilities {
pub disable: Vec<FourCC>,
}
#[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,

View File

@ -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;

View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

View File

@ -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"

6
veilid-wasm/README.md Normal file
View File

@ -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.

View File

@ -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<veilid_core::VeilidAPI, veilid_core::VeilidAPIErr
.ok_or(veilid_core::VeilidAPIError::NotInitialized)
}
// Marshalling helpers
pub fn unmarshall(b64: String) -> Vec<u8> {
data_encoding::BASE64URL_NOPAD
.decode(b64.as_bytes())
.unwrap()
}
pub fn marshall(data: &Vec<u8>) -> String {
data_encoding::BASE64URL_NOPAD.encode(data)
}
// JSON Helpers for WASM
pub fn to_json<T: Serialize + Debug>(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<u8>,
}
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,

View File

@ -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<VeilidState> {
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<String> {
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()
}
}

View File

@ -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<String> {
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<String> {
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<String> {
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<u32> {
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<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let password: Vec<u8> = data_encoding::BASE64URL_NOPAD
.decode(password.as_bytes())
.unwrap();
let salt: Vec<u8> = 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<bool> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let password: Vec<u8> = 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<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let password: Vec<u8> = data_encoding::BASE64URL_NOPAD
.decode(password.as_bytes())
.unwrap();
let salt: Vec<u8> = 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<String> {
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<String> {
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<KeyPair> {
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<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let data: Vec<u8> = 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<bool> {
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<bool> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let data: Vec<u8> = 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<String> {
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<String> {
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<u8> = 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<u8> = 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<usize> {
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<String>,
) -> APIResult<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let body: Vec<u8> = 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<Vec<u8>> = 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<String>,
) -> APIResult<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let body: Vec<u8> = 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<Vec<u8>> = 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<String> {
let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?;
let mut body: Vec<u8> = 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)
}
}

View File

@ -0,0 +1,333 @@
#![allow(non_snake_case)]
use super::*;
#[wasm_bindgen()]
pub struct VeilidRoutingContext {
inner_routing_context: Option<RoutingContext>,
}
#[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<VeilidRoutingContext> {
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<VeilidRoutingContext> {
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<VeilidRoutingContext> {
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<VeilidRoutingContext> {
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<VeilidRouteBlob> {
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<VeilidRouteBlob> {
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<RoutingContext> {
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<String> {
let request: Vec<u8> = 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<DHTRecordDescriptor> {
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<String>,
) -> APIResult<DHTRecordDescriptor> {
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<Option<ValueData>> {
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<Option<ValueData>> {
let key = TypedKey::from_str(&key).unwrap();
let data: Vec<u8> = 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<String> {
// 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)
// }
}

View File

@ -0,0 +1,176 @@
#![allow(non_snake_case)]
use super::*;
#[wasm_bindgen()]
pub struct VeilidTableDB {
inner_table_db: Option<TableDB>,
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<TableDB> {
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<bool> {
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<Option<String>> {
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<Option<String>> {
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<StringArray> {
self.ensureOpen().await;
let table_db = self.getTableDB()?;
let keys = table_db.clone().get_keys(columnId).await?;
let out: Vec<String> = 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<VeilidTableDBTransaction> {
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<TableDBTransaction>,
}
#[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<TableDBTransaction> {
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)
}
}

View File

@ -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<String>` into a `js_sys::Array` with the type of `string[]`
pub(crate) fn into_unchecked_string_array(items: Vec<String>) -> StringArray {
items
.iter()
.map(JsValue::from)
.collect::<js_sys::Array>()
.unchecked_into::<StringArray>() // TODO: can I do this a better way?
}

View File

@ -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::<Promise>()
.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::<Promise>()
.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);
}

View File

@ -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

4
veilid-wasm/wasm_test.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
set -eo pipefail
WASM_BINDGEN_USE_NO_MODULE=true wasm-pack test --firefox "$@"