first commit
This commit is contained in:
158
docs/networking/drive/drive-mesh/README.md
Normal file
158
docs/networking/drive/drive-mesh/README.md
Normal 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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
86
docs/networking/drive/drive-mesh/src/types/config.types.md
Normal file
86
docs/networking/drive/drive-mesh/src/types/config.types.md
Normal 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.
|
||||
@@ -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()`.
|
||||
105
docs/networking/drive/drive-mesh/src/types/events.types.md
Normal file
105
docs/networking/drive/drive-mesh/src/types/events.types.md
Normal 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.
|
||||
108
docs/networking/drive/drive-mesh/src/types/peer.types.md
Normal file
108
docs/networking/drive/drive-mesh/src/types/peer.types.md
Normal 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.
|
||||
Reference in New Issue
Block a user