Files
suaveplan/openspec/archive/2026-05-19-add-broadcast-channel/proposal.md
2026-05-27 23:29:53 +01:00

95 lines
6.3 KiB
Markdown

# 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.