Files
suaveplan/openspec/specs/drive-transport-webrtc/spec.md
2026-05-27 23:29:53 +01:00

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.