A WIT interface for WebSocket client connections, plus multiple
implementations that run the same guest component against real WebSocket
stacks — a sibling of
polymorph:webcrypto and
polymorph:webrtc-datachannels,
deliberately mirroring their architecture.
One guest component binary runs unchanged against:
- a browser-first host (
js/jco): the standardWebSocketAPI only, zero dependencies — the same file loads in a browser and under Node (24+ for JSPI); - a native Rust host (
rust/wasmtime): Wasmtime +tokio-tungstenite, modeled afterwasmtime_wasi_http::p3.
The conformance suite runs the shared guest's corpus against every implementation — including inside a real headless Chromium — and holds them to one behavior. Open questions and findings are tracked in the issues.
WebAssembly components have no standard WebSocket path today: wasi:http
carries request/response and has no upgrade mechanism, and wasi:sockets
gives raw TCP with no TLS, HTTP handshake, or framing. Meanwhile the
browser — the platform this family of packages treats as a first-class
deployment target — offers only WebSocket (and WebRTC) for long-lived
duplex connections.
The immediate consumer is
component-iroh: an iroh
endpoint keeps a persistent secure WebSocket open to its home relay, and
publishes signed discovery records over HTTPS. The relay leg is exactly
the capability gap this package fills. But the interface is
general-purpose: message-oriented duplex connectivity that behaves
identically in a browser, on a native host, and (where feasible) fully
in-guest.
| Path | Deliverable |
|---|---|
wit/ |
The polymorph:websocket package: a types interface for structural types and a connections interface owning the websocket resource. One copy at the root; consumers pull it in via wit/deps symlinks. Package-wide contracts live in wit/README.md. |
js/jco |
The browser-first host library (websocket.js): the standard WebSocket API only, no node: modules, no runtime dependencies. Shared verbatim by the demo runners and the conformance jco legs. |
rust/wasmtime |
The Wasmtime host crate (wasmtime-websocket): add_to_linker + WasiWebsocketView, per-store WasiWebsocketCtx knobs for the bounds the WIT leaves implementation-defined. |
js/componentize |
The browser-API shim for componentize-js guests: the WHATWG WebSocket interface implemented over the WIT imports — the inverse of js/jco — plus the WPT parity gate: vendored web-platform-tests run round-trip (shim → WIT → jco → platform WebSocket) and held to the platform baseline's pass set. |
rust/guest-provider |
The in-guest provider (websocket-guest-provider): a WebSocket client stack over wasi:sockets TCP, exporting the package surface as a composable component; wss: via the composed polymorph:tls component. See its README for the TLS posture and configuration channel. |
conformance/ |
The cross-implementation conformance suite on the polymorph:test harness: a shared guest suite (guest-ct, its committed tests.lock the corpus inventory), a suite-owned echo server with fault-injection modes, and the driver (driver-ct) that runs every target (wasmtime, jco under Node, jco under headless Chromium) into the matrix. |
examples/ |
The echo-demo guest component plus native (just demo::wasmtime) and Node (just demo::node) runners against the suite echo server. |
The package defines two interfaces (see wit/websocket.wit):
types— theerrorvariant, themessagevariant (binary(list<u8>)/%string(string)), the stream-backedstream-messageform,close-info, and the lifecycle enum.connections— thewebsocketresource:connect: static async func(url, protocols)resolves once the connection is open (so there is no observableconnectingstate);send/receivecarry exactly one message per call, preserving WebSocket message boundaries, with concurrent calls supported for pipelining;send-via-stream/receive-via-streamcarry each message's payload as a bytestreamto bound in-guest buffering;stateis the lifecycle getter (mirroring the poll-only W3CreadyState);wait-closedis the latched authority for close details (the peer's close frame, ornonefor an abnormal closure);close(code, reason)validates eagerly against the codes a browser client may send and initiates the closing handshake.
The browser WebSocket API is the least capable implementation and bounds
the portable surface: no request headers, client certificates, proxy
control, trust decisions, or ping/pong access appear here. The
close-semantics contract — what receive and wait-closed observe around
local, remote-clean, and abnormal closes — is pinned in
wit/README.md ("Close contract") and exercised by
conformance rows against the suite server's fault modes.
A shared shape with the webrtc sibling's data-channel is deliberate:
component-iroh wants to treat a relay WebSocket and a WebRTC data
channel as interchangeable message transports.
Prerequisites: rustup, Node 24+ (the jco runners need JSPI), a
Chrome/Chromium 137+ for the browser conformance target (discovered from
the usual locations, or set CHROME_PATH), and ./scripts/setup.sh
(installs the pinned wasm-tools/wac/just/pnpm and the JS package
trees).
./scripts/setup.sh
just check # fmt + clippy + WIT validation + native tests
just conformance-ct # the full matrix: wasmtime, composed, jco-node, jco-browser
just demo::wasmtime # the echo demo on the native host
just demo::node # the same component under Node + jco
just ci # exactly what CI runsIf a standard wasi:websocket (or an upgrade path in a future wasi:http)
materializes, this package's job becomes migration, not competition — the
same posture the siblings take toward their domains. The interface stays
small enough that mapping onto a standard surface is mechanical.