# channel/channel _(no submodules)_ ## Purpose `TypedBroadcastChannel` is the high-level cross-tab messaging object exported by `@suaveplan/broadcast-channel`. It wraps the structurally-narrow `ChannelLike` transport with the policy layer the upstream package made famous: deterministic lowest-tab-ID leader election, peer-tab presence tracking via periodic heartbeats with eviction of stale entries, and a per-type handler registry that returns an `Unsubscribe` closure. The class is also the boundary at which suaveplan fixes the four enterprise gaps flagged in `enterprise/broadcast-channel.md`: every inbound frame passes the Zod `ChannelMessageSchema` before any handler is invoked, every outbound payload is sized-guarded against `config.maxMessageBytes`, the `ChannelLike` listener is `(event: MessageEvent) => void` (no `any` smell), and the class takes injectable `Clock` / `Rng` / `LoggerLike` so all timestamps, IDs, and diagnostic warnings are deterministic in tests. All hot paths emit OTel counters under meter `@suaveplan/broadcast-channel`. ## Features - Typed `send` / `on` / `handleIncoming` surface preserving payload generics. - Lowest-tab-ID leader election with `leader:elected` lifecycle event on transition. - Heartbeat + cleanup intervals via the `HeartbeatRunner` helper. - Zod boundary validation on every inbound frame; malformed frames are dropped, warned, and counted. - Payload-size guard at `config.maxMessageBytes` raising `BroadcastChannelPayloadTooLargeError`. - `BroadcastChannelClosedError` on `send` before `open` / after `close`. - suaveplan-shape `EventBus` lifecycle bus. - Injectable `Clock` / `Rng` / `LoggerLike` from `@suaveplan/types`. ## Usage: Basic ```ts import { nativeChannelFactory, TypedBroadcastChannel, } from "@suaveplan/broadcast-channel"; const channel = new TypedBroadcastChannel({ config: { name: "app-bus" }, factory: nativeChannelFactory, }); channel.open(); channel.on<{ userId: string }>("user:signed-in", (msg) => { console.log("peer", msg.senderId, "signed in as", msg.payload.userId); }); channel.send("user:signed-in", { userId: "u_123" }); ``` ## Usage: Advanced ```ts import { createLocalStorageChannelFactory, isNativeBroadcastChannelAvailable, nativeChannelFactory, TypedBroadcastChannel, } from "@suaveplan/broadcast-channel"; import type { LoggerLike, StorageLike } from "@suaveplan/types"; declare const memoryStorage: StorageLike; declare const appLogger: LoggerLike; const channel = new TypedBroadcastChannel({ config: { name: "app-bus", heartbeatInterval: 2_000, tabTimeout: 8_000, maxMessageBytes: 64 * 1024, }, factory: isNativeBroadcastChannelAvailable() ? nativeChannelFactory : createLocalStorageChannelFactory(memoryStorage, { pollIntervalMs: 50, logger: appLogger, }), clock: () => Date.now(), logger: appLogger, }); channel.events.on("leader:elected", (e) => { appLogger.info("became leader", { tabId: e.tabId }); }); channel.open(); ``` ## API Reference - `class TypedBroadcastChannel` - `constructor(options: TypedBroadcastChannelOptions)` — accepts `config`, `factory`, optional `clock`, `rng`, `logger`. - `open(): void` — idempotent; subsequent `open()` is a no-op. - `close(): void` — safe before `open()`; clears tabs, handlers, and timers. - `send(type: string, payload: T): void` — throws `BroadcastChannelClosedError` if the channel is not open; throws `BroadcastChannelPayloadTooLargeError` if the JSON-encoded byte length of the message exceeds `config.maxMessageBytes`. - `on(type: string, handler: ChannelHandler): () => void` — returns an unsubscribe closure. - `getTabId(): string` — local 16-hex tab ID derived from the injected `Rng`. - `getIsLeader(): boolean` — true after the local tab has been elected leader. - `getTabs(): TabInfo[]` — snapshot of known tabs. - `getTabCount(): number` — count of known tabs (self + peers). - `isOpen(): boolean` - `handleIncoming(raw: unknown): void` — exposed for tests; runs the Zod-validated dispatch pipeline. - `events: EventBus` — lifecycle bus. - `type ChannelHandler` — `(message: ChannelMessage) => void`. - `interface TypedBroadcastChannelOptions` — `{ config, factory, clock?, rng?, logger? }`. ## Implementation Notes `open()` registers a `(event: MessageEvent) => void` listener on the underlying `ChannelLike` and forwards `event.data` straight into `handleIncoming`. Inbound validation uses `ChannelMessageSchema.safeParse`; failures call `recordMessageDropped("malformed", channelName)` and `logger.warn` with the Zod issues but do not throw. Internal frames (`tab-join`, `tab-leave`, `heartbeat`) bypass the public counters but still pass validation so a rogue peer cannot inject coordination state. Leader election is run after every `tab-join` / `tab-leave` / cleanup tick; only `non-leader -> leader` transitions emit `leader:elected`. The heartbeat / cleanup pair is owned by the `HeartbeatRunner` helper to keep this file inside the 300-line budget.