(feat): redesign WASM module, add TypeScript SDK, migrate to pnpm
Some checks failed
CI / rustfmt (push) Successful in 17s
CI / wasm build (push) Successful in 1m16s
CI / clippy (push) Successful in 1m28s
CI / test (push) Successful in 1m48s
CI / example (push) Successful in 1m31s
CI / duplicate code (push) Failing after 33s
CI / web client (push) Failing after 34s
CI / cargo-machete (push) Successful in 1m18s
CI / cargo-deny (push) Failing after 3m2s

This commit is contained in:
Alois 2026-06-27 23:44:27 +02:00
commit 5caa1c9d5f
49 changed files with 3717 additions and 1501 deletions

View file

@ -1,6 +1,9 @@
use std::cell::Cell;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use futures_channel::oneshot;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
@ -8,9 +11,116 @@ use mtp_type_map::CommunicationTypeId;
use mtp_crypto::SignatureScheme;
use crate::config::ConnectionConfig;
use crate::error::js_error;
use crate::transport::WasmTransport;
struct PendingRequest {
response_type: Option<String>,
sender: oneshot::Sender<Result<JsValue, JsValue>>,
}
struct PingTimer {
id: i32,
closure: Closure<dyn FnMut()>,
}
fn frame_property(frame: &JsValue, key: &str) -> Option<JsValue> {
js_sys::Reflect::get(frame, &JsValue::from_str(key))
.ok()
.filter(|value| !value.is_null() && !value.is_undefined())
}
fn frame_id(frame: &JsValue) -> Option<u32> {
frame_property(frame, "id")
.and_then(|value| value.as_f64())
.map(|value| value as u32)
}
fn frame_type(frame: &JsValue) -> Option<String> {
frame_property(frame, "type").and_then(|value| value.as_string())
}
fn route_incoming_frame(
frame: &JsValue,
on_message: &js_sys::Function,
subscriptions: &Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
) {
let message_type = frame_type(frame);
if let Some(request_id) = frame_id(frame) {
let pending = pending_requests.borrow_mut().remove(&request_id);
if let Some(pending) = pending {
let type_matches = pending
.response_type
.as_ref()
.zip(message_type.as_ref())
.map(|(expected, actual)| expected == actual)
.unwrap_or(true);
if type_matches {
let _ = pending.sender.send(Ok(frame.clone()));
} else {
let actual = message_type.clone().unwrap_or_else(|| "unknown".into());
let _ = pending.sender.send(Err(js_error(&format!(
"unexpected response type: expected {}, got {}",
pending.response_type.unwrap_or_else(|| "unknown".into()),
actual
))));
}
}
}
if let Some(message_type) = message_type.as_ref() {
let matching_id = pending_requests
.borrow()
.iter()
.find_map(|(id, pending)| match pending.response_type.as_ref() {
Some(response_type) if response_type == message_type => Some(*id),
_ => None,
});
if let Some(id) = matching_id {
if let Some(pending) = pending_requests.borrow_mut().remove(&id) {
let _ = pending.sender.send(Ok(frame.clone()));
}
}
}
let _ = on_message.call1(&JsValue::NULL, frame);
let Some(message_type) = message_type else {
return;
};
for (_, (subscription_type, callback)) in subscriptions.borrow().iter() {
if subscription_type == &message_type {
let _ = callback.call1(&JsValue::NULL, frame);
}
}
}
fn stop_ping_timer(ping_timer: &Rc<RefCell<Option<PingTimer>>>) {
let Some(timer) = ping_timer.borrow_mut().take() else {
return;
};
if let Ok(clear_interval) =
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
{
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
}
drop(timer.closure);
}
fn reject_pending_requests(
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
message: &str,
) {
let pending = std::mem::take(&mut *pending_requests.borrow_mut());
for (_, pending) in pending {
let _ = pending.sender.send(Err(js_error(message)));
}
}
fn raw_frame_preview(bytes: &[u8]) -> String {
let shown = bytes.len().min(256);
let mut preview = hex::encode(&bytes[..shown]);
@ -139,45 +249,6 @@ pub enum ConnectionState {
Failed = 3,
}
#[wasm_bindgen]
pub struct ConnectionConfig {
url: String,
server_certificate_hashes: Option<Vec<String>>,
client_id: u64,
}
#[wasm_bindgen]
impl ConnectionConfig {
#[wasm_bindgen(constructor)]
pub fn new(url: String) -> Self {
Self {
url,
server_certificate_hashes: None,
client_id: 0,
}
}
#[wasm_bindgen(getter)]
pub fn url(&self) -> String {
self.url.clone()
}
#[wasm_bindgen(setter)]
pub fn set_client_id(&mut self, id: u64) {
self.client_id = id;
}
#[wasm_bindgen(getter)]
pub fn client_id(&self) -> u64 {
self.client_id
}
#[wasm_bindgen(setter)]
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
self.server_certificate_hashes = Some(hashes);
}
}
#[wasm_bindgen]
pub struct WasmClient {
transport: Option<WasmTransport>,
@ -185,6 +256,10 @@ pub struct WasmClient {
on_state_change: js_sys::Function,
pub(crate) on_message: js_sys::Function,
pub(crate) on_error: js_sys::Function,
subscriptions: Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
next_subscription_id: Rc<Cell<u32>>,
pending_requests: Rc<RefCell<HashMap<u32, PendingRequest>>>,
ping_timer: Rc<RefCell<Option<PingTimer>>>,
}
#[wasm_bindgen]
@ -201,6 +276,10 @@ impl WasmClient {
on_state_change: on_state_change.clone(),
on_message: on_message.clone(),
on_error: on_error.clone(),
subscriptions: Rc::new(RefCell::new(HashMap::new())),
next_subscription_id: Rc::new(Cell::new(1)),
pending_requests: Rc::new(RefCell::new(HashMap::new())),
ping_timer: Rc::new(RefCell::new(None)),
}
}
@ -455,12 +534,134 @@ impl WasmClient {
}
}
#[wasm_bindgen]
pub async fn request(
&self,
frame: Vec<u8>,
response_type: Option<String>,
) -> Result<JsValue, JsValue> {
let request = CommunicationValue::from_bytes(&frame)
.map_err(|e| js_error(&format!("parse request: {}", e)))?;
let request_id = request.get_id();
if request_id == 0 {
return Err(js_error("request frame must have a non-zero id"));
}
let Some(transport) = self.transport.clone() else {
return Err(js_error("not connected"));
};
let (sender, receiver) = oneshot::channel();
self.pending_requests.borrow_mut().insert(
request_id,
PendingRequest {
response_type,
sender,
},
);
if let Err(error) = transport.send_frame(&frame).await {
self.pending_requests.borrow_mut().remove(&request_id);
return Err(error);
}
match receiver.await {
Ok(result) => result,
Err(_) => Err(js_error("request cancelled")),
}
}
#[wasm_bindgen]
pub fn subscribe(&self, message_type: String, callback: js_sys::Function) -> u32 {
let id = self.next_subscription_id.get();
self.next_subscription_id.set(id.wrapping_add(1).max(1));
self.subscriptions
.borrow_mut()
.insert(id, (message_type, callback));
id
}
#[wasm_bindgen]
pub fn unsubscribe(&self, id: u32) -> bool {
self.subscriptions.borrow_mut().remove(&id).is_some()
}
#[wasm_bindgen]
pub fn start_protocol_pings(&self, interval_ms: u32, client_id: u64) -> Result<(), JsValue> {
self.stop_protocol_pings();
let Some(transport) = self.transport.clone() else {
return Err(js_error("not connected"));
};
let interval_ms = interval_ms.max(1_000) as i32;
let on_error = self.on_error.clone();
let closure = Closure::wrap(Box::new(move || {
let transport = transport.clone();
let on_error = on_error.clone();
wasm_bindgen_futures::spawn_local(async move {
let timestamp = js_sys::Date::now() as u64;
let frame = CommunicationValue::new(CommunicationType::Ping)
.add_typed_default(
DataType::Description,
DataValue::Str("protocol ping".into()),
)
.add_typed_default(
DataType::Timestamp,
DataValue::UnsignedNumber(timestamp as u128),
)
.with_sender(client_id)
.to_bytes()
.map_err(|e| js_error(&format!("encode ping failed: {}", e)));
match frame {
Ok(frame) => {
if let Err(error) = transport.send_frame(&frame).await {
let _ = on_error.call1(&JsValue::NULL, &error);
}
}
Err(error) => {
let _ = on_error.call1(&JsValue::NULL, &error);
}
}
});
}) as Box<dyn FnMut()>);
let set_interval =
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setInterval"))?
.dyn_into::<js_sys::Function>()?;
let id = set_interval
.call2(
&JsValue::NULL,
closure.as_ref().unchecked_ref(),
&JsValue::from_f64(interval_ms as f64),
)?
.as_f64()
.ok_or_else(|| js_error("setInterval did not return an id"))? as i32;
*self.ping_timer.borrow_mut() = Some(PingTimer { id, closure });
Ok(())
}
#[wasm_bindgen]
pub fn stop_protocol_pings(&self) {
let Some(timer) = self.ping_timer.borrow_mut().take() else {
return;
};
if let Ok(clear_interval) =
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
{
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
}
drop(timer.closure);
}
#[wasm_bindgen]
pub fn disconnect(&mut self) {
self.stop_protocol_pings();
if let Some(t) = &self.transport {
t.close();
}
self.transport = None;
self.subscriptions.borrow_mut().clear();
self.reject_pending_requests("disconnected");
self.set_state(ConnectionState::Disconnected);
}
@ -479,12 +680,34 @@ impl WasmClient {
let state = self.state.clone();
let on_msg = self.on_message.clone();
let on_err = self.on_error.clone();
let subscriptions = self.subscriptions.clone();
let pending_requests = self.pending_requests.clone();
let loop_pending_requests = pending_requests.clone();
let ping_timer = self.ping_timer.clone();
wasm_bindgen_futures::spawn_local(async move {
loop_transport.receive_loop(on_msg, on_err).await;
let route_frame = Closure::wrap(Box::new(move |frame: JsValue| {
route_incoming_frame(&frame, &on_msg, &subscriptions, &loop_pending_requests);
}) as Box<dyn FnMut(JsValue)>);
loop_transport
.receive_loop(
route_frame
.as_ref()
.unchecked_ref::<js_sys::Function>()
.clone(),
on_err.clone(),
)
.await;
drop(route_frame);
state.set(ConnectionState::Disconnected);
stop_ping_timer(&ping_timer);
reject_pending_requests(&pending_requests, "disconnected");
});
}
fn reject_pending_requests(&self, message: &str) {
reject_pending_requests(&self.pending_requests, message);
}
async fn read_verified_challenge(
&self,
transport: &WasmTransport,

40
wasm/src/config.rs Normal file
View file

@ -0,0 +1,40 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct ConnectionConfig {
pub(crate) url: String,
pub(crate) server_certificate_hashes: Option<Vec<String>>,
pub(crate) client_id: u64,
}
#[wasm_bindgen]
impl ConnectionConfig {
#[wasm_bindgen(constructor)]
pub fn new(url: String) -> Self {
Self {
url,
server_certificate_hashes: None,
client_id: 0,
}
}
#[wasm_bindgen(getter)]
pub fn url(&self) -> String {
self.url.clone()
}
#[wasm_bindgen(setter)]
pub fn set_client_id(&mut self, id: u64) {
self.client_id = id;
}
#[wasm_bindgen(getter)]
pub fn client_id(&self) -> u64 {
self.client_id
}
#[wasm_bindgen(setter)]
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
self.server_certificate_hashes = Some(hashes);
}
}

