6.3 KiB
Add @suaveplan/broadcast-channel
Package: @suaveplan/broadcast-channel
Status: PROPOSED
Type: new-package
Design: ./design.md
Spec delta: ./specs/broadcast-channel/spec.md
Why
Browser apps need a typed cross-tab messaging primitive built on the BroadcastChannel Web API:
multi-tab sync, leader election, tab presence and heartbeats. Upstream ../suaveplan/packages/browser/broadcast-channel/
shipped at v1.0.5 with a clean public surface but four documented enterprise gaps (enterprise/broadcast-channel.md
§2 smells / §3 required improvements): no Zod boundary on inbound frames, no OTel observability, an untyped
(event: any) listener, and no fallback for browsers without BroadcastChannel (Safari < 15.4 and any
extension/test sandbox without the global). Genesis ships the migration at full parity plus those four fixes.
What changes
- ADDED:
TypedBroadcastChannel— typed cross-tab channel with leader election, tab presence, heartbeats, message-type handler registry, andEventBus<BroadcastChannelEventMap>lifecycle events. - ADDED:
ChannelFactoryseam — caller injects native or polyfillChannelLike. - ADDED:
nativeChannelFactory— wraps the globalBroadcastChannel. - ADDED:
createLocalStorageChannelFactory—StorageLike-backed fallback for environments whereBroadcastChannelis unavailable. - ADDED:
ChannelMessageSchema— Zod boundary validator; malformed inbound frames are dropped and surfaceBROADCAST_CHANNEL.MESSAGE_MALFORMED. - ADDED: OTel instruments under meter
@suaveplan/broadcast-channel—channel.message.sent,channel.message.received,channel.message.dropped,channel.tab.joined,channel.tab.left,channel.leader.elected. - ADDED: Typed listener —
ChannelLike.addEventListener(type, listener: (event: MessageEvent) => void). Upstreamanyremoved. - ADDED:
BROADCAST_CHANNEL_CODESregistry —CHANNEL_CLOSED,MESSAGE_MALFORMED,PAYLOAD_TOO_LARGE,STORAGE_FAILED. - ADDED: Payload-size guard —
maxMessageBytes(default 1 MiB); over-budgetsend()throwsBroadcastChannelPayloadTooLargeError. - ADDED: Injectable
Clock/Rng/LoggerLikefor deterministic tests.
Out of scope
ask()request/response helper — not in the four mandated gap fixes; consumers can layer correlation-ID on top ofsend/on. Will land in a follow-up change withBROADCAST_CHANNEL.ASK_TIMEOUTcode.- Leader-election strategies (round-robin, active-tab-wins) — upstream parity is lowest-tab-ID; alternative strategies are §4 feature-rich extensions not required by the four gaps.
- Message replay buffer, CRDT adapter, dev overlay, encryption hook, namespaced channels, presence hooks (
onTabFocus/onTabBlur), compression — all §4 enhancements deferred to dedicated changes; each is a meaningful feature with its own design and test surface.
Source mapping
This change derives from ../suaveplan/enterprise/broadcast-channel.md and
../suaveplan/packages/browser/broadcast-channel/src/.
| Requirement | Source lines / symbol | Notes |
|---|---|---|
| Typed channel with leader election | enterprise.md L9-L11; channel/channel.ts::TypedBroadcastChannel |
verbatim parity |
| Tab presence + heartbeats | enterprise.md L9-L11; channel/channel.ts::startHeartbeat |
verbatim parity |
| Per-type handler registry | enterprise.md L9-L11; channel/channel.ts::on |
verbatim parity |
ChannelFactory injection seam |
enterprise.md L11; channel/channel.ts::ChannelFactory |
verbatim parity |
BroadcastChannelEventMap lifecycle events |
enterprise.md L11; types/broadcast-channel.types.ts |
parity, adapted to genesis EventBus<TMap> (single-payload map) |
DEFAULT_CHANNEL_CONFIG |
types/broadcast-channel.types.ts::DEFAULT_CHANNEL_CONFIG |
verbatim parity |
| Zod boundary on inbound frames | enterprise.md L31, L38, L56, L79 | gap fix |
localStorage fallback transport |
enterprise.md L29, L38, L51, L64-L65, L80, L96 | gap fix |
Typed MessageEvent listener |
enterprise.md L27, L39, L78 | gap fix |
| OTel counters for sent / received / tabs / leader | enterprise.md L43, L83 | gap fix |
| Payload-size guard | enterprise.md L33, L42, L57, L99 | gap fix, paired with PAYLOAD_TOO_LARGE code |
defineCodes("BROADCAST_CHANNEL", [...]) registry |
enterprise.md L57 | extension into genesis error system |
Injectable Clock / Rng / LoggerLike |
enterprise.md L46-L58 (cross-cutting); CLAUDE.md rule 2 | genesis cross-cutting |
Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% Istanbul coverage threshold | Genesis-wide policy (AGENTS.md §7) |
@suaveplan/testing/runner test imports |
Genesis-wide rule 11 / CLAUDE.md |
| Bun catalog refs for external deps | Genesis-wide rule (AGENTS.md §1) |
Excluded from this migration
| Upstream §4 enhancement | Reason |
|---|---|
ask() request/response |
Not in the four mandated gap fixes; deferred to a dedicated change with its own correlation-ID + timeout tests |
| Round-robin / active-tab-wins leader strategies | Upstream parity is lowest-tab-ID; alternative strategies are §4 enhancements |
| Message replay buffer (ring) | §4 enhancement; needs its own bounded-buffer design |
| CRDT adapter, encryption hook, compression, presence hooks, namespacing, dev overlay | §4 enhancements; each is a meaningful feature with its own design |
Verification approach
src/channel/channel.test.ts—TypedBroadcastChannelparity scenarios, leader election, heartbeat, stale cleanup, payload-size guard.src/transport/native.test.ts—nativeChannelFactoryround-trip via realBroadcastChannelin happy-dom.src/transport/local-storage.test.ts—createLocalStorageChannelFactoryround-trip via realStorageLikefallback.src/schemas/messages.test.ts— Zod boundary acceptance / rejection.src/otel/metrics.test.ts— every instrument asserted against real in-memoryMeterProvider.src/id/id.test.ts—generateId(rng)determinism + format.src/errors/codes.test.ts— registry frozen-shape + every code thrown by ≥1 production path.
Impact
Affected dependents
No dependents on genesis yet (verified by bunx turbo ls --filter=...@suaveplan/broadcast-channel — package is new).
This is a leaf browser-tier package.
Migration impact
None — additive, new package.