A WIT interface and multiple implementations showing that high-performance
WebRTC data-channel communication can be expressed with the WebAssembly
Component Model's async features (stream, future, async imports/exports),
with a single guest component binary running unchanged against very
different stacks:
- a browser-first host (Node.js +
jco+node-datachannel), - a native Rust host (Wasmtime +
webrtc-rs), and - an in-guest component (
wasip3-impl) that runs the whole sans-I/O WebRTC stack inside wasm overwasi:sockets.
All of them run the identical component and round-trip every message through a genuine WebRTC/SCTP data channel; the conformance suite asserts they behave compatibly.
| Path | Deliverable |
|---|---|
wit/ |
The streaming WIT interface, the polymorph:webrtc-datachannels@0.1.0 package. Each demo component keeps its own demo-only WIT and symlinks this package in as a dependency. |
examples/echo-demo |
A Rust example component exercising a data channel one message at a time. |
wasmtime-impl |
The Wasmtime host crate (webrtc-rs), modeled after wasmtime_wasi_http::p3. Provides add_to_linker + WebrtcView for the types interface and the data-channel resource of connections (the peer-connection resource is unimplemented). Crate name: wasmtime-webrtc-datachannels. |
jco-impl |
The browser-first host (Node stand-in for the browser, jco + node-datachannel). |
examples/wasmtime-demo |
The native Rust host (Wasmtime + webrtc-rs): demo binaries built on wasmtime-impl. |
examples/cli-signaling |
The manual-signaling CLI guest component (Rust), driving connections.peer-connection with guest-side vanilla ICE. |
examples/webrtc-consumer |
A minimal consumer component that imports connections. Composed (wac plug) with wasip3-impl for the in-guest round-trip integration test (just examples::test-webrtc-composed). |
wasip3-impl |
The third implementation: a wasm component (built for wasm32-wasip2) that runs the sans-I/O rtc 0.21 WebRTC stack in-guest — importing only wasi:sockets/wasi:clocks — and exports polymorph:webrtc-datachannels/connections. Its SansIoPeer core is driven over wasi:sockets UDP and WASI timers by an in-guest runtime pump. Composable via wac plug. Crate name: wasip3-webrtc-datachannels. |
conformance/ |
The cross-implementation conformance suite, on the polymorph:test harness: two suite components (full and pair-only) run against every target (wasmtime, the composed wasip3 stack, jco under Node and headless Chrome), an interop matrix pairing every implementation with a non-wasm reference peer (Google's libwebrtc via LiveKit's Rust bindings), a netns lab, and a Shadow lab. just conformance; see conformance/README.md. |
AGENTS.md |
Orientation for agents/contributors, linking the lann/wasm-component-starter knowledge base. |
The interface lives at the root wit/ as the
polymorph:webrtc-datachannels package. Each demo component keeps its own demo-only
WIT alongside it and pulls the package in as a deps symlink, so there is still
a single copy of the shared surface to edit:
polymorph:webrtc-datachannels — the shared interfaces:
types— every structural (non-resource) type in the package: theerrorvariant, themessage/message-kind/stream-message/send-via-stream-errordata-channel types, and thesdp-type/session-description/ice-candidatesignaling types. Structural types carry no host identity, so a single composition can share them across components.connections— the stateful WebRTC resources, which (unlike the structuraltypes) are each owned by the one component that implements them:-
data-channel-options— a configuration builder for a data channel (a subset ofRTCDataChannelInit:label,ordered,max-retransmits), shaped afterwasi:http'srequest-options: construct a default value, adjust fields through the setters, then hand it to a data-channel-creating function such aspeer-connection.create-data-channel. -
data-channel— the high-throughput surface. Adata-channelis bidirectional and message-oriented; each call carries exactly one data-channel message, preserving WebRTC message boundaries:send: async func(message: message) -> result<_, error>receive: async func() -> result<message, error>
A
messageis a variant —binary(list<u8>)or%string(string)(text, valid UTF-8). Concurrent calls are supported so the host and guest can pipeline messages and let the async ABI apply backpressure. To bound in-memory buffering, a message may instead flow through a bytestreamas astream-message(kind,length,data: stream<u8>):send-via-stream: async func(messages: stream<stream-message>) -> result<_, send-via-stream-error>receive-via-stream: func() -> result<stream<stream-message>, error>
send-via-stream-errorcarries the underlyingerrorplussent, the number of messages handed to the transport before the failure.receive-via-streamtakes over the channel's inbound messages: it may be called only once, after which it (andreceive) fail witherror.receiving-via-stream. -
peer-connection— a fullerRTCPeerConnection-style surface (SDP offer/answer + trickle ICE) that documents where a guest-driven connection API is headed. It is the design target and is not required by the runnable demo.
-
demo:webrtc-echo — the demo-only interfaces, which live with the echo
demo that uses them (examples/echo-demo/wit; the
manual-signaling CLI demo in
examples/cli-signaling imports only the standard
connections interface and handles vanilla ICE guest-side):
rendezvous— a proposed, deliberately unstandardized HTTP signaling mailbox for carrying SDP/ICE between two separate peers via an existing server overwasi:http@0.3, so remote connections can be developed locally. Like theconnections.peer-connectionresource, it is designed but not yet wired into the runnable demo (seeAGENTS.md).demo— the exported entry point (run) the hosts call.
The demo world is intentionally tiny:
world webrtc-echo-demo {
import polymorph:webrtc-datachannels/connections@0.1.0;
export demo;
}examples/echo-demo is host-agnostic Rust
(wit-bindgen, wasm32-unknown-unknown + wasm-tools component new). Its
run:
- stands up two peer connections in-component through the standard
connectionsinterface (a real SDP offer/answer exchange plus trickled ICE) and adopts the negotiated channel on both ends, - sends
message-countmessages one at a time throughdata-channel.sendon one end, echoes each back from the other, and concurrently reads them back one at a time fromdata-channel.receive(all loops underfutures::join!), - returns counts so the host can assert a complete round trip.
Prerequisites: Rust (with the wasm32-unknown-unknown target),
wasm-tools, Node 24+ (for the Node host). The Node host needs JSPI, which
Node exposes on 24+ behind --experimental-wasm-jspi.
cd jco-impl
npm install
just examples::build-component # build the guest .wasm component (cargo + wasm-tools)
npm run transpile # jco transpile with JSPI async + host maps
npm startThe host builds two RTCPeerConnections with node-datachannel, performs a real
SDP/ICE handshake, and echoes on the far side.
The same webrtc.js host module runs unchanged in a real browser: it resolves
RTCPeerConnection from the browser global when present and falls back to
node-datachannel under Node. A headless-Chrome test drives the identical
transpiled component through that browser path and is what runs in CI:
cd jco-impl
just examples::build-component && npm run transpile
npm run test:browser # headless Chrome (137+); set CHROME_PATH to overridecd examples/wasmtime-demo
cargo run --release --bin wasmtime-webrtc-host -- ../echo-demo/build/echo-demo.component.wasm 1000 4096
# ^msg count ^msg size(Run just examples::build-component once first, or build the component
manually, to produce the .wasm.)