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,158 @@
# @suaveplan/drive-mesh - P2P mesh orchestrator for the drive chunks plugin. Peer registry, availability gossip, parallel-pull scheduler with racing window, server fallback, and bad-sig denylist. Consumes ChunkSource (drive-plugin-chunks) and DriveTransport (drive-transport-*).
## Summary
P2P mesh orchestrator over the drive chunks plugin. Consumes
`ChunkSource` (from `@suaveplan/drive-plugin-chunks`) and one or more
`DriveTransport` implementations (HTTPS, WebRTC, libp2p, in-memory).
Provides parallel chunk pull with a first-byte racing window, peer
scoring, bad-signature denylist, availability bitset gossip, and
server fallback.
The package is universal: zero browser- or React-specific code.
It composes on the server (Node) for vault / CDN sources, in the
browser for distributed clients, and inside test harnesses with
in-memory transports.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [error-codes](./src/error-codes/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_ - [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_ - [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_ - [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ - [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
## Quick start
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks, // ChunkSource from chunksPlugin
transports: [httpsTransport], // any DriveTransport-compatible impl
serverPubKey, // server's ed25519 public key
serverFallback: { url: "https://cdn.example.com/chunks", transport: "https" },
});
await mesh.start();
const chunkBytes = await mesh.requestChunk(fileIdHex, 0);
await mesh.stop();
```
## Layering
```text
┌──────────────────────────────────────────────────────────────┐
│ application (vault / drive) │
├──────────────────────────────────────────────────────────────┤
│ @suaveplan/drive-mesh (this package) │
│ peer registry · gossip · racing puller · server fallback │
├──────────────────────────────────────────────────────────────┤
│ @suaveplan/drive-mesh-protocol (envelopes + bitsets) │
├──────────────────────────────────────────────────────────────┤
│ @suaveplan/drive-plugin-chunks (sig-verify, manifest) │
├──────────────────────────────────────────────────────────────┤
│ DriveTransport implementations (HTTPS / WebRTC / libp2p) │
└──────────────────────────────────────────────────────────────┘
```
`DriveMesh` is the only public class most callers construct. The four
subsystems (`PeerRegistry`, `GossipEngine`, `ParallelPuller`,
`ServerFallback`) are exported under subpaths so advanced consumers
can compose them directly.
## Subpath table
| Subpath | What |
| ---------------------------------------- | ----------------------------------- |
| `@suaveplan/drive-mesh` | Root barrel — `DriveMesh` + types. |
| `@suaveplan/drive-mesh/mesh/drive-mesh` | `DriveMesh` class. |
| `@suaveplan/drive-mesh/peer-registry/peer-registry` | `PeerRegistry`. |
| `@suaveplan/drive-mesh/gossip/gossip-engine` | `GossipEngine`. |
| `@suaveplan/drive-mesh/scheduler/parallel-puller` | `ParallelPuller`. |
| `@suaveplan/drive-mesh/server-fallback/server-fallback` | `ServerFallback`. |
| `@suaveplan/drive-mesh/types` | All public types. |
| `@suaveplan/drive-mesh/schemas` | Zod schemas. |
| `@suaveplan/drive-mesh/errors` | `DRIVE_MESH_CODES`, `DriveMeshError`. |
## Trust model
The server's ed25519 public key (`serverPubKey`) is the only root of
trust. Every chunk returned by a peer or the fallback URL must pass
`verifyChunkSig` against this key before drive-mesh resolves its
`requestChunk` promise. A peer that ever returns a chunk with an
invalid signature is moved to a denylist — `denylistMode: "session"`
(default) clears the list on `mesh.stop()`; `"persistent"` survives
stops.
Mesh never holds file decryption keys. Plaintext only ever materialises
inside the chunks plugin's decrypt pipeline, which is keyed by the
recipient's per-vault key.
## Scoring formula
`PeerRegistry` ranks peers per `(fileId, chunkId)` using a weighted
sum:
```text
score = 2 · (100 / (rttMs + 1))
+ 3 · successRate
− 5 · errorRate
```
- `rttMs` is an EMA (α = 0.3) of observed chunk-fetch wall time.
- `successRate` and `errorRate` are over the lifetime of the registry
entry. Peers with no observations start at `successRate = 0.5`
(neutral) and `errorRate = 0`.
- Bad-sig peers are pinned at `-Infinity` so they never appear in
`rank()` output once denied.
## OpenTelemetry metrics
The mesh emits the following events; consumers wire them to whichever
OTel meter they prefer. Drive-mesh stays metric-emitter-agnostic so it
does not pin a meter provider on universal consumers.
| Event | Suggested metric | Type |
| ----------------- | -------------------------------------------------- | ----------- |
| `chunk:served` | `drive.mesh.chunks_served_total{outcome="peer"}` | counter |
| `chunk:fallback` | `drive.mesh.chunks_served_total{outcome="fallback"}` | counter |
| `chunk:failed` | `drive.mesh.chunks_served_total{outcome="fail"}` | counter |
| `chunk:served` | `drive.mesh.chunk_fetch_ms{outcome="peer"}` | histogram |
| `chunk:fallback` | `drive.mesh.chunk_fetch_ms{outcome="fallback"}` | histogram |
| `peer:joined` | `drive.mesh.peers_admitted_total` | counter |
| `peer:denied` | `drive.mesh.peers_denied_total{reason}` | counter |
| `gossip:sent` | `drive.mesh.gossip_size_bytes` | histogram |
## Error codes
| Code | When |
| ----------------------------- | ----------------------------------------------- |
| `DRIVE_MESH.INVALID_OPTIONS` | Construction or `fileId` shape rejected. |
| `DRIVE_MESH.NOT_STARTED` | `requestChunk` before `start()`. |
| `DRIVE_MESH.CHUNK_UNAVAILABLE`| No candidate peers and no fallback configured. |
| `DRIVE_MESH.SIG_VERIFY_FAILED`| Winning peer returned a chunk with bad sig. |
| `DRIVE_MESH.ALL_PEERS_FAILED` | Every candidate failed; no fallback configured. |
| `DRIVE_MESH.FALLBACK_FAILED` | Peers + fallback both failed. |
| `DRIVE_MESH.INVALID_BITSET` | `advertise` got bad `totalChunks`. |
| `DRIVE_MESH.TRANSPORT_FAILED` | Single-attempt transport failure (internal). |
| `DRIVE_MESH.PEER_DENIED` | Peer on denylist (manual operations). |
| `DRIVE_MESH.ABORTED` | Caller-supplied AbortSignal aborted. |
## License
MIT © Suaveplan

View File

@@ -0,0 +1,93 @@
# `error-codes.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the frozen error-code registry for `@suaveplan/drive-mesh` via
`defineCodes("DRIVE_MESH", …)` from `@suaveplan/error`. Every failure
that crosses a public API boundary in the mesh — peer denial, signature
verification failure, transport breakage, fallback exhaustion — surfaces
as a `DriveMeshError` whose `code` is one of the literals in
`DRIVE_MESH_CODES`. Callers pattern-match on `error.code` rather than
parsing free-form messages, which keeps log-shipping, alerting, and
retry policies stable across releases.
## Features
- Namespaced codes (`DRIVE_MESH.CHUNK_UNAVAILABLE`, etc).
- Frozen at module load — additions go through OpenSpec.
- `DriveMeshError` widens the protected base constructor to public so
feature modules can throw directly without subclassing further.
- Codes cover the full lifecycle: peer selection, signature verify,
transport failure, server fallback, configuration validation.
## Usage: Basic Example
```ts
import { DRIVE_MESH_CODES, DriveMeshError } from "@suaveplan/drive-mesh/errors";
throw new DriveMeshError(
DRIVE_MESH_CODES.SIG_VERIFY_FAILED,
"chunk signature did not validate against server pubkey"
);
```
## Usage: Advanced Example
```ts
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "@suaveplan/drive-mesh/errors";
try {
await mesh.requestChunk(fileId, chunkId);
} catch (err) {
if (err instanceof DriveMeshError) {
switch (err.code) {
case DRIVE_MESH_CODES.ALL_PEERS_FAILED:
// every peer attempted; falling back to server
break;
case DRIVE_MESH_CODES.SIG_VERIFY_FAILED:
// the responsible peer is now denylisted
break;
case DRIVE_MESH_CODES.ABORTED:
// caller aborted via AbortSignal
break;
default:
throw err;
}
} else {
throw err;
}
}
```
## API Reference
- `DRIVE_MESH_CODES` — `Readonly<Record<Reason, "DRIVE_MESH.<Reason>">>`
containing: `CHUNK_UNAVAILABLE`, `ALL_PEERS_FAILED`,
`SIG_VERIFY_FAILED`, `TRANSPORT_FAILED`, `FALLBACK_FAILED`,
`INVALID_BITSET`, `PEER_DENIED`, `INVALID_OPTIONS`, `ABORTED`,
`NOT_STARTED`.
- `DriveMeshErrorCode` — type alias for the union of values above.
- `DriveMeshError(code, message, options?)` — concrete error class
extending `SuaveplanError<DriveMeshErrorCode>`; supports the standard
`cause` chain via `SuaveplanErrorOptions`.
## Implementation Notes
The registry is closed: each new failure mode must be added via an
OpenSpec change and corresponding spec requirement. Because
`defineCodes` returns a frozen record, accidental in-process mutation
fails fast in development.
`DriveMeshError`'s constructor is a thin pass-through that widens the
base's `protected` constructor to `public`. The class exists so
`instanceof` checks distinguish mesh errors from generic
`SuaveplanError`s thrown by sibling packages (e.g.
`ChunksError`/`DmpError`). Causes propagated from those packages remain
inspectable through the standard `error.cause` chain.

View File

@@ -0,0 +1,116 @@
# `gossip-engine.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`GossipEngine` runs the bitset-gossip loop drive-mesh uses to keep
peers informed of local chunk availability. Two responsibilities:
(1) periodically push local availability to every connected peer,
and (2) accept inbound advertisements and forward them to the
`PeerRegistry`.
The engine is transport-agnostic — it iterates the configured
`DriveTransport[]` and uses each transport's `advertise` /
`onAdvertisement` surface. Gossip is intentionally best-effort: a
failed `advertise` call is swallowed and retried on the next tick.
Chunk-fetch failures, by contrast, drive the scoring + denylist
inside the registry.
## Features
- Periodic outbound tick (default 5_000 ms, configurable).
- Inbound handler wired automatically on `start`.
- `tick()` exposed for deterministic testing.
- Cleanup on `stop` removes every subscription and clears the timer.
- Pluggable `advertSource` so the mesh decides what bytes go on the
wire (the chunks plugin's `hasChunks` result + framing).
## Usage: Basic Example
```ts
import { GossipEngine } from "@suaveplan/drive-mesh/gossip/gossip-engine";
const gossip = new GossipEngine({
registry,
transports: [httpsTransport],
advertSource: fileId => buildAdvert(fileId),
trackedFiles: () => chunks.knownFiles(),
});
gossip.start();
// ...
gossip.stop();
```
## Usage: Advanced Example
```ts
import { GossipEngine } from "@suaveplan/drive-mesh/gossip/gossip-engine";
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
const gossip = new GossipEngine({
registry,
transports: [webrtcTransport, httpsTransport],
intervalMs: 1_000,
advertSource: async fileId => {
const bits = await chunks.hasChunks(fromHex(fileId));
const totalChunks = bits.length;
const raw = boolsToBytes(bits);
return {
fileId,
totalChunks,
bitset: encodeFramedBitset(raw, totalChunks),
};
},
trackedFiles: () => activeFiles,
onAdvertiseSent: (peerId, advert) =>
telemetry.emit("gossip:sent", { peerId, fileId: advert.fileId }),
onAdvertiseReceived: (peerId, advert) =>
telemetry.emit("gossip:received", { peerId, fileId: advert.fileId }),
});
gossip.start();
// Force-trigger a tick for an integration test:
await gossip.tick();
```
## API Reference
- `class GossipEngine`:
- `constructor(options: GossipEngineOptions)`.
- `start(): void` — wires inbound subscriptions + schedules the
outbound tick. Idempotent.
- `stop(): void` — cancels the tick and detaches subscriptions.
Idempotent.
- `tick(): Promise<void>` — run a single outbound pass; intended
for tests.
- `GossipEngineOptions`:
- `registry` — `PeerRegistry` instance.
- `transports` — `readonly DriveTransport[]`.
- `advertSource(fileId)` — returns the local advertisement or
`undefined` when nothing to advertise.
- `trackedFiles()` — generator of `fileId` hex strings.
- `intervalMs?` — outbound tick period; default `5_000`.
- `clock?` — defaults to `Date.now`-based clock.
- `onAdvertiseSent?(peerId, advert)` — observability hook.
- `onAdvertiseReceived?(peerId, advert)` — observability hook.
## Implementation Notes
Outbound fan-out iterates every peer reported by every transport.
Duplicate peers across transports receive duplicate advertisements,
which is fine: receivers idempotently overwrite the bitset and the
registry's `recordAvailability` is set-shaped.
The scheduling primitive is `setTimeout` rather than `setInterval` so
overlapping ticks cannot occur — the next tick is only scheduled
after the previous tick's promise resolves. `unref` is applied where
available (Node) so the event loop can drain in tests; browsers do
not implement `unref`, which is fine because they manage timers
through page lifecycle instead.
Inbound advertisements flow through `addOrUpdate` (updates
`lastSeen`) and `recordAvailability` (updates the bitset) so a peer
sending its first advertisement is admitted to the registry without
a separate `peer:joined` event being required.

View File

@@ -0,0 +1,74 @@
# `typed-event-emitter.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Internal helper wrapping `EventBus` from `@suaveplan/events` with a
tuple-shaped event map. The public mesh class extends this so call
sites can write `mesh.emit("chunk:served", payload)` rather than
wrapping every argument in an object literal.
The implementation is intentionally private to `@suaveplan/drive-mesh`
— the public API exposes only `on`, `off`, and `once`. Re-implementing
locally (rather than importing from `@suaveplan/drive-webrtc`) keeps
the universal dependency closure clean: drive-mesh must not pull
in WebRTC code.
## Features
- Strict typing of event names → argument tuples.
- Synchronous `emit` matching Node's `EventEmitter` semantics.
- `once` auto-unsubscribes after delivery.
- `removeAllListeners` clears every subscription across every event.
## Usage: Basic Example
```ts
import { TypedEventEmitter } from "./typed-event-emitter.js";
type Events = { hello: [string] };
const ee = new TypedEventEmitter<Events>();
ee.on("hello", name => console.log("hi", name));
ee.emit("hello", "world");
```
## Usage: Advanced Example
```ts
import { TypedEventEmitter } from "./typed-event-emitter.js";
type Events = {
"chunk:served": [{ readonly chunkId: number; readonly bytes: number }];
"chunk:failed": [{ readonly chunkId: number; readonly reason: string }];
};
const ee = new TypedEventEmitter<Events>();
const off = ee.on("chunk:served", e => console.log(e.chunkId, e.bytes));
ee.once("chunk:failed", e => console.error(e.reason));
ee.emit("chunk:served", { chunkId: 0, bytes: 1024 });
off();
ee.removeAllListeners();
```
## API Reference
- `EventArgsMap = Record<string, unknown[]>` — event-map shape.
- `class TypedEventEmitter<TMap>`:
- `on(event, handler): Unsubscribe`.
- `once(event, handler): Unsubscribe` — auto-unsubscribes on first
delivery.
- `off(event, handler): void`.
- `emit(event, ...args): boolean` — `true` iff at least one
subscriber received the event.
- `removeAllListeners(): void`.
## Implementation Notes
`EventBus` keeps payloads as a single value per event, so the wrapper
stores `args` (the tuple) as that value and spreads it back through
`handler(...args)` on dispatch. The auxiliary `#wrapped` map tracks
the unsubscribe closures so `off(event, handler)` removes the
correct registration even though the underlying bus only sees the
wrapping closure.

View File

@@ -0,0 +1,144 @@
# `drive-mesh.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DriveMesh` is the public face of `@suaveplan/drive-mesh`. It binds
the four supporting subsystems — `PeerRegistry`, `GossipEngine`,
`ParallelPuller`, and (optionally) `ServerFallback` — into a single
class with explicit lifecycle and a single chunk-fetch entry point.
The class is the only mesh component most consumers ever construct.
It validates options at the Zod boundary, wires telemetry hooks
between subsystems, and surfaces a typed event stream callers can
subscribe to. The chunks plugin's signature contract is upheld
end-to-end: every byte returned from `requestChunk` has passed
`verifyChunkSig` against the configured `serverPubKey`.
## Features
- Strict option validation at construction (`INVALID_OPTIONS`).
- Idempotent `start()` / `stop()` lifecycle.
- `requestChunk` racing-window scheduler with automatic fallback.
- `advertise` / `unadvertise` to publish local availability bitsets.
- Typed event emitter (`chunk:*`, `peer:*`, `gossip:*`).
- Read-only access to the registry (`mesh.registry`) for
`NetworkBus` consumers that wire `peer:joined`/`peer:left` events.
## Usage: Basic Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks,
transports: [httpsTransport],
serverPubKey,
});
await mesh.start();
const bytes = await mesh.requestChunk(fileIdHex, 0);
await mesh.stop();
```
## Usage: Advanced Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
const mesh = new DriveMesh({
chunks,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
serverFallback: { url: "https://cdn.example.com/chunks", transport: "https" },
maxParallelRequests: 16,
racingWindowMs: 150,
denylistMode: "persistent",
});
// Mirror NetworkBus events into the registry overlay.
networkBus.on("peer:joined", ({ peerId, transports }) =>
mesh.registry.addOrUpdate(peerId, { transports })
);
networkBus.on("peer:left", ({ peerId }) => mesh.registry.forget(peerId));
await mesh.start();
mesh.on("chunk:served", e =>
telemetry.counter("drive.mesh.chunks_served_total", 1, { outcome: "peer" })
);
mesh.on("chunk:fallback", e =>
telemetry.counter("drive.mesh.chunks_served_total", 1, {
outcome: "fallback",
})
);
mesh.on("peer:denied", e =>
telemetry.counter("drive.mesh.peers_denied_total", 1, { reason: e.reason })
);
const bits = await chunks.hasChunks(fileIdBytes);
const raw = boolsToBytes(bits);
mesh.advertise(fileIdHex, encodeFramedBitset(raw, bits.length), bits.length);
const bytes = await mesh.requestChunk(fileIdHex, 0);
await mesh.stop();
```
## API Reference
- `class DriveMesh extends TypedEventEmitter<DriveMeshEventMap>`:
- `constructor(options: DriveMeshOptions)` — Zod-validated.
- `start(signal?): Promise<void>` — idempotent.
- `stop(reason?): Promise<void>` — idempotent; clears registry per
`denylistMode`.
- `requestChunk(fileId, chunkId, signal?): Promise<Uint8Array>` —
racing-window peer pull → server fallback → throw.
- `advertise(fileId, bitset, totalChunks): void`.
- `unadvertise(fileId): void`.
- `peers(): Iterable<PeerInfo>`.
- `registry: PeerRegistry` (read-only getter).
- `tickGossip(): Promise<void>` — single gossip pass for tests.
- Inherited from `TypedEventEmitter`:
- `on(event, handler): Unsubscribe`.
- `once(event, handler): Unsubscribe`.
- `off(event, handler): void`.
- `removeAllListeners(): void`.
- Throws:
- `DRIVE_MESH.INVALID_OPTIONS` — bad construction args / bad
`fileId`.
- `DRIVE_MESH.NOT_STARTED` — `requestChunk` before `start()`.
- `DRIVE_MESH.CHUNK_UNAVAILABLE` — no candidates and no fallback.
- `DRIVE_MESH.SIG_VERIFY_FAILED` — winning peer was Byzantine.
- `DRIVE_MESH.ALL_PEERS_FAILED` — every candidate failed and no
fallback configured.
- `DRIVE_MESH.FALLBACK_FAILED` — peers + fallback both failed.
- `DRIVE_MESH.ABORTED` — caller-supplied signal aborted.
- `DRIVE_MESH.INVALID_BITSET` — `advertise` got bad totalChunks.
## Implementation Notes
`DriveMesh` does NOT register itself as a `NetworkBus` consumer
internally — it exposes `mesh.registry` so the bridge from
`network-plugin-registry`'s `peer:joined` / `peer:left` events lives
in application glue. This keeps the universal package free of a
dependency on `@suaveplan/network` / `@suaveplan/network-plugin-*`,
matching the layering pinned in `drive-network-integration`.
`requestChunk` deliberately does not retry on peers after a bad-sig
event from the winning attempt: the denylist update means the next
call will not even consider that peer. Re-trying inside a single
request would risk amplifying a Byzantine peer's influence over the
score formula. Instead, the fallback path runs.
Bad-sig events flow `puller → registry.recordBadSig → onBadSig
callback → mesh.emit("peer:denied")`. The registry update happens
synchronously inside the puller; the event is fired afterwards so
subscribers see the state-after-update.
`exactOptionalPropertyTypes: true` (per the repo's `tsconfig.base`)
forces the explicit `...(x !== undefined ? { x } : {})` spread shape
when forwarding optionals down into subsystem options — never
`{ x: maybeUndefined }` because that materialises an `undefined`
property the inner schema would reject.

View File

@@ -0,0 +1,112 @@
# `peer-registry.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PeerRegistry` is the in-memory overlay drive-mesh maintains on top of
the live peer set provided by `network-plugin-registry`. The registry
adds three concerns the network layer does not own: per-`fileId`
availability bitsets, an EMA-smoothed RTT tracker, and a composite
score the parallel puller uses to rank candidates.
The registry never originates peer-identity events. Peers enter via
`addOrUpdate` (called by the mesh in response to bus events) and exit
via `forget` (called when the network registry emits `peer:left`). The
denylist is owned by the registry because bad signatures are a
file-layer concern.
## Features
- Per-file availability bitsets stored as framed bytes (output of
`encodeFramedBitset` from `@suaveplan/drive-mesh-protocol`).
- EMA latency tracker (α = 0.3).
- Composite score: `2 * (100 / (rtt+1)) + 3 * successRate - 5 *
errorRate`. Weights documented in the README.
- Bad-sig denylist with two modes (`"session"` clears on stop;
`"persistent"` survives).
- `rank(fileId, chunkId)` returns scored candidates in
descending-score order, filtered to peers that claim availability of
the chunk.
## Usage: Basic Example
```ts
import { PeerRegistry } from "@suaveplan/drive-mesh/peer-registry/peer-registry";
const registry = new PeerRegistry();
registry.addOrUpdate("peerA", { transports: ["https"], rttMs: 80 });
registry.recordAvailability("peerA", "abcd…", framedBitset);
const candidates = registry.rank("abcd…", 7);
```
## Usage: Advanced Example
```ts
import { PeerRegistry } from "@suaveplan/drive-mesh/peer-registry/peer-registry";
const registry = new PeerRegistry({
clock: { now: () => fakeClock.now() },
denylistMode: "persistent",
});
registry.addOrUpdate("peerA", { transports: ["webrtc", "https"] });
registry.recordRtt("peerA", 50);
registry.recordSuccess("peerA");
registry.recordSuccess("peerA");
registry.addOrUpdate("peerB", { transports: ["https"] });
registry.recordRtt("peerB", 400);
registry.recordFailure("peerB");
const top = registry.rank("file-id-hex", -1);
// peerA outranks peerB because of lower RTT and higher success rate.
registry.recordBadSig("peerB");
console.log(registry.isDenied("peerB")); // true
console.log(registry.denialReason("peerB")); // "bad_sig"
```
## API Reference
- `class PeerRegistry`:
- `constructor(options?: { clock?; denylistMode? })`.
- `addOrUpdate(peerId, update?): PeerInfo` — idempotent insertion /
merge.
- `forget(peerId): void`.
- `forgetAll(): void` — clears the registry; clears the denylist
iff `denylistMode === "session"`.
- `recordAvailability(peerId, fileId, framedBitset): void`.
- `recordRtt(peerId, rttMs): void` — α=0.3 EMA.
- `recordSuccess(peerId): void`.
- `recordFailure(peerId): void`.
- `recordBadSig(peerId): void` — moves to denylist
(`reason="bad_sig"`).
- `deny(peerId, reason): void` — denylist with explicit reason.
- `removeFromDenylist(peerId): void`.
- `isDenied(peerId): boolean`.
- `denialReason(peerId): PeerDenialReason | undefined`.
- `peers(): IterableIterator<PeerInfo>`.
- `rank(fileId, chunkId): PeerInfo[]` — descending-score order.
- `get(peerId): PeerInfo | undefined`.
## Implementation Notes
`addOrUpdate` returns the post-merge snapshot rather than `void` so
the mesh can immediately emit `peer:joined`/`peer:scored` events with
the canonical shape without re-querying. Snapshots are fresh objects
on every call so callers may not assume reference equality.
`rank(fileId, -1)` is a sentinel meaning "any peer with any
availability for the file" — the gossip engine uses it to fan-out
advertisements without requiring a specific chunk. The negative path
is the only branch that skips the bitset bit-check, which keeps the
hot-path tight.
The score formula is intentionally simple: weighted-sum of latency,
success rate, and error rate. The weights are tuned so that a peer
with double the RTT of another remains preferred if it has a higher
success rate, which matches the drive-webrtc-era tuning. Bad-sig
peers are pinned at `-Infinity` so they sort to the bottom of every
ranking without special-casing the consumer.

View File

@@ -0,0 +1,126 @@
# `parallel-puller.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ParallelPuller` is the chunk-fetch scheduler that turns "I want chunk
N of file F" into a verified `Uint8Array`. The puller asks the
`PeerRegistry` for ranked candidates, issues a request to the best
peer, waits `racingWindowMs`, then issues parallel requests to the
runners-up. The first response whose server signature verifies wins;
the other in-flight attempts are aborted via a shared
`AbortController`.
The puller is the mesh's primary trust enforcement point. Every byte
returned from a transport flows through `verifyChunkSig` before the
puller acknowledges success. A signature failure moves the offending
peer to the registry's denylist with `score = -Infinity` and surfaces
`DriveMeshError(SIG_VERIFY_FAILED)` — the orchestrator can choose to
fall back to the server rather than retry on peers.
## Features
- Top-K candidate selection (default 3).
- Racing-window staggered issuance (default 250 ms).
- Shared `AbortController` short-circuits losing attempts.
- Mandatory `verifyChunkSig` gate against the configured
`serverPubKey`.
- Bad-sig denylisting via `registry.recordBadSig`.
- Non-signature failures update success / failure counters so
ranking adapts.
- `maxParallelRequests` global cap.
## Usage: Basic Example
```ts
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
const puller = new ParallelPuller({
registry,
transports: [httpsTransport],
serverPubKey,
});
const bytes = await puller.pull("file-id-hex", 0);
```
## Usage: Advanced Example
```ts
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
const puller = new ParallelPuller({
registry,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
racingWindowMs: 150,
topK: 4,
maxParallelRequests: 16,
clock: fakeClock,
onAttempt: (peerId, fileId, chunkId) =>
log.debug("attempt", peerId, fileId, chunkId),
onSuccess: (peerId, _f, _c, bytes, elapsedMs) =>
log.info("served", peerId, bytes, elapsedMs),
onBadSig: peerId => log.warn("denylisting", peerId),
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
try {
const bytes = await puller.pull(fileId, chunkId, controller.signal);
} catch (err) {
if (err instanceof DriveMeshError && err.code === DRIVE_MESH_CODES.ABORTED) {
// caller-driven cancellation
}
}
```
## API Reference
- `class ParallelPuller`:
- `constructor(options: ParallelPullerOptions)`.
- `pull(fileId, chunkId, signal?): Promise<Uint8Array>`.
- `inflight: number` (read-only) — in-flight request count.
- `ParallelPullerOptions`:
- `registry`, `transports`, `serverPubKey` (required).
- `maxParallelRequests?` (default `8`).
- `racingWindowMs?` (default `250`).
- `topK?` (default `3`).
- `clock?`.
- `onAttempt?(peerId, fileId, chunkId)`.
- `onSuccess?(peerId, fileId, chunkId, bytes, elapsedMs)`.
- `onFailure?(peerId, fileId, chunkId, reason)`.
- `onBadSig?(peerId, fileId, chunkId)`.
- Throws:
- `DRIVE_MESH.CHUNK_UNAVAILABLE` — no candidate peers.
- `DRIVE_MESH.SIG_VERIFY_FAILED` — winning peer returned bad sig.
- `DRIVE_MESH.TRANSPORT_FAILED` — single-attempt transport failure.
- `DRIVE_MESH.ALL_PEERS_FAILED` — every candidate failed.
- `DRIVE_MESH.ABORTED` — caller-provided signal aborted.
## Implementation Notes
`firstSettled` is hand-rolled rather than using `Promise.any`:
`Promise.any` resolves on first success but does NOT abort losing
attempts, leaving them in flight wastefully. The shared
`AbortController` is the cancellation primitive — `controller.abort()`
fires both when a winner appears and when the outer caller aborts.
`SIG_VERIFY_FAILED` short-circuits the "all peers failed" wrapping:
the orchestrator distinguishes "every peer failed transport" (try
server fallback) from "winning peer was Byzantine" (denylist + retry
on a different peer). Re-wrapping the bad-sig error inside
`ALL_PEERS_FAILED` would lose that distinction.
`maxParallelRequests` is enforced by a yielding micro-loop in
`#awaitSlot`. The loop is bounded by the number of concurrent `pull`
calls — drive-mesh issues `pull` from a small, controlled set of
async contexts so the loop never spins indefinitely. Under heavier
fan-in the orchestrator should queue requests in front of the puller
rather than issuing every chunk concurrently.
Bad-sig denylist is permanent within the registry's denylist scope:
`session` (default) clears on `mesh.stop()`, `persistent` survives.
The puller does not concern itself with scope — the registry is the
authority.

View File

@@ -0,0 +1,105 @@
# `server-fallback.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ServerFallback` is the chunk-fetch path drive-mesh uses when every
peer attempt in `ParallelPuller` has failed. The server is the root of
trust, so a fallback request must still produce a chunk that passes
`verifyChunkSig` against the configured `serverPubKey`. The fallback
does not bypass trust — it bypasses peers.
The fallback URL is composed as `<baseUrl>/<fileId>/<chunkId>` and
fetched via the injectable `fetchFn`. Failures are retried with
exponential backoff and equal jitter so a wave of mesh nodes
simultaneously falling back does not stampede the origin.
## Features
- Plain `GET <baseUrl>/<fileId>/<chunkId>` request shape.
- Equal-jitter exponential backoff
(`exp/2 + rand(0, exp/2)`, clamped `[base, max]`).
- Mandatory signature verification against `serverPubKey`.
- AbortSignal threaded through `fetch` and through delay waits.
- Configurable retry count (`maxAttempts`, default 3).
## Usage: Basic Example
```ts
import { ServerFallback } from "@suaveplan/drive-mesh/server-fallback/server-fallback";
const fallback = new ServerFallback({
baseUrl: "https://cdn.example.com/chunks",
serverPubKey,
});
const bytes = await fallback.fetchChunk(fileId, 0);
```
## Usage: Advanced Example
```ts
import { ServerFallback } from "@suaveplan/drive-mesh/server-fallback/server-fallback";
const fallback = new ServerFallback({
baseUrl: "https://cdn.example.com/chunks",
serverPubKey,
fetchFn: customFetch,
maxAttempts: 5,
backoffBaseMs: 100,
backoffMaxMs: 3_000,
clock: fakeClock,
random: deterministicRng,
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
const bytes = await fallback.fetchChunk(fileId, chunkId, controller.signal);
} catch (err) {
if (err instanceof DriveMeshError) {
if (err.code === DRIVE_MESH_CODES.FALLBACK_FAILED) {
// every retry exhausted; surface to the caller
}
}
}
```
## API Reference
- `class ServerFallback`:
- `constructor(options: ServerFallbackOptions)`.
- `fetchChunk(fileId, chunkId, signal?): Promise<Uint8Array>`.
- `now(): number` — current clock reading.
- `ServerFallbackOptions`:
- `baseUrl` (required) — trailing slashes stripped.
- `serverPubKey` (required) — ed25519 32-byte public key.
- `fetchFn?` — defaults to `globalThis.fetch`; throws
`FALLBACK_FAILED` lazily if missing.
- `maxAttempts?` (default `3`).
- `backoffBaseMs?` (default `200`).
- `backoffMaxMs?` (default `5_000`).
- `clock?` — defaults to `Date.now`-based clock.
- `random?` — `() => number` in `[0, 1)`; default `Math.random`.
## Implementation Notes
The path shape (`<base>/<fileId>/<chunkId>`) is intentionally simple
rather than templated — drive-mesh consumers usually front the
fallback URL with a CDN that knows the layout. Customers who want
shaped URLs (e.g., reverse-id sharding per `drive-template`) can
wrap `fetchFn` with their own URL builder.
`verifyChunkSig` is called on every successful response before
returning bytes. A signature failure in the fallback path is a
configuration error, not a transient failure — but for symmetry with
the peer path the error still surfaces as `FALLBACK_FAILED` rather
than `SIG_VERIFY_FAILED`, so that the caller's denylist counter
remains a "peer was Byzantine" counter rather than "server was
Byzantine".
`defaultFetch` returns a lazy `globalThis.fetch` bridge so the
package remains universal: a Node ≥18 / Deno / browser runtime
ships `fetch` natively, and consumers that build to older targets
inject `fetchFn` explicitly.

View File

@@ -0,0 +1,86 @@
# `config.types.ts`
## Purpose
Defines the `DriveMeshOptions` shape consumed by `new DriveMesh(...)`.
The interface is the TypeScript-side contract; the matching Zod
boundary lives in `./schemas/config.schemas.ts` and is the runtime
validator that rejects malformed input before the mesh's internal
state is constructed.
Splitting the two halves keeps the type surface dependency-free at
compile time and the runtime validation centralised at the package
boundary.
## Features
- Single shape covering scheduler tuning, denylist policy, server
fallback, transports, and the `ChunkSource` handle.
- Every tuning parameter is optional with a documented default.
- `serverPubKey` is mandatory — trust roots are not optional.
- Injectable `clock` and `fetchFn` for deterministic testing.
## Usage: Basic Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks,
transports: [httpsTransport],
serverPubKey,
});
```
## Usage: Advanced Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
serverFallback: { url: "https://cdn.example.com", transport: "https" },
maxParallelRequests: 16,
racingWindowMs: 150,
denylistMode: "persistent",
clock: fakeClock,
fetchFn: testFetch,
});
await mesh.start();
const chunkBytes = await mesh.requestChunk(fileIdHex, 0);
```
## API Reference
- `DenylistMode = "session" | "persistent"` — controls how long
bad-sig peers stay denylisted. `session` clears on `mesh.stop()`;
`persistent` keeps the denylist across stops.
- `ServerFallbackConfig`:
- `url: string` — absolute URL the fallback path hits.
- `transport: string` — `DriveTransport.id` carrying the fallback.
- `DriveMeshOptions`:
- `chunks: ChunkSource` — handle returned by `chunksPlugin`.
- `transports: readonly DriveTransport[]` — injectable transports.
- `serverPubKey: Uint8Array` — server's ed25519 public key.
- `serverFallback?: ServerFallbackConfig` — optional fallback.
- `maxParallelRequests?: number` — default `8`.
- `racingWindowMs?: number` — default `250`.
- `denylistMode?: DenylistMode` — default `"session"`.
- `clock?: Clock` — defaults to `Date.now`-based clock.
- `fetchFn?: typeof fetch` — defaults to `globalThis.fetch`.
## Implementation Notes
`fetchFn` is typed loosely as `(input, init?) => Promise<Response>`
rather than reusing `typeof fetch`, because `typeof fetch` carries a
DOM-specific overload set that fights the universal package
target. The looser shape still binds `globalThis.fetch` correctly at
runtime.
`serverPubKey` is required — there is no implicit default. Drive-mesh
treats peers as untrusted caches, so a configuration with no root of
trust would silently disable signature verification, which is a footgun
the type system prevents.

View File

@@ -0,0 +1,118 @@
# `drive-transport.types.ts`
## Purpose
Defines the chunk-shaped `DriveTransport` contract every mesh transport
implementation conforms to. This is a structural ("ducked") interface so
the mesh can compose HTTPS, WebRTC, libp2p, or in-memory transports
without depending on their concrete classes. Drive-mesh consumes the
contract; transport packages implement it.
The contract is intentionally narrow: a transport answers chunk
requests, pushes advertisements, surfaces inbound advertisements via a
handler, and reports the live peer set. Everything else (handshake,
authentication, replay protection) belongs inside the transport and
never crosses this boundary.
## Features
- Structural interface — no inheritance required.
- Async `requestChunk` returns the full chunk bytes (locked v0x0001
format) so the mesh can hand them directly to
`verifyChunkSig` and `parseChunk`.
- `advertise` is `peerId`-targeted — broadcasts are open-coded by the
gossip engine via repeated unicast.
- `onAdvertisement` returns an unsubscribe; multiple subscribers are
allowed.
- Optional `dispose()` honours `DriveMesh.stop()` lifecycle.
## Usage: Basic Example
```ts
import type { DriveTransport } from "@suaveplan/drive-mesh/types";
const transport: DriveTransport = {
id: "https",
async requestChunk(peerId, fileId, chunkId) {
return await myHttpsClient.get(peerId, fileId, chunkId);
},
async advertise(peerId, advert) {
await myHttpsClient.push(peerId, advert);
},
onAdvertisement(handler) {
const unsub = myHttpsClient.subscribe(handler);
return unsub;
},
peers() {
return myHttpsClient.connectedPeers();
},
};
```
## Usage: Advanced Example
```ts
import type { DriveTransport } from "@suaveplan/drive-mesh/types";
import { DriveMesh } from "@suaveplan/drive-mesh";
class WebRtcTransport implements DriveTransport {
public readonly id = "webrtc";
readonly #handlers = new Set<AdvertisementHandler>();
public async requestChunk(peerId, fileId, chunkId, signal) {
const chan = this.channelFor(peerId);
const reply = await chan.request({ fileId, chunkId }, { signal });
return reply.bytes;
}
public async advertise(peerId, advert) {
this.channelFor(peerId).send({ kind: "advert", ...advert });
}
public onAdvertisement(handler) {
this.#handlers.add(handler);
return () => this.#handlers.delete(handler);
}
public peers() {
return this.connectedChannels.keys();
}
public async dispose() {
for (const chan of this.connectedChannels.values()) chan.close();
}
}
const mesh = new DriveMesh({
chunks,
transports: [new WebRtcTransport(), httpsTransport],
serverPubKey,
});
```
## API Reference
- `ChunkAdvertisement { fileId: string; bitset: Uint8Array; totalChunks: number }`
— payload pushed in `advertise`; bitset is framed via
`encodeFramedBitset` from `@suaveplan/drive-mesh-protocol`.
- `AdvertisementHandler = (peerId, advert) => void` — callback invoked
for every inbound advertisement.
- `Unsubscribe = () => void` — handle returned by `onAdvertisement`.
- `DriveTransport`:
- `id: string` — stable identifier used by `serverFallback.transport`.
- `requestChunk(peerId, fileId, chunkId, signal?): Promise<Uint8Array>` —
fetch chunk bytes from one peer.
- `advertise(peerId, advert): Promise<void>` — unicast availability
push.
- `onAdvertisement(handler): Unsubscribe` — subscribe to inbound
advertisements.
- `peers(): Iterable<PeerId>` — live peer snapshot.
- `dispose?(): Promise<void> | void` — optional teardown.
## Implementation Notes
The contract is deliberately bytes-in / bytes-out: the mesh never
inspects payload metadata before signature verification, which keeps
trust boundaries explicit. Signature verification is performed by the
mesh against the configured `serverPubKey` before the chunk is handed
to the chunks plugin.
`peers()` returns an `Iterable<PeerId>` (not an array) so transports
can stream live views without materialising arrays on every gossip
tick. Implementations are free to return a `Set` or `Map.keys()`.

View File

@@ -0,0 +1,105 @@
# `events.types.ts`
## Purpose
Defines the tuple-shaped event map emitted by `DriveMesh`. The map
follows the same `Record<string, unknown[]>` convention as
`@suaveplan/drive-webrtc`'s `TypedEventEmitter`, so call sites read
`mesh.emit("chunk:served", payload)` rather than wrapping arguments in
an object literal on every emission.
Events are organised in three families: chunk lifecycle (`chunk:*`),
peer lifecycle (`peer:*`), and gossip telemetry (`gossip:*`). Each
event payload is fully typed so downstream subscribers get
compile-time validation of the property names they touch.
## Features
- Strict typing — `mesh.on("chunk:served", e => …)` infers
`ChunkServedEvent`.
- Stable shape: every event carries the contextual identifiers
(`fileId`, `chunkId`, `peerId` where applicable) needed to correlate
it with the request that triggered it.
- Latency observability via `elapsedMs` on chunk events.
## Usage: Basic Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
mesh.on("chunk:served", ({ fileId, chunkId, peerId, elapsedMs }) => {
console.log("hit", fileId, chunkId, peerId, elapsedMs);
});
mesh.on("chunk:fallback", ({ fileId, chunkId }) => {
console.log("fallback", fileId, chunkId);
});
```
## Usage: Advanced Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
import type { DriveMeshEventMap } from "@suaveplan/drive-mesh/types";
function wire(mesh: DriveMesh, sink: TelemetrySink) {
const map: { [K in keyof DriveMeshEventMap]: () => void } = {
"chunk:requested": mesh.on("chunk:requested", e =>
sink.counter("drive.mesh.chunk_requested_total", 1, e)
),
"chunk:served": mesh.on("chunk:served", e => {
sink.counter("drive.mesh.chunk_served_total", 1, { outcome: "peer" });
sink.histogram("drive.mesh.chunk_fetch_ms", e.elapsedMs, e);
}),
"chunk:fallback": mesh.on("chunk:fallback", e =>
sink.counter("drive.mesh.chunk_served_total", 1, { outcome: "fallback" })
),
"chunk:failed": mesh.on("chunk:failed", e =>
sink.counter("drive.mesh.chunk_served_total", 1, { outcome: "fail" })
),
"peer:joined": mesh.on("peer:joined", () =>
sink.counter("drive.mesh.peers_admitted_total", 1)
),
"peer:left": mesh.on("peer:left", () => {}),
"peer:denied": mesh.on("peer:denied", e =>
sink.counter("drive.mesh.peers_denied_total", 1, { reason: e.reason })
),
"peer:scored": mesh.on("peer:scored", () => {}),
"gossip:sent": mesh.on("gossip:sent", e =>
sink.histogram("drive.mesh.gossip_size_bytes", e.sizeBytes)
),
"gossip:received": mesh.on("gossip:received", () => {}),
};
return () => Object.values(map).forEach(off => off());
}
```
## API Reference
- `DriveMeshEventMap` — `Record<EventName, [PayloadType]>` consumed by
`TypedEventEmitter`.
- Chunk lifecycle:
- `ChunkRequestedEvent { fileId; chunkId; peerId }`.
- `ChunkServedEvent { fileId; chunkId; peerId; bytes; elapsedMs }`.
- `ChunkFallbackEvent { fileId; chunkId; bytes; elapsedMs }`.
- `ChunkFailedEvent { fileId; chunkId; reason }`.
- Peer lifecycle:
- `PeerJoinedEvent { peerId; transports }`.
- `PeerLeftEvent { peerId }`.
- `PeerDeniedEvent { peerId; reason }`.
- `PeerScoredEvent { peerId; score; rttMs }`.
- Gossip telemetry:
- `GossipSentEvent { peerId; fileId; sizeBytes }`.
- `GossipReceivedEvent { peerId; fileId; sizeBytes }`.
## Implementation Notes
Every payload is a single object rather than a positional tuple of
primitives. This is intentional: object payloads survive future
schema growth without changing the call signature at every subscriber
site.
`elapsedMs` is measured against the injectable `clock` so tests
running under a `FakeClock` get deterministic latency numbers.
`bytes` is always the size of the verified ciphertext, not the wire
payload, so consumers can compare against the file size in the
manifest without double-counting headers.

View File

@@ -0,0 +1,108 @@
# `peer.types.ts`
## Purpose
Defines the `PeerInfo` overlay tracked by the drive-mesh peer registry.
Drive-mesh does not own peer identity — that comes from
`network-plugin-registry` via the `NetworkBus` (see
`drive-network-integration` spec). What drive-mesh adds on top is
file-specific state: which chunks of which files a peer claims to have,
and a composite score derived from observed latency / success / error
rates.
The `PeerInfo` shape is intentionally read-only at every nested level
so consumers can hand snapshots to telemetry / dashboards without
worrying about external mutation.
## Features
- File-keyed availability bitsets (one per `fileId`).
- EMA latency tracker surfaced as `rttMs`.
- Composite `score: number`; `-Infinity` is the denylist sentinel.
- Multi-transport tracking — same `peerId` may be reachable via
multiple wires.
## Usage: Basic Example
```ts
import type { PeerInfo } from "@suaveplan/drive-mesh/types";
function logTop(peers: Iterable<PeerInfo>) {
const ranked = [...peers].sort((a, b) => b.score - a.score);
for (const peer of ranked.slice(0, 5)) {
console.log(peer.peerId, peer.score, peer.rttMs);
}
}
```
## Usage: Advanced Example
```ts
import type { PeerInfo, PeerDenialReason } from "@suaveplan/drive-mesh/types";
import { DRIVE_MESH_CODES, DriveMeshError } from "@suaveplan/drive-mesh/errors";
function ensureReachable(peer: PeerInfo): void {
if (peer.score === Number.NEGATIVE_INFINITY) {
throw new DriveMeshError(
DRIVE_MESH_CODES.PEER_DENIED,
`${peer.peerId} denylisted`
);
}
if (peer.transports.length === 0) {
throw new DriveMeshError(
DRIVE_MESH_CODES.TRANSPORT_FAILED,
`${peer.peerId} has no reachable transport`
);
}
}
function fileCoverage(peer: PeerInfo, fileId: string): number {
const bits = peer.availability.get(fileId);
if (bits === undefined) return 0;
let count = 0;
for (const byte of bits) count += popcount(byte);
return count;
}
function popcount(byte: number): number {
let n = byte & 0xff;
let c = 0;
while (n) {
c += n & 1;
n >>>= 1;
}
return c;
}
```
## API Reference
- `PeerDenialReason = "bad_sig" | "manual"` — taxonomy surfaced on
denylist counters.
- `PeerInfo`:
- `peerId: PeerId` — wire identity (hex pubkey).
- `transports: readonly string[]` — transport ids the peer is
reachable on; empty iff every transport has dropped it.
- `rttMs: number | null` — EMA-smoothed RTT in ms; `null` until the
first measurement.
- `lastSeen: number` — unix ms of the most recent observation.
- `score: number` — composite score; `-Infinity` is the denylist
sentinel.
- `availability: ReadonlyMap<string, Uint8Array>` — `fileId` →
framed bitset of held chunks.
- `PeerUpdate { transports?; rttMs?; lastSeen? }` — partial mutation
accepted by `PeerRegistry.addOrUpdate`.
## Implementation Notes
`score` is computed by the peer registry from observations rather than
stored mutably from the outside — the registry is the authority. The
field is exposed read-only on `PeerInfo` so consumers (scheduler,
gossip engine, telemetry) can read a stable snapshot. Mutations happen
inside the registry's `record*` methods.
`availability` is keyed by hex `fileId` rather than raw `Uint8Array`
because `Map`'s equality is reference-based — comparing two byte
arrays with the same content would always miss. The framed bitset is
the value rather than a parsed `BitSet` so the gossip engine can
forward bytes without re-encoding.