479
wasm/src/frame.rs Normal file
View file

@ -0,0 +1,479 @@
use wasm_bindgen::{JsCast, prelude::*};
use mtp_codec::{
CommunicationType, CommunicationValue, DataType, DataValue, communication_type_name,
data_type_name,
};
use mtp_type_map::TypeMap;
use crate::error::js_error;
#[wasm_bindgen(typescript_custom_section)]
const PARSED_FRAME_TS: &'static str = r#"
export interface ParsedFrame {
id?: number;
type: string;
sender?: bigint;
receiver?: bigint;
data: Record<string, unknown>;
raw: Uint8Array;
}
"#;
fn set_prop(obj: &js_sys::Object, key: &str, value: &JsValue) -> Result<(), JsValue> {
js_sys::Reflect::set(obj, &JsValue::from_str(key), value).map(|_| ())
}
fn integer_value(value: &str) -> JsValue {
if let Ok(number) = value.parse::<f64>() {
if number.fract() == 0.0 && number.abs() <= 9_007_199_254_740_991.0 {
return JsValue::from_f64(number);
}
}
JsValue::from_str(value)
}
fn data_value_to_js(value: &DataValue) -> Result<JsValue, JsValue> {
match value {
DataValue::BoolTrue => Ok(JsValue::TRUE),
DataValue::BoolFalse => Ok(JsValue::FALSE),
DataValue::Bool(v) => Ok(JsValue::from_bool(*v)),
DataValue::SignedNumber(n) => Ok(integer_value(&n.to_string())),
DataValue::UnsignedNumber(n) => Ok(integer_value(&n.to_string())),
DataValue::Float(exp, mant) => {
Ok(JsValue::from_f64((*mant as f64) * 10f64.powi(*exp as i32)))
}
DataValue::Str(s) => Ok(JsValue::from_str(s)),
DataValue::Bytes(bytes) => Ok(js_sys::Uint8Array::from(&bytes[..]).into()),
DataValue::Array(values) => {
let arr = js_sys::Array::new();
for value in values {
arr.push(&data_value_to_js(value)?);
}
Ok(arr.into())
}
DataValue::Container(entries) => {
let obj = js_sys::Object::new();
for (key, value) in entries {
let name = data_type_name(key.0)
.map(str::to_string)
.unwrap_or_else(|| key.0.to_string());
set_prop(&obj, &name, &data_value_to_js(value)?)?;
}
Ok(obj.into())
}
DataValue::EncryptedContainer(bytes)
| DataValue::SignedContainer(bytes)
| DataValue::SignedEncryptedContainer(bytes) => {
Ok(js_sys::Uint8Array::from(&bytes[..]).into())
}
DataValue::Null => Ok(JsValue::NULL),
}
}
fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
if value.is_null() || value.is_undefined() {
return Ok(DataValue::Null);
}
if let Some(v) = value.as_bool() {
return Ok(DataValue::Bool(v));
}
if let Some(v) = value.as_string() {
return Ok(DataValue::Str(v));
}
if js_sys::Uint8Array::instanceof(value) {
return Ok(DataValue::Bytes(js_sys::Uint8Array::new(value).to_vec()));
}
if js_sys::Array::is_array(value) {
let array = js_sys::Array::from(value);
let mut values = Vec::with_capacity(array.length() as usize);
for item in array.iter() {
values.push(js_to_data_value(&item)?);
}
return Ok(DataValue::Array(values));
}
if let Some(v) = value.as_f64() {
if v.fract() == 0.0 {
if v >= 0.0 {
return Ok(DataValue::UnsignedNumber(v as u128));
}
return Ok(DataValue::SignedNumber(v as i128));
}
let mantissa = (v * 1_000_000.0).round() as u32;
return Ok(DataValue::Float(246, mantissa));
}
let type_name = value.js_typeof().as_string().unwrap_or_default();
if type_name == "bigint" {
let bigint = value.clone().unchecked_into::<js_sys::BigInt>();
let as_string = bigint
.to_string(10)?
.as_string()
.ok_or_else(|| js_error("failed to stringify bigint"))?;
if let Some(unsigned) = as_string.strip_prefix('-') {
let n = unsigned
.parse::<i128>()
.map_err(|_| js_error("bigint out of range"))?;
return Ok(DataValue::SignedNumber(-n));
}
let n = as_string
.parse::<u128>()
.map_err(|_| js_error("bigint out of range"))?;
return Ok(DataValue::UnsignedNumber(n));
}
if value.is_object() {
let object = js_sys::Object::from(value.clone());
let keys = js_sys::Object::keys(&object);
let mut entries = Vec::with_capacity(keys.length() as usize);
for key in keys.iter() {
let key = key
.as_string()
.ok_or_else(|| js_error("object key must be a string"))?;
let data_type = DataType::from_name(&key)
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
entries.push((
data_type.to_id(&TypeMap::latest()),
js_to_data_value(&value)?,
));
}
return Ok(DataValue::Container(entries));
}
Err(js_error("unsupported data value"))
}
fn option_u32(options: &JsValue, key: &str) -> Result<Option<u32>, JsValue> {
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
if value.is_null() || value.is_undefined() {
return Ok(None);
}
let Some(n) = value.as_f64() else {
return Err(js_error(&format!("{key} must be a number")));
};
Ok(Some(n as u32))
}
fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
if value.is_null() || value.is_undefined() {
return Ok(None);
}
if let Some(n) = value.as_f64() {
return Ok(Some(n as u64));
}
let type_name = value.js_typeof().as_string().unwrap_or_default();
if type_name == "bigint" {
let bigint = value.clone().unchecked_into::<js_sys::BigInt>();
let as_string = bigint
.to_string(10)?
.as_string()
.ok_or_else(|| js_error("failed to stringify bigint"))?;
return as_string
.parse::<u64>()
.map(Some)
.map_err(|_| js_error(&format!("{key} out of range")));
}
Err(js_error(&format!("{key} must be a number or bigint")))
}
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
let obj = js_sys::Object::new();
let data = js_sys::Object::new();
if comm.get_id() != 0 {
set_prop(&obj, "id", &JsValue::from_f64(comm.get_id() as f64))?;
}
let frame_type = communication_type_name(comm.get_type().0)
.map(str::to_string)
.unwrap_or_else(|| comm.get_type().0.to_string());
set_prop(&obj, "type", &JsValue::from_str(&frame_type))?;
if comm.get_sender() != 0 {
set_prop(
&obj,
"sender",
&JsValue::bigint_from_str(&comm.get_sender().to_string()),
)?;
}
if comm.get_receiver() != 0 {
set_prop(
&obj,
"receiver",
&JsValue::bigint_from_str(&comm.get_receiver().to_string()),
)?;
}
for (key, value) in comm.data() {
let name = data_type_name(key.0)
.map(str::to_string)
.unwrap_or_else(|| key.0.to_string());
set_prop(&data, &name, &data_value_to_js(value)?)?;
}
set_prop(&obj, "data", &data.into())?;
set_prop(&obj, "raw", &js_sys::Uint8Array::from(frame).into())?;
Ok(obj.into())
}
/// Build a protocol-level Ping frame with description, timestamp, and optional data.
#[wasm_bindgen]
pub fn build_ping_frame(
client_id: u64,
description: &str,
timestamp: u64,
data: &[u8],
) -> Result<Vec<u8>, JsValue> {
let mut msg = CommunicationValue::new(CommunicationType::Ping)
.add_typed_default(
DataType::Description,
DataValue::Str(description.to_string()),
)
.add_typed_default(
DataType::Timestamp,
DataValue::UnsignedNumber(timestamp as u128),
)
.with_sender(client_id);
if !data.is_empty() {
msg = msg.add_typed_default(DataType::Id, DataValue::Bytes(data.to_vec()));
}
msg.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))
}
/// Parse an auth response frame into a JS object.
#[wasm_bindgen]
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(response)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
let connected = matches!(
comm.get_data(DataType::Connected.to_id(&TypeMap::latest())),
DataValue::BoolTrue
);
let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) {
DataValue::UnsignedNumber(n) => Some(*n),
_ => None,
};
let assigned_id = match comm.get_data(DataType::Id.to_id(&TypeMap::latest())) {
DataValue::UnsignedNumber(n) => Some(*n as u64),
_ => None,
};
let timestamp = match comm.get_data(DataType::Timestamp.to_id(&TypeMap::latest())) {
DataValue::UnsignedNumber(n) => Some(*n),
_ => None,
};
let signature = match comm.get_data(DataType::Signature.to_id(&TypeMap::latest())) {
DataValue::Bytes(b) => Some(b.clone()),
_ => None,
};
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"connected".into(), &JsValue::from(connected)).ok();
if let Some(n) = client_nonce {
let arr = js_sys::Uint8Array::from(&n.to_be_bytes()[..]);
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
}
if let Some(id) = assigned_id {
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
}
if let Some(ts) = timestamp {
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
}
if let Some(sig) = signature {
let arr = js_sys::Uint8Array::from(&sig[..]);
js_sys::Reflect::set(&obj, &"signature".into(), &arr).ok();
}
Ok(obj.into())
}
/// Parse any MTP frame into the human-readable CommunicationValue display form.
#[wasm_bindgen]
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
Ok(comm.to_string())
}
/// Parse any MTP frame into structured JavaScript data.
#[wasm_bindgen(unchecked_return_type = "ParsedFrame")]
pub fn parse_frame(frame: &[u8]) -> Result<JsValue, JsValue> {
parse_frame_value(frame)
}
/// Build a typed MTP frame using generated communication/data type names.
#[wasm_bindgen]
pub fn build_frame(
message_type: &str,
data: JsValue,
options: JsValue,
) -> Result<Vec<u8>, JsValue> {
let comm_type = CommunicationType::from_name(message_type)
.ok_or_else(|| js_error(&format!("unknown communication type: {message_type}")))?;
let mut msg = CommunicationValue::new(comm_type);
if !options.is_null() && !options.is_undefined() {
if let Some(id) = option_u32(&options, "id")? {
msg = msg.with_id(id);
}
if let Some(sender) = option_u64(&options, "sender")? {
msg = msg.with_sender(sender);
}
if let Some(receiver) = option_u64(&options, "receiver")? {
msg = msg.with_receiver(receiver);
}
}
if data.is_object() && !js_sys::Uint8Array::instanceof(&data) && !js_sys::Array::is_array(&data)
{
let object = js_sys::Object::from(data);
let keys = js_sys::Object::keys(&object);
for key in keys.iter() {
let key = key
.as_string()
.ok_or_else(|| js_error("object key must be a string"))?;
let data_type = DataType::from_name(&key)
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
msg = msg.add_data(
data_type.to_id(&TypeMap::latest()),
js_to_data_value(&value)?,
);
}
} else if !data.is_null() && !data.is_undefined() {
return Err(js_error(
"frame data must be an object keyed by MTP data type",
));
}
msg.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))
}
#[cfg(test)]
#[cfg(target_arch = "wasm32")]
mod tests {
use super::*;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn build_ping_frame_roundtrip() {
let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 42);
assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("test-ping".into())
);
assert_eq!(
cv.get_data(DataType::Timestamp.to_id(&tm)),
&DataValue::UnsignedNumber(1234567890)
);
}
#[wasm_bindgen_test]
fn build_ping_frame_with_data() {
let payload = b"attachment-data";
let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 99);
assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("with-data".into())
);
assert_eq!(
cv.get_data(DataType::Timestamp.to_id(&tm)),
&DataValue::UnsignedNumber(555)
);
assert_eq!(
cv.get_data(DataType::Id.to_id(&tm)),
&DataValue::Bytes(payload.to_vec())
);
}
#[wasm_bindgen_test]
fn build_ping_frame_client_id_zero() {
let bytes = build_ping_frame(0, "zero-id", 0, &[]).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
assert_eq!(cv.get_sender(), 0);
}
#[wasm_bindgen_test]
fn parse_auth_response_success() {
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(42))
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345))
.to_bytes()
.expect("encode failed");
let result = parse_auth_response(&resp).expect("parse failed");
let connected = js_sys::Reflect::get(&result, &"connected".into())
.ok()
.and_then(|v| v.as_bool());
assert_eq!(connected, Some(true));
let id = js_sys::Reflect::get(&result, &"assignedId".into())
.ok()
.and_then(|v| v.as_f64());
assert_eq!(id, Some(42.0));
}
#[wasm_bindgen_test]
fn parse_auth_response_rejected() {
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.to_bytes()
.expect("encode failed");
let result = parse_auth_response(&resp).expect("parse failed");
let connected = js_sys::Reflect::get(&result, &"connected".into())
.ok()
.and_then(|v| v.as_bool());
assert_eq!(connected, Some(false));
let has_id = js_sys::Reflect::has(&result, &"assignedId".into()).unwrap_or(false);
assert!(!has_id);
}
#[wasm_bindgen_test]
fn parse_auth_response_with_signature() {
let sig_bytes = vec![0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe];
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::Signature, DataValue::Bytes(sig_bytes.clone()))
.to_bytes()
.expect("encode failed");
let result = parse_auth_response(&resp).expect("parse failed");
let has_sig = js_sys::Reflect::has(&result, &"signature".into()).unwrap_or(false);
assert!(has_sig);
}
#[wasm_bindgen_test]
fn parse_auth_response_invalid_frame() {
let result = parse_auth_response(b"garbage-data");
assert!(result.is_err());
}
}

