first commit
This commit is contained in:
122
docs/networking/events/broadcast-channel/src/channel/channel.md
Normal file
122
docs/networking/events/broadcast-channel/src/channel/channel.md
Normal 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.
|
||||
Reference in New Issue
Block a user