first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
# `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.