first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
# @suaveplan/drive-transport-webrtc - WebRTC datachannel transport for the drive mesh. Implements DriveTransport over @suaveplan/drive-webrtc's WebRTCClient + DataChannelManager with a length-prefixed binary framing protocol.
## Summary
WebRTC datachannel transport for the drive mesh. Implements
the canonical `DriveTransport` interface from `@suaveplan/drive-mesh`
over a `@suaveplan/drive-webrtc` `WebRTCClient` + `DataChannelManager`
pair, multiplexing chunk requests, chunk responses, and presence
advertisements over a single named data-channel label per peer.
This package re-exports `DriveTransport` / `ChunkAdvertisement` /
`AdvertisementHandler` / `Unsubscribe` from `@suaveplan/drive-mesh` —
there is no local interface mirror. A drive-mesh `DriveMesh` instance
slots a `WebRTCDriveTransport` into its transport plane and consumes
this transport's `requestChunk` / `advertise` / `onAdvertisement` /
`peers` surface directly. For the HTTPS / server-fallback path, mesh
ships its own built-in `ServerFallback` (no separate transport
package).
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Defines the typed error-code registry and `DriveTransportWebRtcError` class used by every public boundary of `@suavepla…
- [error-codes](./src/error-codes/error-codes.md) — Defines the typed error-code registry and `DriveTransportWebRtcError` class used by every public boundary of `@suavepla…
- [frame](./src/framing/frame/frame.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [frame](./src/framing/frame/frame.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [frame.schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bitset-codec](./src/transport/webrtc-transport/bitset-codec.md) — Provides the gzip codec used to compress advertisement bitsets before they hit the wire. Bitsets describing which chunk…
- [hex](./src/transport/webrtc-transport/hex.md) — Boundary helpers converting between the on-the-wire 32-byte file id representation and the hex-encoded form used by eve…
- [payload-codec](./src/transport/webrtc-transport/payload-codec.md) — Implements the byte-level encoder/decoder for the two structured frame bodies the WebRTC drive transport carries: chunk…
- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [frame](./src/framing/frame/frame.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame.schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset-codec](./src/transport/webrtc-transport/bitset-codec.md) — Provides the gzip codec used to compress advertisement bitsets before they hit the wire. Bitsets describing which chunk…
- [hex](./src/transport/webrtc-transport/hex.md) — Boundary helpers converting between the on-the-wire 32-byte file id representation and the hex-encoded form used by eve…
- [payload-codec](./src/transport/webrtc-transport/payload-codec.md) — Implements the byte-level encoder/decoder for the two structured frame bodies the WebRTC drive transport carries: chunk…
- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the typed error-code registry and `DriveTransportWebRtcError` class used by every public boundary of `@suavepla…
- [frame](./src/framing/frame/frame.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame.schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset-codec](./src/transport/webrtc-transport/bitset-codec.md) — Provides the gzip codec used to compress advertisement bitsets before they hit the wire. Bitsets describing which chunk…
- [hex](./src/transport/webrtc-transport/hex.md) — Boundary helpers converting between the on-the-wire 32-byte file id representation and the hex-encoded form used by eve…
- [payload-codec](./src/transport/webrtc-transport/payload-codec.md) — Implements the byte-level encoder/decoder for the two structured frame bodies the WebRTC drive transport carries: chunk…
- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the typed error-code registry and `DriveTransportWebRtcError` class used by every public boundary of `@suavepla…
- [frame](./src/framing/frame/frame.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [frame.schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset-codec](./src/transport/webrtc-transport/bitset-codec.md) — Provides the gzip codec used to compress advertisement bitsets before they hit the wire. Bitsets describing which chunk…
- [hex](./src/transport/webrtc-transport/hex.md) — Boundary helpers converting between the on-the-wire 32-byte file id representation and the hex-encoded form used by eve…
- [payload-codec](./src/transport/webrtc-transport/payload-codec.md) — Implements the byte-level encoder/decoder for the two structured frame bodies the WebRTC drive transport carries: chunk…
- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the typed error-code registry and `DriveTransportWebRtcError` class used by every public boundary of `@suavepla…
- [frame](./src/framing/frame/frame.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [frame.schemas](./src/schemas/frame.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bitset-codec](./src/transport/webrtc-transport/bitset-codec.md) — Provides the gzip codec used to compress advertisement bitsets before they hit the wire. Bitsets describing which chunk…
- [hex](./src/transport/webrtc-transport/hex.md) — Boundary helpers converting between the on-the-wire 32-byte file id representation and the hex-encoded form used by eve…
- [payload-codec](./src/transport/webrtc-transport/payload-codec.md) — Implements the byte-level encoder/decoder for the two structured frame bodies the WebRTC drive transport carries: chunk…
- [webrtc-transport](./src/transport/webrtc-transport/webrtc-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the typed error-code registry and `DriveTransportWebRtcError` class used by every public boundary of `@suavepla…
## Quick start
```ts
import {
DataChannelManager,
WebRTCClient,
} from "@suaveplan/drive-webrtc";
import { WebRTCDriveTransport } from "@suaveplan/drive-transport-webrtc";
const client = new WebRTCClient({
peerId: "self",
config: { signaling: { url: "wss://signal.example.com" } },
});
const channels = new DataChannelManager({ client });
// negotiate connection out of band via SignalingClient ...
const transport = new WebRTCDriveTransport({
client,
dataChannels: channels,
defaultLabel: "drive-chunks",
maxConcurrentRequests: 16,
requestTimeoutMs: 10_000,
});
// pull a chunk from a peer (fileId is the hex-encoded 32-byte digest)
const fileId = "ab".repeat(32);
const bytes = await transport.requestChunk("bob", fileId, 0);
// broadcast presence — single `ChunkAdvertisement` object
await transport.advertise("bob", {
fileId,
bitset: ourBitset,
totalChunks: 1024,
});
// subscribe to incoming advertisements (peerId + advert two-arg form)
const unsubscribe = transport.onAdvertisement((peerId, advert) => {
presenceIndex.update(peerId, advert.fileId, advert.bitset);
});
// teardown
await transport.dispose();
unsubscribe();
```
## Integration with `drive-mesh`
`WebRTCDriveTransport` implements the canonical `DriveTransport`
contract from `@suaveplan/drive-mesh`. A mesh instance accepts a
list of `DriveTransport` implementations:
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
import { WebRTCDriveTransport } from "@suaveplan/drive-transport-webrtc";
const mesh = new DriveMesh({
transports: [
new WebRTCDriveTransport({ client, dataChannels }),
],
// ...peer registry, signed-chunk source, optional ServerFallback
});
```
The HTTPS / server-fallback path lives inside drive-mesh itself
(`ServerFallback` — see `@suaveplan/drive-mesh`'s
`./server-fallback/server-fallback` module), so there is no
companion `drive-transport-https` package to plug in.
## Wire framing
Every datachannel message is a length-prefixed binary frame:
```text
+--------+--------+----------------------+
| 4 B | 1 B | frameLen - 1 B |
| length | type | payload |
+--------+--------+----------------------+
```
- `length` — big-endian uint32 byte count of `type + payload`.
- `type` — `0x01` chunk-request, `0x02` chunk-response, `0x03`
advertisement.
- `payload` shape varies by type:
- Chunk request: `fileId(32 raw bytes) ‖ chunkId(4 BE uint32)`.
- Chunk response: raw chunk bytes (length is already known from
the outer header).
- Advertisement: `fileId(32 raw bytes) ‖ totalChunks(4 BE uint32) ‖ gzip(bitset)`.
Public API takes / returns the hex-encoded form (`fileId: string`,
64 lowercase hex chars). The codec converts to / from raw bytes at
the boundary; on-the-wire bytes are unchanged from the pre-rewrite
representation except for the new 4-byte `totalChunks` field.
Frame bodies are capped at 16 MiB. Malformed frames are dropped on the
receive side and surface as `DriveTransportWebRtcError(FRAME_INVALID)`
on the send side.
## Backpressure
Outgoing sends route through `DataChannelManager.send`, which pauses
when `bufferedAmount` exceeds the data-channel high-water mark and
resumes on the `bufferedamountlow` event. No additional flow-control
state is held in this package.
## OTel surface
| Instrument | Kind | Attributes |
| --------------------------------------------------- | --------- | ------------------------------------- |
| `drive.transport.webrtc.requests_total` | counter | `outcome` ∈ ok / timeout / aborted / send_failed / rejected_overflow |
| `drive.transport.webrtc.advertisements_total` | counter | `direction` ∈ inbound / outbound |
| `drive.transport.webrtc.request_duration_ms` | histogram | `outcome` |
| `drive.transport.webrtc.frame_size_bytes` | histogram | `type` ∈ chunk-request / advertisement |
## Error codes
All errors are instances of `DriveTransportWebRtcError`:
- `FRAME_INVALID` — frame header or payload failed validation.
- `FRAME_TOO_LARGE` — frame body exceeded the 16 MiB cap.
- `REQUEST_TIMEOUT` — chunk request did not receive a response in time.
- `PEER_NOT_CONNECTED` — no data channel could be opened for the peer.
- `DATA_CHANNEL_CLOSED` — channel closed before / during send.
- `BITSET_COMPRESSION_FAILED` — gzip codec failed.
- `ABORTED` — `AbortSignal` fired or transport was disposed.
## License
MIT

View File

@@ -0,0 +1,91 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the typed error-code registry and `DriveTransportWebRtcError`
class used by every public boundary of
`@suaveplan/drive-transport-webrtc`. The registry is created via the
shared `defineCodes` helper from `@suaveplan/error` so that codes are
emitted to the cross-package registry on first import; the wrapper
class extends `SuaveplanError<DriveTransportWebRtcErrorCode>` so that
consumers receive narrowly-typed `code` discriminators in catch blocks
and the stack trace carries the `DriveTransportWebRtcError` class name.
The registry deliberately ships a small, stable surface (seven codes)
matching the failure modes of a binary-framing, single-channel
transport — frame validation, request lifecycle, channel state, and a
bitset compression failure.
## Features
- `DRIVE_TRANSPORT_WEBRTC_CODES`: frozen record of the seven codes.
- `DriveTransportWebRtcErrorCode`: discriminated string-literal union.
- `DriveTransportWebRtcError`: subclass of `SuaveplanError` whose
constructor is public so call sites can `throw new`.
- `name = "DriveTransportWebRtcError"` for stack-trace clarity.
## Usage: Basic Example
```ts
import {
DRIVE_TRANSPORT_WEBRTC_CODES,
DriveTransportWebRtcError,
} from "@suaveplan/drive-transport-webrtc";
throw new DriveTransportWebRtcError(
DRIVE_TRANSPORT_WEBRTC_CODES.REQUEST_TIMEOUT,
"chunk request to alice timed out"
);
```
## Usage: Advanced Example
```ts
import {
DRIVE_TRANSPORT_WEBRTC_CODES,
DriveTransportWebRtcError,
type DriveTransportWebRtcErrorCode,
} from "@suaveplan/drive-transport-webrtc";
function isRetryable(code: DriveTransportWebRtcErrorCode): boolean {
return (
code === DRIVE_TRANSPORT_WEBRTC_CODES.REQUEST_TIMEOUT ||
code === DRIVE_TRANSPORT_WEBRTC_CODES.DATA_CHANNEL_CLOSED
);
}
try {
await transport.requestChunk(peerId, fileId, chunkId).next();
} catch (err) {
if (err instanceof DriveTransportWebRtcError && isRetryable(err.code)) {
// schedule retry with backoff
} else {
throw err;
}
}
```
## API Reference
- `DRIVE_TRANSPORT_WEBRTC_CODES` — frozen const containing
`FRAME_INVALID`, `FRAME_TOO_LARGE`, `REQUEST_TIMEOUT`,
`PEER_NOT_CONNECTED`, `DATA_CHANNEL_CLOSED`,
`BITSET_COMPRESSION_FAILED`, `ABORTED`.
- `DriveTransportWebRtcErrorCode` — union of the seven code strings.
- `class DriveTransportWebRtcError extends SuaveplanError<DriveTransportWebRtcErrorCode>` —
carries `code`, optional `cause` (`SuaveplanErrorOptions`), and
`name = "DriveTransportWebRtcError"`.
## Implementation Notes
The class explicitly assigns `this.name` rather than relying on the
default `Error.name` so that test harnesses and observability sinks
can filter by class name without resolving the prototype chain. The
codes are deliberately not numbered — the
`SuaveplanError`/`defineCodes` family keys errors by namespaced string
literals (`DRIVE_TRANSPORT_WEBRTC.REQUEST_TIMEOUT`), keeping the
registry stable across refactors.

View File

@@ -0,0 +1,96 @@
# `framing/frame`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the length-prefixed binary frame codec used by the WebRTC
drive transport. Every datachannel message is wrapped in a frame with
a 4-byte big-endian length, a 1-byte type discriminator, and a
variable-length payload. Encoding and decoding are kept in a single
module so the wire shape has exactly one source of truth.
The codec is deliberately runtime-agnostic — it consumes and produces
`Uint8Array` views with no dependency on `Buffer`, `node:stream`, or
browser-only types — and bounds every allocation against
`MAX_FRAME_BYTES` so that a hostile peer cannot induce
out-of-memory conditions by claiming a multi-gigabyte body length.
## Features
- `encodeFrame(type, payload)` produces the wire bytes for a frame.
- `decodeFrame(bytes)` parses a complete frame and returns
`{ type, payload }`.
- `FRAME_TYPE` constant for the three valid type bytes
(`CHUNK_REQUEST = 0x01`, `CHUNK_RESPONSE = 0x02`,
`ADVERTISEMENT = 0x03`).
- `MAX_FRAME_BYTES = 16 MiB` cap mirroring the data-channel high-water
mark.
- `toFrameBytes(data)` coerces a `MessageEvent.data` value into a
`Uint8Array`, returning `undefined` for string payloads which the
transport refuses to accept.
- Every malformed input maps to a typed
`DriveTransportWebRtcError(FRAME_INVALID | FRAME_TOO_LARGE)`.
## Usage: Basic Example
```ts
import { encodeFrame, decodeFrame, FRAME_TYPE } from "@suaveplan/drive-transport-webrtc";
const bytes = encodeFrame(FRAME_TYPE.CHUNK_REQUEST, new Uint8Array([1, 2, 3]));
const frame = decodeFrame(bytes);
console.log(frame.type === FRAME_TYPE.CHUNK_REQUEST); // true
console.log(frame.payload); // Uint8Array(3) [1, 2, 3]
```
## Usage: Advanced Example
```ts
import {
decodeFrame,
FRAME_TYPE,
toFrameBytes,
DriveTransportWebRtcError,
} from "@suaveplan/drive-transport-webrtc";
function onChannelMessage(event: MessageEvent): void {
const bytes = toFrameBytes(event.data);
if (bytes === undefined) return; // strings rejected by design
try {
const frame = decodeFrame(bytes);
if (frame.type === FRAME_TYPE.ADVERTISEMENT) {
handleAdvertisement(frame.payload);
}
} catch (err) {
if (err instanceof DriveTransportWebRtcError) {
logger.warn({ code: err.code }, "dropping malformed frame");
}
}
}
```
## API Reference
- `encodeFrame(type: FrameType, payload: Uint8Array): Uint8Array` —
throws `FRAME_INVALID` for unknown types and `FRAME_TOO_LARGE` when
the body exceeds `MAX_FRAME_BYTES`.
- `decodeFrame(bytes: Uint8Array): DecodedFrame` — validates length,
body cap, and type discriminator.
- `toFrameBytes(data: ArrayBuffer | string | Uint8Array): Uint8Array | undefined` —
string payloads return `undefined`.
- `FRAME_TYPE` — `{ CHUNK_REQUEST: 0x01, CHUNK_RESPONSE: 0x02, ADVERTISEMENT: 0x03 }`.
- `MAX_FRAME_BYTES` — soft cap on `frameType + payload` byte count.
- `MIN_FRAME_BYTES` — minimum body length (1).
## Implementation Notes
The 4-byte length prefix is interpreted as the byte count of
`frameType + payload` (not the whole frame including the prefix
itself) — this matches the proposal's wire format exactly. The
decoder rejects buffers whose total length disagrees with the
header so that a slicing bug at the call site surfaces as
`FRAME_INVALID` rather than silently truncated data. The codec does
not attempt to handle streamed accumulation — that responsibility
lives in the data-channel layer, which assembles full frames before
invoking `decodeFrame`.

View File

@@ -0,0 +1,84 @@
# `schemas/frame.schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod boundary schemas for the two structured payload shapes the WebRTC
drive transport speaks on the wire: chunk-request and advertisement.
Frames themselves are length-prefixed binary blobs validated by the
`framing/frame` module; once the type discriminator is read, the
payload bytes are projected into one of the shapes validated here so
that consumers handling the payload at the application layer get a
Zod-safe value rather than raw bytes.
The shapes match the canonical `DriveTransport` contract from
`@suaveplan/drive-mesh`: file ids are hex-encoded (`64` lowercase
hex chars) and advertisements carry a `totalChunks` field alongside
the gzipped bitset. Anything else fails Zod validation and surfaces
as a `DriveTransportWebRtcError(FRAME_INVALID)` at the transport
boundary.
## Features
- `RequestChunkPayloadSchema` — `{ fileId: string(hex64), chunkId: uint32 }`.
- `AdvertisementPayloadSchema` — `{ fileId: string(hex64), totalChunks: uint32, bitsetGzip: Uint8Array(>0) }`.
- `FILE_ID_BYTES` — the raw 32-byte wire length constant.
- `FILE_ID_HEX_LENGTH` — the corresponding `64` hex-char length.
## Usage: Basic Example
```ts
import {
RequestChunkPayloadSchema,
} from "@suaveplan/drive-transport-webrtc";
const parsed = RequestChunkPayloadSchema.parse({
fileId: "ab".repeat(32),
chunkId: 42,
});
```
## Usage: Advanced Example
```ts
import {
AdvertisementPayloadSchema,
decodeAdvertisement,
decodeFrame,
FRAME_TYPE,
} from "@suaveplan/drive-transport-webrtc";
function projectAdvertisement(frameBytes: Uint8Array) {
const frame = decodeFrame(frameBytes);
if (frame.type !== FRAME_TYPE.ADVERTISEMENT) return undefined;
const decoded = decodeAdvertisement(frame.payload);
return AdvertisementPayloadSchema.parse(decoded);
}
```
## API Reference
- `FILE_ID_BYTES: 32` — re-exported raw-bytes constant for the
on-the-wire form.
- `FILE_ID_HEX_LENGTH: 64` — corresponding hex-char length for the
public API form.
- `RequestChunkPayloadSchema` — Zod object with exact-64-char hex
`fileId` refinement and `chunkId` clamped to non-negative uint32.
- `AdvertisementPayloadSchema` — Zod object enforcing the hex
`fileId`, a uint32 `totalChunks`, and a non-empty `bitsetGzip`
payload.
- `RequestChunkPayload` / `AdvertisementPayload` — inferred TypeScript
types.
## Implementation Notes
The schemas live outside the codec module so that consumers who want
to validate at the application boundary (e.g. before queueing a
request for retry) can do so without re-importing the binary codec.
The `bitsetGzip` field is asserted non-empty because every well-formed
gzip stream is at least a few bytes; an empty value usually indicates
a slicing bug at the sender, which we surface eagerly. `chunkId` and
`totalChunks` are clamped to uint32 to match the wire encoding
(`DataView.setUint32`).

View File

@@ -0,0 +1,77 @@
# `transport/webrtc-transport/bitset-codec`
## Purpose
Provides the gzip codec used to compress advertisement bitsets before
they hit the wire. Bitsets describing which chunks of a file a peer
has tend to contain long runs of zeros or ones — gzip routinely
trims them from several kilobytes down to a few hundred bytes,
materially reducing datachannel bandwidth on advertisement broadcasts.
The codec is built on the platform `CompressionStream` /
`DecompressionStream` primitives (available in browsers, Bun, and Node
≥ 18). It is exposed as an injectable interface so test environments
or legacy runtimes can supply a synchronous shim if needed.
## Features
- `BitsetCodec` injectable interface with `compress` / `decompress`
methods.
- `createDefaultBitsetCodec()` returns a platform-backed codec.
- Every failure path maps to
`DriveTransportWebRtcError(BITSET_COMPRESSION_FAILED)` carrying the
underlying cause.
## Usage: Basic Example
```ts
import { createDefaultBitsetCodec } from "@suaveplan/drive-transport-webrtc";
const codec = createDefaultBitsetCodec();
const wire = await codec.compress(new Uint8Array([0, 0, 0, 1, 0]));
const round = await codec.decompress(wire);
```
## Usage: Advanced Example
```ts
import {
type BitsetCodec,
WebRTCDriveTransport,
} from "@suaveplan/drive-transport-webrtc";
import { gzipSync, gunzipSync } from "node:zlib";
const nodeZlibCodec: BitsetCodec = {
async compress(bytes) {
return new Uint8Array(gzipSync(bytes));
},
async decompress(bytes) {
return new Uint8Array(gunzipSync(bytes));
},
};
const transport = new WebRTCDriveTransport({
client,
dataChannels,
bitsetCodec: nodeZlibCodec,
});
```
## API Reference
- `interface BitsetCodec` — `compress(bytes: Uint8Array): Promise<Uint8Array>`
and `decompress(bytes: Uint8Array): Promise<Uint8Array>`.
- `createDefaultBitsetCodec(): BitsetCodec` — picks
`CompressionStream` / `DecompressionStream` off `globalThis`. Throws
`BITSET_COMPRESSION_FAILED` at call time if the runtime does not
expose them.
## Implementation Notes
Compression is wrapped in a `Blob` + `Response` round-trip so the
codec works against the standard `ReadableStream`/`Response`
interfaces rather than reaching for `node:stream/web`. This keeps the
universal promise that the package can ship into any modern
runtime without optional native dependencies. The codec rejects with a
typed error rather than letting `TypeError` from a missing
constructor escape, so call sites can branch on `err.code`.

View File

@@ -0,0 +1,74 @@
# `transport/webrtc-transport/hex`
## Purpose
Boundary helpers converting between the on-the-wire 32-byte file id
representation and the hex-encoded form used by every public API in the
canonical `DriveTransport` interface (defined in
`@suaveplan/drive-mesh`). The WebRTC transport keeps the wire frames
binary — 32 raw bytes followed by metadata — but every public method
takes a hex `fileId` so callers can pass the same id they get from
`drive-plugin-chunks`' `ChunkManifest` without doing the conversion
themselves.
The module delegates to `@suaveplan/crypto-utils`' `bytesToHex` /
`hexToBytes` for the actual encoding (constant-time, throughly-tested
shared impl) and wraps the conversion in `DriveTransportWebRtcError`
(`FRAME_INVALID`) on bad input so the codec layer can rely on a single
error type.
## Features
- `fileIdBytesToHex(bytes)` — strict 32-byte input check.
- `fileIdHexToBytes(hex)` — strict 64-hex-char input check.
- Maps both length mismatches and malformed-hex parse errors to the
canonical `FRAME_INVALID` code so the caller never sees a raw
underlying error.
## Usage: Basic Example
```ts
import {
fileIdBytesToHex,
fileIdHexToBytes,
} from "@suaveplan/drive-transport-webrtc";
const bytes = new Uint8Array(32).fill(0xab);
const hex = fileIdBytesToHex(bytes); // "abab...ab"
const back = fileIdHexToBytes(hex);
console.log(back.length); // 32
```
## Usage: Advanced Example
```ts
import {
DriveTransportWebRtcError,
fileIdHexToBytes,
} from "@suaveplan/drive-transport-webrtc";
try {
fileIdHexToBytes("zzzz"); // too short AND non-hex
} catch (err) {
const e = err as DriveTransportWebRtcError;
console.log(e.code); // "DRIVE_TRANSPORT_WEBRTC.FRAME_INVALID"
}
```
## API Reference
- `fileIdBytesToHex(bytes: Uint8Array): string` — returns lower-case
64-char hex. Throws `DriveTransportWebRtcError(FRAME_INVALID)` if
`bytes.length !== 32`.
- `fileIdHexToBytes(hex: string): Uint8Array` — returns 32 raw bytes.
Throws `DriveTransportWebRtcError(FRAME_INVALID)` if the length is
wrong or the string is not valid hex.
## Implementation Notes
The hex codec is a thin pre-validation wrapper around
`@suaveplan/crypto-utils`. Strict length checks happen before the
delegating call so we surface a clear, well-typed error before
`hexToBytes` can throw its more generic parse error. The wrapping
keeps the throw site local to the transport — useful for stack-trace
hygiene when chunk requests fail at the boundary.

View File

@@ -0,0 +1,97 @@
# `transport/webrtc-transport/payload-codec`
## Purpose
Implements the byte-level encoder/decoder for the two structured
frame bodies the WebRTC drive transport carries: chunk-requests
(`fileId(32 raw bytes) ‖ chunkId(4)`) and advertisements
(`fileId(32 raw bytes) ‖ totalChunks(4) ‖ gzip(bitset)`). Frame headers
(length prefix + type byte) are handled by `framing/frame`; this
module operates purely on the body bytes after the type
discriminator has been consumed.
The public API takes / returns hex-encoded file ids to match the
canonical `DriveTransport` contract from `@suaveplan/drive-mesh`; the
on-the-wire representation remains the raw 32 bytes for compactness.
Hex ↔ bytes conversion happens inside this module via the sibling
`hex` helpers.
Keeping the payload codec separate from the frame codec lets callers
acting as chunk servers (i.e. peers fulfilling another peer's request
through this transport) decode requests without paying the cost of
re-validating the outer frame, and lets clients build advertisement
bodies independently of the gzip codec.
## Features
- `encodeChunkRequest(fileId: string, chunkId)` — `fileId(32) ‖ chunkId(4 BE)`.
- `decodeChunkRequest(payload)` — strict length check + uint32 read,
returns `fileId` as hex.
- `encodeAdvertisement(fileId: string, totalChunks, bitsetGzip)` —
prepends the file id and `totalChunks` to the gzipped bitset.
- `decodeAdvertisement(payload)` — splits the fixed-size header at
byte 36 (32-byte fileId + 4-byte totalChunks) and returns the
remaining bytes as `bitsetGzip`.
- All malformed inputs surface as
`DriveTransportWebRtcError(FRAME_INVALID)`.
## Usage: Basic Example
```ts
import {
encodeChunkRequest,
decodeChunkRequest,
} from "@suaveplan/drive-transport-webrtc";
const fileId = "ab".repeat(32);
const payload = encodeChunkRequest(fileId, 42);
const decoded = decodeChunkRequest(payload);
console.log(decoded.chunkId); // 42
console.log(decoded.fileId === fileId); // true
```
## Usage: Advanced Example
```ts
import {
decodeFrame,
decodeChunkRequest,
encodeChunkResponse,
FRAME_TYPE,
} from "@suaveplan/drive-transport-webrtc";
async function respondToChunkRequest(
frameBytes: Uint8Array,
channel: RTCDataChannel,
chunkStore: { get: (fid: string, cid: number) => Promise<Uint8Array> }
) {
const frame = decodeFrame(frameBytes);
if (frame.type !== FRAME_TYPE.CHUNK_REQUEST) return;
const { fileId, chunkId } = decodeChunkRequest(frame.payload);
const bytes = await chunkStore.get(fileId, chunkId);
channel.send(encodeChunkResponse(bytes));
}
```
## API Reference
- `encodeChunkRequest(fileId: string, chunkId: number): Uint8Array` —
rejects non-64-char-hex `fileId` and out-of-range `chunkId`.
- `decodeChunkRequest(payload: Uint8Array): { fileId: string; chunkId: number }` —
rejects payloads whose length is not exactly 36.
- `encodeAdvertisement(fileId: string, totalChunks: number, bitsetGzip: Uint8Array): Uint8Array` —
rejects non-uint32 `totalChunks` and empty `bitsetGzip`.
- `decodeAdvertisement(payload: Uint8Array): { fileId: string; totalChunks: number; bitsetGzip: Uint8Array }` —
rejects payloads ≤ 36 bytes.
## Implementation Notes
The codec uses `DataView.setUint32` / `getUint32` with `littleEndian = false`
so the wire form matches the standard big-endian network byte order.
Every encoder allocates a fresh `Uint8Array` rather than reusing
caller-provided buffers, which keeps the cost model predictable for
upstream callers building advertisement frames in a hot loop.
The advertisement layout deliberately places `totalChunks` ahead of
the gzipped bitset so a streaming decoder can short-circuit on
`totalChunks` mismatches before paying the decompression cost.

View File

@@ -0,0 +1,152 @@
# `transport/webrtc-transport/webrtc-transport`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the canonical `DriveTransport` interface (defined in
`@suaveplan/drive-mesh`) over a `@suaveplan/drive-webrtc`
`WebRTCClient` + `DataChannelManager` pair. Wraps a single named
data-channel label per peer with a length-prefixed binary framing
protocol carrying three frame types — chunk-request, chunk-response,
and advertisement — and threads `AbortSignal` semantics through every
public boundary.
The transport does not own the underlying `RTCPeerConnection`
lifecycle: the caller passes in an already-constructed
`WebRTCClient`, which means signalling, ICE candidates, and offer /
answer exchange all happen out-of-band before any chunk traffic
flows. Once a data-channel for the configured label is open, the
transport multiplexes requests and advertisements over it.
## Features
- `id: "drive-transport-webrtc"` — stable transport identifier
consumed by the mesh's transport-selection policy.
- `requestChunk(peerId, fileId, chunkId, signal?)` returns a
`Promise<Uint8Array>` resolving with the chunk bytes on success.
Per-request timeout (default 10 s) and concurrent request cap
(default 16) are configurable. `fileId` is the hex-encoded 32-byte
identifier.
- `advertise(peerId, advert)` takes a single `ChunkAdvertisement`
object (`fileId` / `bitset` / `totalChunks`), gzips the bitset, and
ships an advertisement frame; the receiver's `onAdvertisement`
handlers fire with the gunzipped bytes.
- `onAdvertisement(handler)` registers a two-arg
`(peerId, advert: ChunkAdvertisement) => void` callback. Returns
an unsubscribe function.
- `peers()` yields every peer id with an open data-channel of the
configured label (no `PeerInfo` shape — drive-mesh resolves
per-peer state from its own `PeerRegistry`).
- `dispose()` is idempotent — tears down event listeners and rejects
every pending request with `ABORTED`. Optional under the interface
but always present.
- OTel surface: `drive.transport.webrtc.requests_total{outcome}`,
`advertisements_total{direction}`,
`request_duration_ms`, `frame_size_bytes`.
## Usage: Basic Example
```ts
import {
DataChannelManager,
WebRTCClient,
} from "@suaveplan/drive-webrtc";
import { WebRTCDriveTransport } from "@suaveplan/drive-transport-webrtc";
const client = new WebRTCClient({ peerId: "self", config: { signaling: { url: "wss://x" } } });
const channels = new DataChannelManager({ client });
const transport = new WebRTCDriveTransport({ client, dataChannels: channels });
const fileId = "ab".repeat(32);
const bytes = await transport.requestChunk("bob", fileId, 0);
console.log("got", bytes.length, "bytes");
```
## Usage: Advanced Example
```ts
import { WebRTCDriveTransport } from "@suaveplan/drive-transport-webrtc";
const transport = new WebRTCDriveTransport({
client,
dataChannels: channels,
defaultLabel: "mesh-chunks",
maxConcurrentRequests: 32,
requestTimeoutMs: 30_000,
});
const unsubscribe = transport.onAdvertisement((peerId, advert) => {
presenceIndex.update(peerId, advert.fileId, advert.bitset);
});
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(new Error("user navigated away")), 2_000);
try {
const bytes = await transport.requestChunk("bob", fileId, 17, ctrl.signal);
await sink.write(bytes);
} catch (err) {
// err.code in REQUEST_TIMEOUT | ABORTED | DATA_CHANNEL_CLOSED
}
await transport.advertise("bob", {
fileId,
bitset: ourBitset,
totalChunks: 1024,
});
await transport.dispose();
unsubscribe();
```
## API Reference
- `class WebRTCDriveTransport extends TypedEventEmitter<WebRTCTransportEventMap> implements DriveTransport`
— constructor takes `WebRTCDriveTransportOptions`.
- `id: "drive-transport-webrtc"` — stable transport identifier.
- `requestChunk(peerId, fileId, chunkId, signal?): Promise<Uint8Array>`
— resolves with the chunk bytes. Throws `REQUEST_TIMEOUT`, `ABORTED`,
`DATA_CHANNEL_CLOSED`, or `PEER_NOT_CONNECTED`.
- `advertise(peerId, advert: ChunkAdvertisement): Promise<void>` —
gzips and sends. `advert.fileId` is hex, `advert.bitset` is the raw
presence bitmap, `advert.totalChunks` is the chunk count baked
into the wire frame.
- `onAdvertisement(handler: (peerId, advert) => void): () => void` —
returns unsubscribe.
- `peers(): Iterable<PeerId>` — yields a peer id per open channel
matching the configured label.
- `dispose(): Promise<void>` — idempotent teardown.
- `encodeChunkResponse(bytes)` — convenience encoder for callers
serving as chunk servers.
## Implementation Notes
The transport does not encode an explicit request id on the wire;
instead, it FIFO-matches incoming chunk-response frames against the
oldest pending request to the responding peer. This matches the
proposal's "one outstanding request per peer per label" contract and
saves bytes on every request frame. If a future variant needs
parallel requests against the same peer, the wire format can grow a
request-id field and the matching policy widens accordingly without
breaking the public TypeScript surface.
`fileId` is hex on every public boundary (matching the canonical
`DriveTransport` interface), but the wire frames still carry the 32
raw bytes for compactness. The `hex` module at `./hex.ts` performs the
boundary conversion and surfaces malformed input as
`DriveTransportWebRtcError(FRAME_INVALID)`.
The advertisement payload now includes a 4-byte big-endian
`totalChunks` field after `fileId` and before the gzipped bitset.
This brings the wire frame in line with `ChunkAdvertisement` so the
receiver can size its presence index without a separate negotiation
round-trip.
Data-channel sends use the buffer slice
`frame.buffer.slice(byteOffset, byteOffset + byteLength)` so that
sub-array views of larger backing buffers are not accidentally
transmitted in full. The transport delegates buffered-amount
backpressure entirely to `DataChannelManager.send`, which awaits
`bufferedamountlow` when the high-water mark is exceeded.