draw-collab-wasm#
Browser (WASM) iroh + iroh-gossip transport for Draw live collaboration. This crate is transport only: it moves opaque byte payloads between browsers over an end-to-end encrypted gossip topic and surfaces presence events. All Draw protocol logic (message subtypes, reconciliation, chunking, file sync) lives in TypeScript.
Browser iroh is relay-only (no UDP in the sandbox): traffic transits n0's public relays, but QUIC connections stay end-to-end encrypted — relays only see ciphertext. The random gossip TopicId inside the ticket is the room capability; it lives only in the URL fragment.
Prerequisites#
-
Rust
stablewith thewasm32-unknown-unknowntarget (seerust-toolchain.toml). -
wasm-pack:cargo install wasm-pack. -
A wasm-capable
clangfor buildingring(the TLS backend). Apple's systemclangdoes not include the WebAssembly target. On macOS:brew install llvm, then point the C compiler at it:export CC_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/clang" export AR_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/llvm-ar"
Build#
From the repo root:
yarn build:wasm
# == wasm-pack build crates/draw-collab-wasm --target web --release
This emits pkg/ containing the JS glue (with an explicit default init()), the .wasm binary, and .d.ts typings. pkg/ is committed so contributors and CI do not need a Rust toolchain. wasm-pack writes a pkg/.gitignore containing *; empty or delete it after building so the artifacts are tracked.
Consuming from the app#
The app aliases the bare specifier draw-collab-wasm to this crate's pkg/ directory (see excalidraw-app/vite.config.mts and the paths entry in the root tsconfig.json). Load it lazily and call init() before use:
import init, { CollabNode } from "draw-collab-wasm";
await init();
const node = await CollabNode.spawn();
API surface#
CollabNode.spawn(): Promise<CollabNode>— bind an endpoint + gossip router.node.node_id(): string— this node's endpoint id.node.create_ticket(): string— mint a fresh room ticket (random topic, self as bootstrap).node.join_room(ticket, wait_joined, on_message, on_neighbor_up, on_neighbor_down): Promise<RoomHandle>—wait_joined = falsefor the room creator,truefor joiners.node.shutdown(): Promise<void>.room.broadcast(payload: Uint8Array): Promise<void>— payloads over 256KB must be chunked by the caller (gossipmax_message_sizeis 256KB).room.ticket(): string— re-mint with self as bootstrap so re-copied links stay fresh.room.close(): void.