Files
2026-05-27 23:29:53 +01:00

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, and EventBus<BroadcastChannelEventMap> lifecycle events.
  • ADDED: ChannelFactory seam — caller injects native or polyfill ChannelLike.
  • ADDED: nativeChannelFactory — wraps the global BroadcastChannel.
  • ADDED: createLocalStorageChannelFactory — StorageLike-backed fallback for environments where BroadcastChannel is unavailable.
  • ADDED: ChannelMessageSchema — Zod boundary validator; malformed inbound frames are dropped and surface BROADCAST_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). Upstream any removed.
  • ADDED: BROADCAST_CHANNEL_CODES registry — CHANNEL_CLOSED, MESSAGE_MALFORMED, PAYLOAD_TOO_LARGE, STORAGE_FAILED.
  • ADDED: Payload-size guard — maxMessageBytes (default 1 MiB); over-budget send() throws BroadcastChannelPayloadTooLargeError.
  • ADDED: Injectable Clock / Rng / LoggerLike for deterministic tests.

Out of scope

  • ask() request/response helper — not in the four mandated gap fixes; consumers can layer correlation-ID on top of send/on. Will land in a follow-up change with BROADCAST_CHANNEL.ASK_TIMEOUT code.
  • 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 — TypedBroadcastChannel parity scenarios, leader election, heartbeat, stale cleanup, payload-size guard.
  • src/transport/native.test.ts — nativeChannelFactory round-trip via real BroadcastChannel in happy-dom.
  • src/transport/local-storage.test.ts — createLocalStorageChannelFactory round-trip via real StorageLike fallback.
  • src/schemas/messages.test.ts — Zod boundary acceptance / rejection.
  • src/otel/metrics.test.ts — every instrument asserted against real in-memory MeterProvider.
  • 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.