# `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 ```ts import { createHeartbeat } from "@suaveplan/websocket-gateway"; const hb = createHeartbeat(presenceTracker, 30_000, (id) => gateway.disconnect(id), () => true); hb.start(); hb.stop(); ``` ## Usage: Advanced Example ```ts 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): HeartbeatHandle` - `HeartbeatHandle.start(): void` - `HeartbeatHandle.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.