# `gossip-engine.ts` _(no submodules)_ ## 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` — 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.