View file

@ -1,7 +1,10 @@
pub mod client;
pub mod config;
pub mod crypto;
pub mod error;
pub mod message;
pub mod frame;
pub mod logging;
pub mod subscription;
pub mod transport;
#[cfg(not(test))]
@ -11,5 +14,4 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
web_sys::console::log_1(&"mtp-wasm: module loaded".into());
}

10
wasm/src/logging.rs Normal file
View file

@ -0,0 +1,10 @@
use wasm_bindgen::prelude::*;
/// Log severity used by the public SDK when translating raw WASM events.
#[wasm_bindgen]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WasmLogHint {
Info = 0,
Warning = 1,
Error = 2,
}

View file

@ -1,411 +0,0 @@
use wasm_bindgen::prelude::*;
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue};
use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm};
use mtp_type_map::{TypeMap, communication_type_name};
use crate::error::js_error;
/// Build a simple Ping frame with description, timestamp, and optional data.
#[wasm_bindgen]
pub fn build_ping_frame(
client_id: u64,
description: &str,
timestamp: u64,
data: &[u8],
) -> Result<Vec<u8>, JsValue> {
let mut msg = CommunicationValue::new(CommunicationType::Ping)
.add_typed_default(
DataType::Description,
DataValue::Str(description.to_string()),
)
.add_typed_default(
DataType::Timestamp,
DataValue::UnsignedNumber(timestamp as u128),
)
.with_sender(client_id);
if !data.is_empty() {
msg = msg.add_typed_default(DataType::Id, DataValue::Bytes(data.to_vec()));
}
msg.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))
}
/// Build a demo Ping frame with encrypted and signed containers
/// (mirrors the Rust client example but uses only reserved data types).
#[wasm_bindgen]
pub fn build_demo_message(
client_id: u64,
keyring_bytes: &[u8],
host_bundle_bytes: &[u8],
) -> Result<Vec<u8>, JsValue> {
let keyring = Keyring::from_bytes(keyring_bytes)
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
// Encrypt to the server's KEM public key; the server decrypts with its keyring.
// (The client keyring only needs the Ed25519 signing key for this demo.)
let recipient = PublicKeyBundle::from_bytes(host_bundle_bytes)
.map_err(|e| js_error(&format!("invalid host bundle: {}", e)))?;
let enc_type = EncryptionType::MlKemChaCha20Poly1305;
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
// Encrypted container
let inner_enc = DataValue::Container(vec![
(
DataType::Version.to_id(&TypeMap::latest()),
DataValue::Str("secret inner data".into()),
),
(
DataType::Id.to_id(&TypeMap::latest()),
DataValue::UnsignedNumber(42),
),
]);
let mut dv_enc = inner_enc;
dv_enc
.encrypt_container(enc_type, &recipient, b"demo-aad")
.ok_or_else(|| js_error("encryption failed"))?;
// Signed container
let inner_sig = DataValue::Container(vec![
(
DataType::Version.to_id(&TypeMap::latest()),
DataValue::Str("signed by client".into()),
),
(
DataType::Id.to_id(&TypeMap::latest()),
DataValue::UnsignedNumber(99),
),
]);
let mut dv_sig = inner_sig;
dv_sig
.sign_container(SigAlgorithm::ED25519, &signer)
.ok_or_else(|| js_error("signing failed"))?;
// Signed + encrypted container
let inner_sec = DataValue::Container(vec![
(
DataType::Version.to_id(&TypeMap::latest()),
DataValue::Str("signed+encrypted payload".into()),
),
(
DataType::Id.to_id(&TypeMap::latest()),
DataValue::UnsignedNumber(7),
),
]);
let mut dv_sec = inner_sec;
dv_sec
.sign_and_encrypt_container(
SigAlgorithm::ED25519,
&signer,
enc_type,
&recipient,
b"demo-aad",
)
.ok_or_else(|| js_error("sign+encrypt failed"))?;
let timestamp = js_sys::Date::now() as u64;
let msg = CommunicationValue::new(CommunicationType::Ping)
.add_typed_default(
DataType::Description,
DataValue::Str("MTP WASM Demo".into()),
)
.add_typed_default(
DataType::Timestamp,
DataValue::UnsignedNumber(timestamp as u128),
)
.add_typed_default(DataType::Version, DataValue::Str("demo-wasm".into()))
.with_sender(client_id);
msg.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))
}
/// Parse an auth response frame into a JS object.
#[wasm_bindgen]
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(response)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
let connected = matches!(
comm.get_data(DataType::Connected.to_id(&TypeMap::latest())),
DataValue::BoolTrue
);
let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) {
DataValue::UnsignedNumber(n) => Some(*n),
_ => None,
};
let assigned_id = match comm.get_data(DataType::Id.to_id(&TypeMap::latest())) {
DataValue::UnsignedNumber(n) => Some(*n as u64),
_ => None,
};
let timestamp = match comm.get_data(DataType::Timestamp.to_id(&TypeMap::latest())) {
DataValue::UnsignedNumber(n) => Some(*n),
_ => None,
};
let signature = match comm.get_data(DataType::Signature.to_id(&TypeMap::latest())) {
DataValue::Bytes(b) => Some(b.clone()),
_ => None,
};
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"connected".into(), &JsValue::from(connected)).ok();
if let Some(n) = client_nonce {
let arr = js_sys::Uint8Array::from(&n.to_be_bytes()[..]);
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
}
if let Some(id) = assigned_id {
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
}
if let Some(ts) = timestamp {
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
}
if let Some(sig) = signature {
let arr = js_sys::Uint8Array::from(&sig[..]);
js_sys::Reflect::set(&obj, &"signature".into(), &arr).ok();
}
Ok(obj.into())
}
/// Build a request frame with the given communication type name, request ID, and JSON data.
///
/// - `comm_type`: communication type name (e.g. "get_model", "rank_models") or PascalCase
/// - `id`: request ID for response correlation
/// - `json_data`: JSON-stringified request payload
#[wasm_bindgen]
pub fn build_request_frame(comm_type: &str, id: u32, json_data: &str) -> Result<Vec<u8>, JsValue> {
let comm_type_enum = CommunicationType::from_name(comm_type)
.or_else(|| {
let pascal = comm_type
.split('_')
.map(|s| {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<String>();
CommunicationType::from_name(&pascal)
})
.ok_or_else(|| js_error(&format!("unknown communication type: {}", comm_type)))?;
let frame = CommunicationValue::new(comm_type_enum)
.with_id(id)
.add_data(DataTypeId(32), DataValue::Str(json_data.to_string()))
.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
Ok(frame)
}
/// Parse a response frame into a JSON string containing `_id`, `_type`, and data fields.
#[wasm_bindgen]
pub fn parse_response_frame(frame: &[u8]) -> Result<String, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
let obj = js_sys::Object::new();
let _ = js_sys::Reflect::set(
&obj,
&JsValue::from_str("_id"),
&JsValue::from(comm.get_id()),
);
let type_name = communication_type_name(comm.get_type().0).unwrap_or("Unknown");
let _ = js_sys::Reflect::set(
&obj,
&JsValue::from_str("_type"),
&JsValue::from_str(&type_name),
);
if let DataValue::Str(s) = comm.get_data(DataTypeId(32)) {
if let Ok(parsed) = js_sys::JSON::parse(s) {
let parsed_obj: &js_sys::Object = parsed.unchecked_ref();
let entries = js_sys::Object::entries(parsed_obj);
let len = entries.length();
for i in 0..len {
let entry = js_sys::Array::get(&entries, i);
if let Some(entry_arr) = entry.dyn_ref::<js_sys::Array>() {
if let Some(key) = entry_arr.get(0).as_string() {
let val = entry_arr.get(1);
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str(&key), &val);
}
}
}
}
}
let stringified =
js_sys::JSON::stringify(&obj).map_err(|_| js_error("JSON stringify failed"))?;
stringified
.as_string()
.ok_or_else(|| js_error("JSON stringify result not a string"))
}
/// Parse any MTP frame into the human-readable CommunicationValue display form.
#[wasm_bindgen]
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
Ok(comm.to_string())
}
#[cfg(test)]
#[cfg(target_arch = "wasm32")]
mod tests {
use super::*;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn build_ping_frame_roundtrip() {
let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 42);
assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("test-ping".into())
);
assert_eq!(
cv.get_data(DataType::Timestamp.to_id(&tm)),
&DataValue::UnsignedNumber(1234567890)
);
}
#[wasm_bindgen_test]
fn build_ping_frame_with_data() {
let payload = b"attachment-data";
let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 99);
assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("with-data".into())
);
assert_eq!(
cv.get_data(DataType::Timestamp.to_id(&tm)),
&DataValue::UnsignedNumber(555)
);
assert_eq!(
cv.get_data(DataType::Id.to_id(&tm)),
&DataValue::Bytes(payload.to_vec())
);
}
#[wasm_bindgen_test]
fn build_ping_frame_client_id_zero() {
let bytes = build_ping_frame(0, "zero-id", 0, &[]).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
assert_eq!(cv.get_sender(), 0);
}
#[wasm_bindgen_test]
fn build_demo_message_roundtrip() {
// The demo KEM-encrypts to the host's bundle, so a real host keypair is
// required; the client keyring only needs its Ed25519 signing key.
let keyring = Keyring::generate();
let keyring_bytes = keyring.to_bytes();
let host_bundle = Keyring::generate().public_key_bundle().as_bytes();
let result = build_demo_message(7, &keyring_bytes, &host_bundle);
assert!(result.is_ok());
let bytes = result.unwrap();
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(cv.get_sender(), 7);
assert_eq!(
cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("MTP WASM Demo".into())
);
}
#[wasm_bindgen_test]
fn build_demo_message_invalid_keyring() {
let host_bundle = Keyring::generate().public_key_bundle().as_bytes();
let result = build_demo_message(1, b"not-a-valid-keyring", &host_bundle);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.as_string().unwrap().contains("invalid keyring"));
}
#[wasm_bindgen_test]
fn parse_auth_response_success() {
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(42))
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345))
.to_bytes()
.expect("encode failed");
let result = parse_auth_response(&resp).expect("parse failed");
let connected = js_sys::Reflect::get(&result, &"connected".into())
.ok()
.and_then(|v| v.as_bool());
assert_eq!(connected, Some(true));
let id = js_sys::Reflect::get(&result, &"assignedId".into())
.ok()
.and_then(|v| v.as_f64());
assert_eq!(id, Some(42.0));
}
#[wasm_bindgen_test]
fn parse_auth_response_rejected() {
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.to_bytes()
.expect("encode failed");
let result = parse_auth_response(&resp).expect("parse failed");
let connected = js_sys::Reflect::get(&result, &"connected".into())
.ok()
.and_then(|v| v.as_bool());
assert_eq!(connected, Some(false));
// rejected should have no assignedId
let has_id = js_sys::Reflect::has(&result, &"assignedId".into()).unwrap_or(false);
assert!(!has_id);
}
#[wasm_bindgen_test]
fn parse_auth_response_with_signature() {
let sig_bytes = vec![0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe];
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::Signature, DataValue::Bytes(sig_bytes.clone()))
.to_bytes()
.expect("encode failed");
let result = parse_auth_response(&resp).expect("parse failed");
let has_sig = js_sys::Reflect::has(&result, &"signature".into()).unwrap_or(false);
assert!(has_sig);
}
#[wasm_bindgen_test]
fn parse_auth_response_invalid_frame() {
let result = parse_auth_response(b"garbage-data");
assert!(result.is_err());
}
}

