3.1 KiB
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
setIntervals driven by a singleheartbeatInterval. - Idempotent
stop()— safe to call beforestart()or twice in a row. - No shared state with the channel; lifecycle ownership is local to the runner.
Usage: Basic
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
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 HeartbeatRunnerconstructor(opts: HeartbeatRunnerOptions)start(): void— schedules both intervals; calling twice replaces the timers.stop(): void— clears both intervals if running; safe to call repeatedly.
interface HeartbeatRunnerOptionsheartbeatInterval: number— millisecond cadence foronHeartbeat.onHeartbeat: () => voidonCleanup: () => void— fires atheartbeatInterval * 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.