wasm: wasm-bindgen surface — WasmEngine + roomForAccount for the browser
src/wasm.rs exposes the engine to JS (gated to wasm32 + iroh-transport): a WasmEngine wrapping Arc<Engine> with new(server, room, from, account) / start( on_message) / send / stop / irohId, plus roomForAccount so the browser derives the same room as native clients. Inbound clipboards bridge to a JS callback via a JsHandler. MessageHandler's Send+Sync bound is relaxed on wasm (single JS thread; a js_sys::Function isn't Send and the engine spawns via spawn_local where it isn't needed). Adds wasm-bindgen + js-sys (wasm-only deps). Native (UniFFI) + wasm both compile clean. Next: generate the JS bindings with wasm-bindgen-cli and a browser smoke test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+2
@@ -4857,6 +4857,7 @@ dependencies = [
|
|||||||
"iroh",
|
"iroh",
|
||||||
"iroh-blobs",
|
"iroh-blobs",
|
||||||
"iroh-gossip",
|
"iroh-gossip",
|
||||||
|
"js-sys",
|
||||||
"mdns-sd",
|
"mdns-sd",
|
||||||
"n0-future",
|
"n0-future",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
@@ -4865,6 +4866,7 @@ dependencies = [
|
|||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
|||||||
getrandom = "0.4.3"
|
getrandom = "0.4.3"
|
||||||
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
# Browser iroh: default-features off (drops native portmapper/apple-datapath),
|
# Browser iroh: default-features off (drops native portmapper/apple-datapath),
|
||||||
# but keep tls-ring — ring works on wasm and presets::N0 needs it. (Matches n0's
|
# but keep tls-ring — ring works on wasm and presets::N0 needs it. (Matches n0's
|
||||||
# own browser-chat example.)
|
# own browser-chat example.)
|
||||||
|
|||||||
+14
-1
@@ -67,6 +67,9 @@ mod iroh;
|
|||||||
mod lan;
|
mod lan;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod rtc;
|
mod rtc;
|
||||||
|
// The browser's wasm-bindgen surface (iroh-only build).
|
||||||
|
#[cfg(all(target_arch = "wasm32", feature = "iroh-transport"))]
|
||||||
|
mod wasm;
|
||||||
|
|
||||||
use crypto::Crypto;
|
use crypto::Crypto;
|
||||||
|
|
||||||
@@ -101,13 +104,23 @@ pub struct Message {
|
|||||||
/// Implemented on the foreign side (Swift/Kotlin) or natively (desktop agent).
|
/// Implemented on the foreign side (Swift/Kotlin) or natively (desktop agent).
|
||||||
/// Callbacks fire on the engine's runtime thread — marshal to the UI thread
|
/// Callbacks fire on the engine's runtime thread — marshal to the UI thread
|
||||||
/// on the consumer side.
|
/// on the consumer side.
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), uniffi::export(callback_interface))]
|
// Native: Send + Sync (engine runs on a tokio thread pool) + a UniFFI callback.
|
||||||
|
// wasm: single JS thread, so no Send/Sync — a JS callback (js_sys::Function)
|
||||||
|
// isn't Send, and the engine spawns via spawn_local where it isn't needed.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[uniffi::export(callback_interface)]
|
||||||
pub trait MessageHandler: Send + Sync {
|
pub trait MessageHandler: Send + Sync {
|
||||||
fn on_message(&self, msg: Message);
|
fn on_message(&self, msg: Message);
|
||||||
fn on_status(&self, connected: bool);
|
fn on_status(&self, connected: bool);
|
||||||
/// A file/image received P2P (RTC data channel), reassembled from chunks.
|
/// A file/image received P2P (RTC data channel), reassembled from chunks.
|
||||||
fn on_file(&self, name: String, mime: String, data: Vec<u8>);
|
fn on_file(&self, name: String, mime: String, data: Vec<u8>);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub trait MessageHandler {
|
||||||
|
fn on_message(&self, msg: Message);
|
||||||
|
fn on_status(&self, connected: bool);
|
||||||
|
fn on_file(&self, name: String, mime: String, data: Vec<u8>);
|
||||||
|
}
|
||||||
|
|
||||||
/// A tether device discovered on the local network (mDNS). Surfaced to the UI
|
/// A tether device discovered on the local network (mDNS). Surfaced to the UI
|
||||||
/// for an AirDrop-style "nearby devices" list. `room` lets the UI offer to pair
|
/// for an AirDrop-style "nearby devices" list. `room` lets the UI offer to pair
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//! wasm-bindgen surface — the browser's entry point to tethercore.
|
||||||
|
//!
|
||||||
|
//! On native the engine is exposed via UniFFI (Swift/Kotlin); in the browser it's
|
||||||
|
//! the same `Engine`, wrapped for JS here. A browser peer is relay-only (no
|
||||||
|
//! holepunch in the sandbox), but otherwise identical: same gossip topic, same
|
||||||
|
//! E2E, same rendezvous bootstrap.
|
||||||
|
//!
|
||||||
|
//! JS usage:
|
||||||
|
//! import init, { WasmEngine, roomForAccount } from "./tethercore.js";
|
||||||
|
//! await init();
|
||||||
|
//! const room = roomForAccount("you@example.com");
|
||||||
|
//! const e = new WasmEngine("https://rendezvous.example", room, deviceId, "you@example.com");
|
||||||
|
//! e.start((text) => { /* inbound clipboard */ });
|
||||||
|
//! e.send("hello from the browser");
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::{Engine, Message, MessageHandler};
|
||||||
|
|
||||||
|
/// The clipboard engine, for the browser.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct WasmEngine {
|
||||||
|
inner: Arc<Engine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl WasmEngine {
|
||||||
|
/// `server` = rendezvous URL, `room` = account-derived room (see
|
||||||
|
/// `roomForAccount`), `from` = a stable device id, `account` = the shared
|
||||||
|
/// identity/secret (same on all your devices).
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(server: String, room: String, from: String, account: String) -> WasmEngine {
|
||||||
|
let inner = Engine::new(server, room, from, "web".into(), "Web".into(), account);
|
||||||
|
WasmEngine { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start syncing. `on_message` is invoked with each inbound clipboard's text.
|
||||||
|
pub fn start(&self, on_message: js_sys::Function) {
|
||||||
|
self.inner.start(Box::new(JsHandler { on_message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish clipboard text to the room.
|
||||||
|
pub fn send(&self, text: String) {
|
||||||
|
self.inner.send(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop syncing.
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.inner.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This node's iroh EndpointId once online (for debugging), else `null`.
|
||||||
|
#[wasm_bindgen(js_name = irohId)]
|
||||||
|
pub fn iroh_id(&self) -> Option<String> {
|
||||||
|
self.inner.iroh_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `account` → account-derived room id. Re-exported so the browser derives the
|
||||||
|
/// SAME room as the native clients.
|
||||||
|
#[wasm_bindgen(js_name = roomForAccount)]
|
||||||
|
pub fn room_for_account(account: String) -> String {
|
||||||
|
crate::room_for_account(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridges inbound clipboards to a JS callback. Single-threaded (the engine runs
|
||||||
|
/// on the JS event loop), so the `js_sys::Function` never crosses a thread.
|
||||||
|
struct JsHandler {
|
||||||
|
on_message: js_sys::Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageHandler for JsHandler {
|
||||||
|
fn on_message(&self, m: Message) {
|
||||||
|
let _ = self.on_message.call1(&JsValue::NULL, &JsValue::from_str(&m.text));
|
||||||
|
}
|
||||||
|
fn on_status(&self, _connected: bool) {}
|
||||||
|
fn on_file(&self, _name: String, _mime: String, _data: Vec<u8>) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user