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,122 @@
# channel/channel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<T>` / `on<T>` / `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<BroadcastChannelEventMap>` 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<T>(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<T>(type: string, handler: ChannelHandler<T>): () => 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<BroadcastChannelEventMap>` — lifecycle bus.
- `type ChannelHandler<T = unknown>` — `(message: ChannelMessage<T>) => 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.