first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user