Files
suaveplan/packages/networking/events/broadcast-channel/README.md
2026-05-27 23:47:53 +01:00

115 lines
6.3 KiB
Markdown

# @suaveplan/broadcast-channel - Typed cross-tab messaging built on the BroadcastChannel API — leader election, tab presence, Zod-validated boundaries, OpenTelemetry-instrumented hot paths, StorageLike-backed fallback for Safari < 15.4. Browser-tier.
Typed cross-tab messaging built on the Web `BroadcastChannel` API. Browser-tier package — leader election, tab presence and heartbeats, message-type handler registry, Zod-validated boundaries, OpenTelemetry-instrumented hot paths, deterministic `Clock` / `Rng` / `LoggerLike` injection, and a `StorageLike`-backed fallback transport for Safari < 15.4 (and any sandbox where the native `BroadcastChannel` is unavailable).
<!-- begin:auto-docs -->
## Documentation tree
- [ask](./src/ask/ask.md) — `AskController` is the request-side engine for the cross-tab request/response primitive layered on top of `TypedBroadca…
- [ask](./src/ask/ask.md) — `AskController` is the request-side engine for the cross-tab request/response primitive layered on top of `TypedBroadca…
- [responder](./src/ask/responder.md) — `AskResponderRegistry` is the response-side counterpart to `AskController`. It maps an inbound `handlerKey` to the sing…
- [channel](./src/channel/channel.md) — `TypedBroadcastChannel` is the high-level cross-tab messaging object exported by `@suaveplan/broadcast-channel`. It wra…
- [channel](./src/channel/channel.md) — `TypedBroadcastChannel` is the high-level cross-tab messaging object exported by `@suaveplan/broadcast-channel`. It wra…
- [heartbeat](./src/channel/heartbeat.md) — `HeartbeatRunner` is a tiny lifecycle helper that owns the pair of `setInterval` timers a `TypedBroadcastChannel` needs…
- [errors](./src/errors/codes.md) — This module declares the stable error-code registry for `@suaveplan/broadcast-channel`. Every error the package throws…
- [codes](./src/errors/codes.md) — This module declares the stable error-code registry for `@suaveplan/broadcast-channel`. Every error the package throws…
- [id](./src/id/id.md) — `generateId` is the shared 16-character lowercase-hex identifier generator used for tab IDs and per-message IDs through…
- [id](./src/id/id.md) — `generateId` is the shared 16-character lowercase-hex identifier generator used for tab IDs and per-message IDs through…
- [logger](./src/logger/noop.md) — `noopLogger` is a shared zero-cost `LoggerLike` implementation used as the default by `TypedBroadcastChannel` and by `c…
- [noop](./src/logger/noop.md) — `noopLogger` is a shared zero-cost `LoggerLike` implementation used as the default by `TypedBroadcastChannel` and by `c…
- [otel](./src/otel/metrics.md) — This module declares the six OpenTelemetry counter instruments emitted by `@suaveplan/broadcast-channel`. Every counter…
- [metrics](./src/otel/metrics.md) — This module declares the six OpenTelemetry counter instruments emitted by `@suaveplan/broadcast-channel`. Every counter…
- [schemas](./src/schemas/messages.md) — This module declares the Zod boundary schemas for `@suaveplan/broadcast-channel`. `ChannelMessageSchema` validates ever…
- [messages](./src/schemas/messages.md) — This module declares the Zod boundary schemas for `@suaveplan/broadcast-channel`. `ChannelMessageSchema` validates ever…
- [local-storage](./src/transport/local-storage.md) — `createLocalStorageChannelFactory` is the `StorageLike`-backed fallback transport. Use it whenever the host lacks a nat…
- [native](./src/transport/native.md) — `nativeChannelFactory` is a thin adapter that exposes the global `BroadcastChannel` constructor as a `ChannelLike`. Use…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/broadcast-channel
```
## Quick start
```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 tab ${msg.senderId} signed in as ${msg.payload.userId}`);
});
channel.send("user:signed-in", { userId: "u_123" });
```
## Fallback transport (Safari < 15.4)
```ts
import {
createLocalStorageChannelFactory,
isNativeBroadcastChannelAvailable,
nativeChannelFactory,
TypedBroadcastChannel,
} from "@suaveplan/broadcast-channel";
import { yourStorageLike } from "./storage";
const factory = isNativeBroadcastChannelAvailable()
? nativeChannelFactory
: createLocalStorageChannelFactory(yourStorageLike, {
pollIntervalMs: 50,
retentionMs: 30_000,
});
const channel = new TypedBroadcastChannel({
config: { name: "app-bus" },
factory,
});
```
## Modules
- [`channel/channel`](./src/channel/channel.md) — `TypedBroadcastChannel` core.
- [`channel/heartbeat`](./src/channel/heartbeat.md) — `HeartbeatRunner` interval pair.
- [`errors/codes`](./src/errors/codes.md) — `BROADCAST_CHANNEL_CODES` registry + four typed error subclasses.
- [`id/id`](./src/id/id.md) — `generateId(rng)` shared 16-hex id generator.
- [`logger/noop`](./src/logger/noop.md) — shared no-op `LoggerLike` default.
- [`otel/metrics`](./src/otel/metrics.md) — six OpenTelemetry counters.
- [`schemas/messages`](./src/schemas/messages.md) — Zod boundary schemas.
- [`transport/native`](./src/transport/native.md) — native `BroadcastChannel` adapter.
- [`transport/local-storage`](./src/transport/local-storage.md) — `StorageLike`-backed fallback.
- [`types/broadcast-channel`](./src/types/broadcast-channel.types.ts) — public shapes.
## Error codes
| Code | When |
|---|---|
| `BROADCAST_CHANNEL.CHANNEL_CLOSED` | `send()` called before `open()` or after `close()` |
| `BROADCAST_CHANNEL.MESSAGE_MALFORMED` | inbound frame fails `ChannelMessageSchema` |
| `BROADCAST_CHANNEL.PAYLOAD_TOO_LARGE` | outbound payload exceeds `config.maxMessageBytes` (default 1 MiB) |
| `BROADCAST_CHANNEL.STORAGE_FAILED` | fallback-transport `StorageLike` get/set/list/delete rejected |
## OpenTelemetry
Six counters under meter `@suaveplan/broadcast-channel@0.1.0`:
- `channel.message.sent` (`{ type, channel }`)
- `channel.message.received` (`{ type, channel }`)
- `channel.message.dropped` (`{ reason, channel }`)
- `channel.tab.joined` (`{ channel }`)
- `channel.tab.left` (`{ channel }`)
- `channel.leader.elected` (`{ channel }`)
## License
MIT