Files
skills/docs/networking/events/broadcast-channel/src/channel/heartbeat.md
2026-05-29 08:20:56 +01:00

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 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

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 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.