2.8 KiB
createHeartbeat
Purpose
Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls PresenceTracker.detectTimedOut() and forwards every returned id through an onTimedOut(id) callback (the daemon disconnects the client). Splitting this out of gateway.ts keeps the daemon file under the line cap and lets tests assert sweep behaviour without booting a server.
Features
- Idempotent
start()/stop()— both methods can be called multiple times safely. enabled()gate so the daemon can disable presence (enablePresence: false) without paying the sweep cost.timer.unref()keeps the sweep from blocking process exit.- Pure factory — no class state escapes the closure.
Usage: Basic Example
import { createHeartbeat } from "@suaveplan/websocket-gateway";
const hb = createHeartbeat(presenceTracker, 30_000, (id) => gateway.disconnect(id), () => true);
hb.start();
hb.stop();
Usage: Advanced Example
import { createHeartbeat } from "@suaveplan/websocket-gateway";
let enabled = true;
const hb = createHeartbeat(
presenceTracker,
1_000,
(id) => {
gateway.disconnectClient(id, "timeout");
},
() => enabled
);
hb.start();
// Pause sweeps without stopping the timer (e.g. during maintenance)
enabled = false;
API Reference
createHeartbeat(presence, intervalMs, onTimedOut, enabled): HeartbeatHandleHeartbeatHandle.start(): voidHeartbeatHandle.stop(): void
Implementation Notes
The handle keeps the timer reference inside the closure so the daemon never needs to introspect or replace it. Using unref() (when available) means the heartbeat does not pin the Node process during graceful shutdown — the daemon still calls stop() explicitly in its stop() path. The enabled() predicate is checked inside the timer callback (not at start-time) so consumers can disable sweeps at runtime without restarting the heartbeat.
The sweep runs synchronously inside the setInterval callback. detectTimedOut() returns at most presence.size entries and the typical workload (a few thousand clients per gateway instance) finishes well within a single millisecond, so the sweep cannot starve other timers. The caller is responsible for handling the actual disconnect — the heartbeat does not own the client map, the room set, or any other state. That separation keeps the unit tests trivial: a PresenceTracker plus an array as the sink is enough to assert every branch.
The interval is bound at construction time and cannot be changed without recreating the handle. This is intentional — runtime interval changes would require re-scheduling logic, and the daemon's stable configuration model has no use case for that. Consumers wanting maintenance-window dynamic behaviour use the enabled() predicate to pause without restarting.