113 lines
4.5 KiB
Markdown
113 lines
4.5 KiB
Markdown
# @suaveplan/drive-transport-webrtc — WebRTC DriveTransport
|
|
|
|
WebRTC datachannel implementation of the `DriveTransport` interface for
|
|
browser-to-browser chunk transfer. Composes `WebRTCClient` +
|
|
`DataChannelManager` from `@suaveplan/drive-webrtc`; consumed by
|
|
`@suaveplan/drive-mesh` alongside `drive-transport-https` as one of the
|
|
pluggable wire transports.
|
|
|
|
## Requirements
|
|
|
|
### Requirement: DriveTransport contract
|
|
|
|
The package SHALL expose `WebRTCDriveTransport` implementing the
|
|
structural `DriveTransport` interface:
|
|
|
|
- `name: "drive-transport-webrtc"`
|
|
- `requestChunk(peerId, fileId, chunkId, signal?): AsyncIterable<Uint8Array>`
|
|
- `advertise(peerId, fileId, bitset): Promise<void>`
|
|
- `onAdvertisement(handler): () => void`
|
|
- `peers(): Iterable<PeerInfo>`
|
|
- `dispose(): Promise<void>`
|
|
|
|
The local `DriveTransport` interface MUST remain structurally compatible
|
|
with the canonical one defined in `@suaveplan/drive-mesh` so consumers can
|
|
swap to the mesh-canonical type without code changes when mesh lands.
|
|
|
|
### Requirement: Wire framing
|
|
|
|
Each datachannel message SHALL be a length-prefixed binary frame:
|
|
|
|
```text
|
|
[4 B uint32 BE frameLen]
|
|
[1 B uint8 frameType] // 0x01 chunk-request, 0x02 chunk-response, 0x03 advertisement
|
|
[frameLen-1 B payload]
|
|
```
|
|
|
|
- `MAX_FRAME_BYTES = 16 MiB`; over-cap frames MUST be rejected with
|
|
`DriveTransportWebRtcError(FRAME_TOO_LARGE)`.
|
|
- Malformed frames (length mismatch, unknown type) MUST raise
|
|
`FRAME_INVALID`.
|
|
- Chunk request payload: `fileId(32) ‖ chunkId(4 BE uint32)`.
|
|
- Chunk response payload: raw chunk bytes (no extra length prefix — frame
|
|
already carries `frameLen`).
|
|
- Advertisement payload: `fileId(32) ‖ gzipped-bitset`. Compression uses
|
|
the platform `CompressionStream`/`DecompressionStream` API; the stream
|
|
ctors are injectable via `StreamCtorResolver` so tests cover missing-
|
|
stream branches without `vi.stubGlobal`.
|
|
|
|
### Requirement: requestChunk semantics
|
|
|
|
`requestChunk(peerId, fileId, chunkId, signal?)`:
|
|
|
|
1. Reuse existing channel via `dataChannels.get(peerId, defaultLabel)` or
|
|
`open(peerId, defaultLabel)`. Unknown peer → `PEER_NOT_CONNECTED`.
|
|
2. Encode + send chunk-request frame.
|
|
3. Wait for the next chunk-response frame from the same `(peerId, label)`
|
|
within `requestTimeoutMs`. AbortSignal cancels the wait + throws
|
|
`ABORTED`. Timeout throws `REQUEST_TIMEOUT`.
|
|
4. Yield the response payload as a single value of the
|
|
`AsyncIterable<Uint8Array>`.
|
|
5. Chunk-responses FIFO-match per peer — the wire format has no request-id
|
|
field. Documented in implementation notes as a known design constraint.
|
|
|
|
### Requirement: advertise + onAdvertisement
|
|
|
|
`advertise(peerId, fileId, bitset)`:
|
|
|
|
- Gzip the bitset via `CompressionStream`. Compression failure throws
|
|
`BITSET_COMPRESSION_FAILED`.
|
|
- Encode advertisement frame, send via `DataChannelManager.send`.
|
|
- Underlying send failure (channel closed) maps to `DATA_CHANNEL_CLOSED`.
|
|
|
|
`onAdvertisement(handler)` subscribes to `dataChannels.on("channel:message")`,
|
|
decodes incoming frames, invokes `handler` only for `frameType === 0x03`,
|
|
returns an unsubscribe function.
|
|
|
|
### Requirement: AbortSignal threading
|
|
|
|
Every async path (`requestChunk`, `advertise`, listener wait) SHALL accept
|
|
and honour an optional `AbortSignal`. Pre-aborted signals throw
|
|
`DriveTransportWebRtcError(ABORTED)` before any side effect.
|
|
|
|
### Requirement: dispose idempotency
|
|
|
|
`dispose()` SHALL unsubscribe from `DataChannelManager` listeners and
|
|
release internal state. It MUST be safe to call multiple times. It MUST NOT
|
|
close the underlying `WebRTCClient` — client lifetime is managed by the
|
|
parent (typically `WebRTCDrivePlugin` or `DriveMesh`).
|
|
|
|
### Requirement: OTel observability
|
|
|
|
The package SHALL register the following instruments under tracer
|
|
`@suaveplan/drive-transport-webrtc`:
|
|
|
|
- Counter `drive.transport.webrtc.requests_total{outcome}`
|
|
- Counter `drive.transport.webrtc.advertisements_total{direction}`
|
|
- Histogram `drive.transport.webrtc.request_duration_ms`
|
|
- Histogram `drive.transport.webrtc.frame_size_bytes`
|
|
|
|
### Requirement: Tests use injected real fakes
|
|
|
|
Tests MUST NOT use `vi.mock` or `vi.stubGlobal`. The
|
|
`FakeRTCPeerConnection` / `FakeRTCDataChannel` real-implementation pattern
|
|
from `@suaveplan/drive-webrtc` MUST be reused (structural `EventTarget`
|
|
subclasses with `simulate*` driver methods). All test imports come from
|
|
`@suaveplan/testing/runner`.
|
|
|
|
### Requirement: Coverage + gate
|
|
|
|
`bunx turbo lint typecheck test:coverage --filter=@suaveplan/drive-transport-webrtc`
|
|
MUST be green and istanbul coverage MUST be 100% on every touched module
|
|
before any release.
|