A WIT interface for WebSocket client connections. This document holds the package-wide contracts; item doc comments state what is specific to their item and reference these sections by name.
types— every structural (non-resource) type: theerrorvariant, the message types,close-info, and the lifecycle enum. Structural types carry no host-side identity, so a single composition can freely share them across components.connections— the statefulwebsocketresource. A resource is owned by the one component that implements it, so the live-object surface is grouped in its own interface.
The surface is client-only: it opens outbound connections and has no listener. It is message-oriented: WebSocket message boundaries are always preserved, never flattened into a byte stream.
Messages normally travel as whole message values. To bound in-memory
buffering for large messages, both directions also have stream-backed
forms (send-via-stream, receive-via-stream) that carry each message as
a stream-message: a kind, a total length in bytes, and a byte
stream payload.
- One
stream-messageis exactly one WebSocket message. Streaming never changes message boundaries. - The bytes carried by
stream-message.datamust matchkind(valid UTF-8 whenkindisstring) and must total exactlylengthbytes. A producer that violates this is in error; the connection may be closed. receive-via-streammay be called once per connection. After that call,receiveand furtherreceive-via-streamcalls failreceiving-via-stream. Pendingreceivecalls fail with the same error: a pending receive is never handed a message once the stream is claimed.- A stream returned by
receive-via-streamends when the connection closes, whatever the cause. The end of a stream carries no error value: consultwait-closedfor close details. - Streaming bounds the guest's memory. It does not promise that the implementation never materializes a message: a browser-backed implementation receives each message fully materialized by the platform before it can stream it onward. The inbound-buffering bound (below) is the only cap on implementation-side buffering.
There is no wire-level inbound backpressure: a guest that receives
slowly does not slow the remote sender down. The W3C WebSocket API
offers no read-side flow control, so a browser-backed implementation
cannot provide it; other implementations deliberately match that floor so
the same guest behaves compatibly everywhere.
Every implementation buffers inbound messages up to an implementation-defined bound (8 MiB of payload bytes by convention). If the buffer overflows:
- the connection is closed toward the peer;
- messages buffered before the overflow remain receivable;
- once the backlog is drained,
receivefailserror.receive-buffer-overflow, and areceive-via-streamstream ends; wait-closedreports whatever the peer's teardown produced, exactly as for any other close.
Guests that need flow control must implement it at the application layer (for example acknowledgment or credit messages).
Outbound sends resolve when the message is handed to the transport.
Implementations bound their outbound buffering; the async ABI carries the
backpressure to the guest (a send future that has not resolved is the
signal to stop producing).
WebSocket close has three distinguishable shapes on this surface:
- Local close.
close(code, reason)validates its arguments eagerly (invalid-argumenton violation; see the method docs for the bounds), then initiates the closing handshake — pending sends flush, then the close frame carryingcodeandreasonis sent — and returns. The close is observed locally at once: in-flight and subsequentsend/receivecalls failerror.closed, the resource's streams end, and unread buffered messages are discarded, as are messages the peer sends during the handshake. The closing procedure is bounded end to end: the connection reachesclosedwhen the handshake completes, or after an implementation-defined bound when pending sends cannot flush or the peer never completes it.closeis idempotent; only the first call's frame is sent. - Remote clean close (a close frame arrives). Messages the peer sent
before its close frame remain receivable:
receivedrains the backlog, then failserror.closed.wait-closedresolvessome(close-info)with the frame's contents (code 1005 and an empty reason when the frame carried none). - Abnormal close (the connection drops without a close frame: TCP
reset or EOF, a TLS failure mid-connection, a handshake that never
completes). The backlog remains receivable, then
receivefailserror.closed.wait-closedresolvesnone.
wait-closed is the one authority for close details. It is latched:
awaiting it any number of times, before or after the close, yields the
same value — the peer's close frame if one was ever received (including
the peer's acknowledgement of a local close), otherwise none.
error.closed deliberately carries no payload. Implementations never
invent a close-info: the browser's synthesized 1006 ("abnormal
closure") is represented as none, not as a frame.
error.closed reports a state, not an event: any operation that cannot
proceed because the connection is closed or closing fails with it,
regardless of who initiated the close or why. After a
receive-buffer-overflow close, receive fails with
receive-buffer-overflow rather than closed once the backlog drains,
so the guest can tell the overflow apart.
Dropping the resource without calling close implies close(none, "").
Which case an operation produces is part of each item's contract; the package-wide rules are:
- The
stringpayloads are human-readable diagnostics for logging. Never match on their contents. They may be empty: a browser-backed implementation cannot observe most failure detail (see "Portability contract"). - Eager cases (
invalid-url,invalid-argument) are produced before any network activity, and the operation has no effect. otheris reserved for implementation-specific failures that fit no named case. A conforming implementation produces a named case whenever one applies.
The browser WebSocket API is the least capable implementation, and it
bounds this surface. Capabilities it cannot serve do not appear here:
request headers, cookies, client certificates, proxy control, TLS trust
decisions, ping/pong access, and read-side flow control. Close codes a
client may send are restricted to 1000 and 3000-4999, and close reasons
to 123 bytes, because the browser enforces exactly that; every
implementation applies the same bounds so guests behave identically
everywhere.
Latitude — points where implementations may differ, recorded here so guests do not rely on either behavior:
- Wire-level extension negotiation. This surface carries no
WebSocket extensions (RFC 6455 section 9): none can be offered,
configured, or observed. Implementations may still negotiate
extensions transparently where their platform does — a browser-backed
implementation always offers
permessage-deflateand cannot be told not to — so the wire under a connection may be compressed on one implementation and not another. Message semantics, boundaries, and buffering bounds (which count payload bytes) are identical either way. - Failure diagnostics. The
stringpayload ofconnect-failed(and the diagnostic detail of abnormal closes generally) is implementation-defined and may be empty. Browsers deliberately hide connection-failure detail; native stacks can report more. Guests get a stable shape (connect-failed, orwait-closedreturningnone), not stable detail. - TCP teardown cleanliness. Whether the transport under a completed
closing handshake tore down cleanly (the W3C
wasCleanflag beyond the frame exchange) is not exposed:close-infopresence means "the peer's close frame was received", nothing more. - Concurrency ordering. Concurrently pending
sends andreceives are served in an implementation-defined order. closingobservability. Whetherstateever reportsclosingis implementation- and timing-defined: a locally initiated close passes through it observably, but no implementation can promise it for a remote close (the browserWebSocketAPI fires no event on that transition), and a poll can always land after the transition completed.- Buffer bounds and timeouts. The inbound-buffer bound, the connect timeout, and the closing-handshake bound are implementation-defined; embedders may configure them through implementation-specific channels.
Decisions that shape the surface, recorded so the doc comments can stay short:
connectreturns an open connection. There is no constructor plus wait-for-open: the resource exists only once the handshake succeeded, so the W3CCONNECTINGstate is unrepresentable andwebsocket-statehas noconnectingmember.- No URL accessor. The guest supplied the URL; echoing it back would only expose implementation-specific normalization differences.
closeis fallible only about its arguments. The result reports eager validation; initiation itself cannot fail and completion is awaited throughwait-closed. Rejecting (rather than truncating or clamping) keeps every implementation's behavior identical to the browser's, which throws for the same inputs.- Close details ride
wait-closed, noterror.closed. A single authority avoids implementations disagreeing about which errors carry the frame, and keeps this package'serror.closedshaped like its sibling transports' for consumers that abstract over message transports. - Lifecycle is a getter plus a latch, not a watch. The only lifecycle
push every implementation can guarantee is the terminal one, and
wait-closedalready delivers it carrying the close frame; the browser fires no event for theclosingtransition, so a state-change stream could promise nothing more thanstate+wait-closeddo, while adding take-once semantics whose "ended without yielding" shape collides with meaningful end-of-stream. connecttakes plain arguments, and the connect surface is complete. The portable connect surface is exactly a URL and a subprotocol offer — the browser constructor accepts nothing more, and this package carries no capabilities the browser cannot serve. A connect-options gate (request headers, proxy control, and the like on non-browser hosts) was considered and rejected: no unportable connect capabilities, not even gated ones.- Text messages are
string. The component model already guarantees valid UTF-8 forstring, which matches the WebSocket text frame contract; a text message can never carry invalid UTF-8 on this surface.
- Message: one WebSocket protocol message (RFC 6455 section 5.6), binary or text. Frames and fragmentation are below this surface.
- Closing handshake: the close-frame exchange of RFC 6455 section 7.
- Close code / reason: the status code and UTF-8 reason of a close frame (RFC 6455 section 5.5.1). Code 1005 means "no code was present"; it is never sent on the wire.
- Subprotocol: an application-level protocol negotiated during the
handshake via
Sec-WebSocket-Protocol(RFC 6455 section 1.9). - Abnormal closure: a connection that ended without a received close frame (RFC 6455 section 7.1.7).