# @caprid/relay-websocket

> The Relay SDK's transport adapter for QMux over WebSocket.

URL: https://www.caprid.com/docs/sdks/typescript/relay-websocket/
Source: https://github.com/caprids/relay-sdks/blob/main/ts/packages/relay-websocket/README.md

The Relay SDK's transport adapter for QMux over WebSocket. It carries MOQT on
[QMux](https://datatracker.ietf.org/doc/draft-ietf-quic-qmux/), which runs QUIC's streams over a reliable byte stream
(wire format `qmux-01`), inside one WebSocket connection. moq-relay serves it on TCP 443
([draft-lcurley-qmux-websocket](https://github.com/moq-dev/moq/blob/main/drafts/draft-lcurley-qmux-websocket.md)). The
adapter reaches the relay where WebTransport cannot: on networks that block UDP, and in browsers whose WebTransport
cannot negotiate protocols. It works in browsers and in Node 24.

It follows the transport adapter contract in [`spec/transport-adapter.md`](https://github.com/caprids/relay-sdks/blob/main/spec/transport-adapter.md), with
the deviations recorded there, and passes its conformance suite in Node, Chromium, Firefox and WebKit. QMux comes from
[`@moq/qmux`](https://www.npmjs.com/package/@moq/qmux), by the author of moq-relay's own QMux, at an exact version;
[ADR 0005](https://github.com/caprids/relay-sdks/blob/main/docs/adr/0005-websocket-adapter-wraps-moq-qmux.md) records why.

## Use

Pass one of its factories as the SDK's `createTransport`. Build it once and reuse it for every connection.

```ts
import { connectRelaySubscriber } from '@caprid/relay';
import { createWebSocketTransport, withWebSocketFallback } from '@caprid/relay-websocket';

// WebTransport first; WebSocket races it once WebTransport has had 250 ms.
const subscriber = await connectRelaySubscriber({ url, getToken, createTransport: withWebSocketFallback() });

// Always WebSocket.
const other = await connectRelaySubscriber({ url, getToken, createTransport: createWebSocketTransport() });
```

- `createWebSocketTransport()` dials the endpoint's `wss:` URL and offers `qmux-01.moqt-22`, then `qmux-01.moqt-20`.
  `https:` becomes `wss:` with the path and query unchanged, so the token travels in the upgrade request. The factory
  declares `framing: 'websocket'`.
- `withWebSocketFallback({ webSocketDelayMs, onFallback })` dials the runtime's `WebTransport` first. If that
  handshake has not completed within `webSocketDelayMs` (default 250, as Happy Eyeballs), it dials WebSocket alongside
  it; a WebTransport handshake that fails sooner starts the WebSocket at once. The first ready carries the session and
  the other dial is closed. Where the runtime has no usable `WebTransport`, as in Node or a Safari without it, it dials
  WebSocket at once. The adapter calls `onFallback(reason)` each time a connection ends up on WebSocket, and every
  connection decides afresh. After `ready`, the factory's `framing` and the transport's `carrier` say which transport
  won.

## What differs from WebTransport

- Every stream shares one TCP connection, so a lost packet holds up every stream behind it (head-of-line blocking).
- No datagrams.
- A relay that refuses the token answers the WebSocket upgrade with HTTP 401, which no WebSocket API exposes, so the
  SDK reports `TRANSPORT_ERROR` in phase `connecting`, not `UNAUTHORIZED`.
- A send stream's close resolves once the FIN is handed to the socket, not once the relay has it.

The relay's side, with measurements, is in [QMux over WebSocket](https://github.com/caprids/relay-sdks/blob/main/spec/relay-interop.md#qmux-over-websocket).

## Trust

The adapter uses the runtime's WebSocket and its certificate checks. A browser offers no way to pin a WebSocket's
certificate. Node trusts the platform's roots, plus any in `NODE_EXTRA_CA_CERTS`, which Node reads only at start-up.

## Tests

Run these from `ts/` in the repository.

- `pnpm test` runs the unit tests in `src/tests/` against an in-process accepting end over plain `ws://`.
- `pnpm test:conformance:websocket` runs the conformance suite in Node against `scripts/websocket-peer.js`, the
  accepting end for QMux over WebSocket.
- `pnpm test:conformance:websocket:browsers` runs the suite in Chromium, Firefox, and WebKit. Set `BROWSERS=chromium`
  to run one.
- `RELAY_RUNTIME=node-websocket,chromium-websocket pnpm test:relay` runs the live scenario over WebSocket.

## Stability

Before 1.0, a minor version can break the API. This package releases together with `@caprid/relay` at the same version;
see the [changelog](https://github.com/caprids/relay-sdks/blob/main/ts/CHANGELOG.md).
