Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`).
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user