44
wasm/src/subscription.rs Normal file
View file

@ -0,0 +1,44 @@
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
/// Minimal message router used by higher-level SDK subscription code.
#[wasm_bindgen]
pub struct WasmSubscriptionRouter {
handlers: HashMap<String, js_sys::Function>,
}
#[wasm_bindgen]
impl WasmSubscriptionRouter {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
#[wasm_bindgen]
pub fn subscribe(&mut self, message_type: String, callback: js_sys::Function) {
self.handlers.insert(message_type, callback);
}
#[wasm_bindgen]
pub fn unsubscribe(&mut self, message_type: &str) -> bool {
self.handlers.remove(message_type).is_some()
}
#[wasm_bindgen]
pub fn dispatch(&self, message_type: &str, message: JsValue) -> bool {
let Some(callback) = self.handlers.get(message_type) else {
return false;
};
let _ = callback.call1(&JsValue::NULL, &message);
true
}
}
impl Default for WasmSubscriptionRouter {
fn default() -> Self {
Self::new()
}
}

View file

@ -4,9 +4,9 @@ use std::rc::Rc;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{WebTransport, WebTransportHash, WebTransportOptions};
use crate::error::js_error;
use crate::frame::parse_frame_value;
const CLOSE_FRAME_LEN: u32 = u32::MAX;
@ -40,42 +40,6 @@ enum FrameOutcome {
Ended,
}
/*
* Debug-log the exact bytes about to be written to the WebTransport stream.
*
* Wire layout (note the DOUBLE length prefix):
* [0..4] outer_len u32 BE - added by send_frame (= inner frame length)
* [4..8] inner_len u32 BE - added by CommunicationValue::to_bytes
* [8..10] comm_type u16 BE - e.g. Identification
* [10] flags u8
* [11..] id/sender/receiver/signature/data, gated by `flags`
*/
fn log_frame_bytes(wire: &[u8]) {
let hex: String = wire
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ");
let outer_len = wire
.get(0..4)
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]));
let inner_len = wire
.get(4..8)
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]));
let comm_type = wire.get(8..10).map(|b| u16::from_be_bytes([b[0], b[1]]));
let flags = wire.get(10).copied();
web_sys::console::log_1(
&format!(
"mtp-wasm send_frame: {len} bytes | outer_len={outer_len:?} inner_len={inner_len:?} \
comm_type={comm_type:?} flags={flags:?}\n{hex}",
len = wire.len(),
)
.into(),
);
}
/*
* WebTransport client transport.
*
@ -90,7 +54,7 @@ fn log_frame_bytes(wire: &[u8]) {
*/
#[derive(Clone)]
pub struct WasmTransport {
inner: WebTransport,
inner: JsValue,
/// Reader over `incoming_unidirectional_streams()` (a singleton stream of streams).
streams_reader: Rc<RefCell<Option<JsValue>>>,
/// Reader over the host's current uni-directional stream, if one is open.
@ -101,28 +65,48 @@ pub struct WasmTransport {
impl WasmTransport {
pub async fn connect(url: &str, cert_hashes: Option<Vec<String>>) -> Result<Self, JsValue> {
let transport = match cert_hashes {
Some(hashes) => {
let opts = WebTransportOptions::new();
let mut wt_hashes = Vec::new();
for h in hashes {
if let Some((algo, hex_val)) = h.split_once(':') {
if let Ok(bytes) = hex::decode(hex_val) {
let hash = WebTransportHash::new();
hash.set_algorithm(algo);
hash.set_value_u8_array(&js_sys::Uint8Array::from(&bytes[..]));
wt_hashes.push(hash);
}
let ctor = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("WebTransport"))?
.dyn_into::<js_sys::Function>()
.map_err(|_| js_error("WebTransport not available"))?;
let args = js_sys::Array::new();
args.push(&JsValue::from_str(url));
if let Some(hashes) = cert_hashes {
let wt_hashes = js_sys::Array::new();
for h in hashes {
if let Some((algo, hex_val)) = h.split_once(':') {
if let Ok(bytes) = hex::decode(hex_val) {
let hash = js_sys::Object::new();
js_sys::Reflect::set(
&hash,
&JsValue::from_str("algorithm"),
&JsValue::from_str(algo),
)?;
js_sys::Reflect::set(
&hash,
&JsValue::from_str("value"),
&js_sys::Uint8Array::from(&bytes[..]),
)?;
wt_hashes.push(&hash);
}
}
if !wt_hashes.is_empty() {
opts.set_server_certificate_hashes(&wt_hashes);
}
WebTransport::new_with_options(url, &opts)?
}
None => WebTransport::new(url)?,
if wt_hashes.length() > 0 {
let opts = js_sys::Object::new();
js_sys::Reflect::set(
&opts,
&JsValue::from_str("serverCertificateHashes"),
&wt_hashes,
)?;
args.push(&opts);
}
};
JsFuture::from(transport.ready())
let transport = js_sys::Reflect::construct(&ctor, &args)?;
let ready = js_sys::Reflect::get(&transport, &JsValue::from_str("ready"))?
.dyn_into::<js_sys::Promise>()
.map_err(|_| js_error("WebTransport.ready is not a Promise"))?;
JsFuture::from(ready)
.await
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
Ok(Self {
@ -133,12 +117,21 @@ impl WasmTransport {
})
}
pub fn inner(&self) -> &WebTransport {
pub fn inner(&self) -> &JsValue {
&self.inner
}
pub async fn send_frame(&self, frame: &[u8]) -> Result<(), JsValue> {
let stream_promise = self.inner.create_unidirectional_stream();
let create_stream = js_sys::Reflect::get(
&self.inner,
&JsValue::from_str("createUnidirectionalStream"),
)?
.dyn_into::<js_sys::Function>()
.map_err(|_| js_error("createUnidirectionalStream not a function"))?;
let stream_promise = create_stream
.call0(&self.inner)?
.dyn_into::<js_sys::Promise>()
.map_err(|_| js_error("createUnidirectionalStream did not return a Promise"))?;
let stream = JsFuture::from(stream_promise).await?;
let writable_or_stream = resolve_stream_writable(&stream)?;
@ -155,8 +148,6 @@ impl WasmTransport {
wire.extend_from_slice(&len.to_be_bytes());
wire.extend_from_slice(frame);
log_frame_bytes(&wire);
let chunk = js_sys::Uint8Array::from(&wire[..]);
let write_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("write"))
@ -185,7 +176,10 @@ impl WasmTransport {
if let Some(reader) = self.streams_reader.borrow().clone() {
return Ok(reader);
}
let incoming = self.inner.incoming_unidirectional_streams();
let incoming = js_sys::Reflect::get(
&self.inner,
&JsValue::from_str("incomingUnidirectionalStreams"),
)?;
let reader = js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader"))
.map_err(|_| js_error("missing getReader"))?
.dyn_into::<js_sys::Function>()
@ -341,10 +335,15 @@ impl WasmTransport {
pub async fn receive_loop(&self, on_message: js_sys::Function, on_error: js_sys::Function) {
loop {
match self.next_frame().await {
Ok(FrameOutcome::Frame(frame)) => {
let arr = js_sys::Uint8Array::from(&frame[..]);
let _ = on_message.call1(&JsValue::NULL, &arr);
}
Ok(FrameOutcome::Frame(frame)) => match parse_frame_value(&frame) {
Ok(parsed) => {
let _ = on_message.call1(&JsValue::NULL, &parsed);
}
Err(e) => {
let message = e.as_string().unwrap_or_else(|| format!("{:?}", e));
let _ = on_error.call1(&JsValue::NULL, &JsValue::from_str(&message));
}
},
Ok(FrameOutcome::Closed) | Ok(FrameOutcome::Ended) => break,
Err(e) => {
let _ = on_error.call1(&JsValue::NULL, &e);
@ -355,7 +354,10 @@ impl WasmTransport {
}
pub fn close(&self) {
let info = web_sys::WebTransportCloseInfo::new();
let _ = self.inner.close_with_close_info(&info);
if let Ok(close) = js_sys::Reflect::get(&self.inner, &JsValue::from_str("close"))
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
{
let _ = close.call1(&self.inner, &js_sys::Object::new());
}
}
}