first commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# channel/heartbeat
|
||||
|
||||
## Purpose
|
||||
|
||||
`HeartbeatRunner` is a tiny lifecycle helper that owns the pair of
|
||||
`setInterval` timers a `TypedBroadcastChannel` needs to keep peer presence
|
||||
warm. The heartbeat callback fires once per `heartbeatInterval` and is meant
|
||||
to post a `heartbeat` frame plus refresh the local tab's `lastSeen`. The
|
||||
cleanup callback fires once per `heartbeatInterval * 2` and is meant to walk
|
||||
the known-tabs map and evict any peer whose `lastSeen` is older than the
|
||||
configured `tabTimeout`.
|
||||
|
||||
The helper exists because the upstream library packed both intervals into
|
||||
`channel.ts` inline; pulling them out keeps the channel core inside the
|
||||
300-line hard budget mandated by `AGENTS.md` §3 and gives each timer
|
||||
lifecycle a discrete unit test surface. The runner has no view into the
|
||||
channel — it only fires the callbacks it was given, so it composes with any
|
||||
substitute clock or scheduler the consumer wants to thread.
|
||||
|
||||
## Features
|
||||
|
||||
- Two independent `setInterval`s driven by a single `heartbeatInterval`.
|
||||
- Idempotent `stop()` — safe to call before `start()` or twice in a row.
|
||||
- No shared state with the channel; lifecycle ownership is local to the runner.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { HeartbeatRunner } from "@suaveplan/broadcast-channel";
|
||||
|
||||
const runner = new HeartbeatRunner({
|
||||
heartbeatInterval: 1_000,
|
||||
onHeartbeat: () => console.log("ping"),
|
||||
onCleanup: () => console.log("gc"),
|
||||
});
|
||||
runner.start();
|
||||
// later
|
||||
runner.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { HeartbeatRunner } from "@suaveplan/broadcast-channel";
|
||||
|
||||
class Presence {
|
||||
private readonly runner = new HeartbeatRunner({
|
||||
heartbeatInterval: 2_000,
|
||||
onHeartbeat: () => this.broadcastSelf(),
|
||||
onCleanup: () => this.evictStale(),
|
||||
});
|
||||
|
||||
public open(): void {
|
||||
this.runner.start();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.runner.stop();
|
||||
}
|
||||
|
||||
private broadcastSelf(): void {
|
||||
// post heartbeat frame ...
|
||||
}
|
||||
|
||||
private evictStale(): void {
|
||||
// remove peers whose lastSeen is past tabTimeout ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class HeartbeatRunner`
|
||||
- `constructor(opts: HeartbeatRunnerOptions)`
|
||||
- `start(): void` — schedules both intervals; calling twice replaces the timers.
|
||||
- `stop(): void` — clears both intervals if running; safe to call repeatedly.
|
||||
- `interface HeartbeatRunnerOptions`
|
||||
- `heartbeatInterval: number` — millisecond cadence for `onHeartbeat`.
|
||||
- `onHeartbeat: () => void`
|
||||
- `onCleanup: () => void` — fires at `heartbeatInterval * 2`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The cleanup cadence is intentionally twice the heartbeat cadence: this gives
|
||||
peers at least one heartbeat opportunity to refresh `lastSeen` before a
|
||||
cleanup tick can evict them, even under jitter. The runner stores the two
|
||||
handles as `ReturnType<typeof setInterval>` so the same code typechecks under
|
||||
both Bun and Node ambient lib variants. Because `start()` re-assigns the
|
||||
handles without clearing the prior ones first, callers must call `stop()`
|
||||
before re-starting — `TypedBroadcastChannel` does exactly that during its
|
||||
`close()`/`open()` cycle. Tests pin the cadence at 20 ms so the suite stays
|
||||
fast while still exercising at least one of each callback.
|
||||
Reference in New Issue
Block a user