Files
2026-05-27 23:29:53 +01:00

135 lines
6.7 KiB
Markdown

# OpenSpec Change: drive-mesh-port
- **Change ID**: drive-mesh-port
- **Package**: `@suaveplan/drive-mesh` (new)
- **Status**: PROPOSED
- **Date**: 2026-04-28
## Summary
P2P mesh layer over the chunks plugin. `DriveMesh` orchestrates peer discovery, availability gossip (compressed bitsets per file), parallel chunk pull (multi-peer racing window), peer scoring, server fallback, and upload backpressure. Mesh consumes the `ChunkSource` interface returned by `@suaveplan/drive-plugin-chunks` — it does NOT import drive core. Mesh consumes one or more `DriveTransport` implementations (HTTPS, WebRTC, libp2p) for the wire.
## Why mesh is a separate package
A vault running on a server doesn't need mesh — it just wants `drive` + `storage-s3`. A browser session in the CDN scenario needs `drive` + chunks plugin + mesh + `drive-transport-webrtc` + `drive-transport-https` (server fallback). Bundling mesh into drive or chunks would push WebRTC peer code and Bloom-filter logic into every consumer.
## Trust model
- **Server is the root of trust.** Server signs every chunk and every manifest with its ed25519 key.
- **Peers are caches, never authorities.** Mesh treats every byte from a peer as untrusted until the chunks plugin's sig-verify passes.
- **Bad-sig peers are punished.** A peer that ever delivers a chunk with an invalid server signature is added to a deny list (configurable: per-session or persistent).
- Mesh never holds the file decryption key — that's in the manifest, decrypted client-side via the recipient's key.
## API surface
```ts
const mesh = new DriveMesh({
chunks, // ChunkSource handle from chunksPlugin
transports: [webrtcTransport, httpsTransport],
serverPubKey: Uint8Array,
serverFallback: { url: string, transport: "https" },
scheduler: { concurrency: 8, perChunkTimeoutMs: 4000, racingWindowMs: 200 },
peerPolicy: { maxConcurrentUploadsPerPeer: 4, blocklistOnBadSig: true },
rng: types.Rng,
clock: types.Clock,
});
await mesh.start(); // open transports, begin advertising
await mesh.fetchFile(fileId); // resolves manifest, schedules missing chunks
await mesh.fetchChunks(fileId, [12, 13, 14]);
mesh.on("chunk:received", ({ fileId, chunkId, fromPeer }) => …);
await mesh.stop();
```
## Sub-systems
### 1. Peer index (`PeerIndex`)
Tracks live peers and their advertised availability per file (compressed bitset keyed by `fileId`), latency EMAs, error rates. **This is an index overlay, not a registry.** Peer identity and lifecycle events come from `network-plugin-registry` via the `NetworkBus`. The index only adds file-specific metadata on top.
```ts
// drive-mesh consumes registry events from the network layer
bus.on("peer:joined", ({ peerId, tier, latency }) => {
driveMesh.peerIndex.add(peerId, { tier, latency });
});
bus.on("peer:left", ({ peerId }) => {
driveMesh.peerIndex.remove(peerId);
});
```
Methods: `addPeer(peerId, transport, metadata)`, `removePeer`, `peersFor(fileId, chunkId)`.
### 2. Availability gossip (`AvailabilityIndex`)
- Each peer maintains a `BitSet` per known fileId of chunks it has.
- On chunk acquisition, peer pushes `advertise(fileId, BitSet)` to known peers via the transport.
- On peer-join, peer pulls `advertisement(fileId)` from new neighbours.
- Bitsets are gzipped on the wire.
### 3. Parallel-pull scheduler (`Scheduler`)
- Takes a list of `(fileId, chunkId)` requests.
- For each, picks top-K peers by score from `peersFor(fileId, chunkId)`.
- Issues request to the best peer; after `racingWindowMs`, issues a parallel request to the second-best (cancelled when the first arrives).
- Per-chunk timeout `perChunkTimeoutMs` triggers retry on next-best peer.
- Honours `concurrency` global cap.
- After all peer attempts exhaust, falls back to `serverFallback.url` via the HTTPS transport.
### 4. Peer scoring (`PeerScore`)
- `score = w_latency / latency_ema + w_success * success_rate - w_errors * error_rate`.
- Updated on every chunk-receive completion or failure.
- Sig-verify failure → permanent score = -∞ (effectively blocked).
### 5. Upload backpressure (`UploadGovernor`)
- Limits concurrent uploads per peer (`maxConcurrentUploadsPerPeer`).
- Honours WebRTC datachannel `bufferedAmount` watermark; pauses sends when high.
- Emits `upload:throttled` events for observability.
### 6. NetworkBus integration
`DriveMesh` listens to `network-plugin-registry` events on the `NetworkBus` to maintain its `PeerIndex` without duplicating peer lifecycle tracking:
```ts
bus.on("peer:joined", ({ peerId, tier, latency }) => {
driveMesh.peerIndex.add(peerId, { tier, latency });
});
bus.on("peer:left", ({ peerId }) => {
driveMesh.peerIndex.remove(peerId);
});
bus.on("peer:updated", ({ peerId, latency }) => {
driveMesh.peerIndex.updateLatency(peerId, latency);
});
```
This ensures that `drive-mesh` and `network-plugin-registry` share a single source of truth for peer identity. File-specific availability bitsets remain the responsibility of `drive-mesh`'s `AvailabilityIndex`.
### 7. Manifest bootstrap
Defaults to **manifest-as-chunk** (chunks plugin's default). Mesh requests `manifest:<fileId>` from peers like any other chunk; if unavailable, falls back to `serverFallback.url`. `mesh.fetchFile(fileId)` is an end-to-end happy-path that:
1. Fetch manifest (peer or server).
2. Verify manifest sig.
3. Diff manifest's chunk list against `chunks.hasChunks(fileId)`.
4. Schedule missing chunks via Scheduler.
5. Resolve when bitset is full.
## Error codes
`MESH_CODES = defineCodes("MESH", [PEER_NOT_FOUND, NO_PEERS_AVAILABLE, ALL_PEERS_FAILED, SERVER_FALLBACK_FAILED, MANIFEST_FETCH_FAILED, ABORTED, TRANSPORT_ERROR, PEER_BLOCKED] as const)`. `MeshError extends SuaveplanError<MeshErrorCode>`.
## Dependencies
- **Consumes:** `@suaveplan/drive-plugin-chunks` (interface only — `ChunkSource`), `@suaveplan/drive-transport-https` (server fallback), and any other registered `DriveTransport`. Plus `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/streams`, `@opentelemetry/api`, `zod`.
- **Consumes (NetworkBus events):** `@suaveplan/network-plugin-registry` (`peer:joined`, `peer:left`, `peer:updated`). Drive-mesh does NOT import the registry package directly; it listens to bus events.
- **Does NOT depend on:** `@suaveplan/drive` core.
- **Blocked on:** `drive-plugin-chunks-port`, at least one `drive-transport-*-port` (HTTPS minimum for fallback), `network-plugin-registry` (for peer identity events).
## Out of scope
- Wire-protocol implementations → `drive-transport-https-port`, `drive-transport-webrtc-port`.
- DHT-based peer discovery → future change `drive-mesh-discovery-dht`.
- BitSwap-style pairwise want-list reconciliation → optimisation in v2.
- Sharing primitives (envelope encryption, grant tokens) → `@suaveplan/drive-sharing` (separate change).