# PowerWebTransportClient

> **Client-side helper** — dials out and owns the connection lifecycle. The
> server-side counterpart is [`createWebTransportAdapter`](powerWebTransportAdapter.md).

Reconnecting WebTransport client with **stream-based back-pressure**, heartbeats, and `PowerMessageCodec` framing.

## Constructor

| option                        | type                            | default                   | description                                                                                                               |
| ----------------------------- | ------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `url`                         | `string`                        | _(required)_              | The `https://` URL.                                                                                                       |
| `WebTransportImpl`            | `Function`                      | `globalThis.WebTransport` | Constructor override, for tests or a non-global implementation.                                                           |
| `codec`                       | `'json'` \| `'raw'`             | `'json'`                  | Frame codec handed to the codec module.                                                                                   |
| `connectTimeoutMs`            | `number`                        | `10000`                   | Abort the connect attempt after this long. `0` disables the timeout.                                                      |
| `maxPayloadSizeBytes`         | `number`                        | `Infinity`                | Maximum accepted size for incoming framed data. Oversized frames are rejected by the decoder and are not delivered.       |
| `heartbeatIntervalMs`         | `number`                        | `30000`                   | Send a heartbeat at this interval. `0` disables heartbeats.                                                               |
| `heartbeatTimeoutMs`          | `number`                        | `10000`                   | Declare the transport dead if a heartbeat reply is not received in this long.                                             |
| `maxReconnectAttempts`        | `number`                        | `Infinity`                | `Infinity` retries forever with decorrelated-jitter backoff.                                                              |
| `maxReconnectElapsedMs`       | `number`                        | `Infinity`                | Wall-clock ceiling on one reconnect run, in milliseconds.                                                                 |
| `reconnectBaseMs`             | `number`                        | `500`                     | Base delay for the backoff.                                                                                               |
| `reconnectMaxMs`              | `number`                        | `30000`                   | Ceiling for the backoff.                                                                                                  |
| `autoReconnect`               | `boolean`                       | `true`                    | Reconnect on an unexpected close.                                                                                         |
| `reconnectOnHeartbeatTimeout` | `boolean`                       | `true`                    | Reconnect when a heartbeat goes unanswered.                                                                               |
| `onMessage`                   | `Function`                      | —                         | Called with each decoded message.                                                                                         |
| `onOpen`                      | `Function`                      | —                         | Called once the transport reaches `OPEN`.                                                                                 |
| `onClose`                     | `Function`                      | —                         | Called with the close code and reason.                                                                                    |
| `onError`                     | `Function`                      | —                         | Called with each transport error.                                                                                         |
| `rtt`                         | `PowerHistogram`                | —                         | Histogram for heartbeat RTT. One is created when omitted.                                                                 |
| `observability`               | `boolean` \| `MetricsCollector` | `false`                   | Opt in to metrics: `true` registers this helper in the shared collector, or pass a collector of your own. Off by default. |
| `streamPriority`              | `number`                        | `0`                       | QUIC stream send priority (higher = more urgent). Passed to `createBidirectionalStream({ sendOrder })` when non-zero.     |
| `sendOrder`                   | `number`                        | `0`                       | QUIC send order hint for partial reliability.                                                                             |
| `datagrams`                   | `boolean`                       | `false`                   | Enable the datagram path when the transport exposes `transport.datagrams`.                                                |
| `datagramFallback`            | `boolean`                       | `true`                    | Fall back to the bidirectional stream when datagrams are unavailable.                                                     |
| `onDatagram`                  | `Function`                      | —                         | Called with each received datagram.                                                                                       |

## The problem this exists for

`PowerWebSocketClient` covers the WebSocket case. WebTransport is the HTTP/3 successor: same request/response shape, but the data path is streams and datagrams rather than messages, and the platform does not provide `ping()`/`pong()` or `bufferedAmount`. This client mirrors the `PowerWebSocketClient` API so a hub can swap transports without changing the call sites.

## Back-pressure

WebTransport exposes back-pressure through the stream's `ready` promise, which resolves when the transport has room. No watermark polling is required:

```javascript
const client = new PowerWebTransportClient({
  url: 'https://example.test/feed',
  WebTransportImpl,
  codec: 'json',
});
```

`send()` and `sendFrame()` both await `writer.ready` before writing, so a slow network naturally slows the producer.

`connect()` also waits for the platform's promise-returning `createBidirectionalStream()` before resolving or emitting `open`. A delayed or rejected stream creation therefore cannot expose a half-open client.

Inbound stream frames are decoded exactly once by the incremental frame decoder, including split and multi-frame chunks. `close()` observes asynchronous reader cancellation and writer shutdown failures so an already-completed stream does not create unhandled rejections.

If the active readable or transform reader fails, the client reports the error and treats it as an abnormal close (`1006`), allowing the configured reconnect policy to run. Failures from an older connection and user-initiated cancellation are ignored.

If the stream ends with a partial frame header or payload, strict decoder flushing reports the truncation through `onError`; partial data is not delivered.

## Heartbeats

Heartbeats are application-level because WebTransport has no native ping/pong. The client sends `{ __heartbeat: 'ping' }` in a framed JSON message at `heartbeatIntervalMs`; the peer replies with `{ __heartbeat: 'pong' }`. Control frames are consumed internally, matching replies clear only their own deadline and record RTT in `rtt`, and they are never passed to `onMessage`. If the deadline fires, the client counts a `heartbeatTimeout` and, if `reconnectOnHeartbeatTimeout` is set, closes the transport and reconnects.

## QUIC stream priority

`streamPriority` and `sendOrder` are forwarded to `createBidirectionalStream()` as
`{ sendOrder }` when non-zero, so a high-priority stream can jump ahead of a
bulk transfer on the same connection. They are hints, not guarantees — the
platform decides how to honour them.

## Datagrams

When `datagrams: true` is set and the transport exposes `transport.datagrams`,
the client starts a receive pump on open and delivers each datagram to
`onDatagram`. Outbound datagrams go through `sendDatagram(data)`, which returns
`true` on success:

```js
const client = new PowerWebTransportClient({
  url: 'https://example.test/feed',
  datagrams: true,
  onDatagram: (value) => console.log('datagram', value),
});
await client.connect();
await client.sendDatagram(new Uint8Array([1, 2, 3]));
```

Datagrams are unreliable and unordered. When the transport does not expose a
datagram path and `datagramFallback` is enabled (the default), `sendDatagram()`
falls back to `send()` over the bidirectional stream.

## Reconnection

Unexpected closes trigger decorrelated-jitter backoff, bounded by `maxReconnectAttempts` and `maxReconnectElapsedMs`. The reconnect state is reset on a successful open, so a flappy network does not accumulate debt.

## Pairing with PowerRealtimeHub

Pass `client.send` as the hub's `send` adapter and `client.close` as the hub's `close` adapter. The hub's `codec: 'json'` path batches values into one JSON array frame, which the client writes as a single stream write.

## Disposal

`PowerWebTransportClient` implements `dispose()` and `[Symbol.dispose]`, so it
works with `using` / `await using` and with a DI container's teardown.

```javascript
{
  using client = new PowerWebTransportClient({ url });
  // ...
} // dispose() runs here, closing the connection
```

`dispose()` detaches metrics and closes the connection. It is idempotent and
safe while idle.
