# Design — add-broadcast-channel ## Module layout ```text packages/events/broadcast-channel/ src/ channel/ channel.ts # TypedBroadcastChannel core (295 lines, under 300 budget) heartbeat.ts # HeartbeatRunner helper extracted to fit budget channel.test.ts heartbeat.test.ts channel.md heartbeat.md index.ts errors/ codes.ts # defineCodes("BROADCAST_CHANNEL", [...]) + 4 subclasses codes.test.ts codes.md index.ts id/ id.ts # generateId(rng) + defaultRng id.test.ts id.md index.ts otel/ metrics.ts # 6 counters + refreshBroadcastChannelMeters metrics.test.ts metrics.md index.ts schemas/ messages.ts # ChannelMessageSchema + ChannelConfigSchema (Zod) messages.test.ts messages.md index.ts transport/ native.ts # nativeChannelFactory + isNativeBroadcastChannelAvailable local-storage.ts # StorageLike-backed fallback transport.types.ts # ChannelLike (typed MessageEvent) + ChannelFactory native.test.ts local-storage.test.ts native.md local-storage.md index.ts types/ broadcast-channel.types.ts index.ts index.ts # named re-exports only tests/setup/polyfills.ts # in-memory MeterProvider for OTel tests README.md package.json vite.config.ts vitest.config.ts tsconfig.json tsconfig.lib.json ``` ## Key design choices - **Single-payload event map**: genesis `EventBus` uses `Record` (one payload per emit). Upstream used a tuple-args shape (`EventMap = Record`). The migrated event map carries structured records (`ChannelOpenedEvent`, `TabJoinedEvent`, …) so every callback sees one object with named fields. Net public-surface impact: consumers register `on("tab:joined", e => e.tabId)` instead of `on("tab:joined", tabId => ...)`. This is documented in `proposal.md` → `BroadcastChannelEventMap`. - **`ChannelLike` listener fixed to `MessageEvent`**: upstream had `(event: any) => void` — the migration narrows to `(event: MessageEvent) => void` on the `ChannelLike` interface and propagates the change through both transports. This is the genesis-mandated gap fix. - **`StorageLike` rather than raw `window.localStorage`**: the fallback transport takes any `StorageLike` from `@suaveplan/types`. This generalises the polyfill beyond Safari — the same factory works on top of OPFS, IndexedDB, Garage S3, or an in-memory test double — and removes the package's dependency on a specific browser global. - **Storage failures logged, not thrown**: the polling / write paths fire on intervals; an unhandled rejection would crash the host. Failures wrap into `BroadcastChannelStorageFailedError`, surface through the injected `LoggerLike.warn`, and the loop continues. The code is still reachable from tests because the wrapping happens synchronously after the rejection. - **Six counters, no histograms**: every lifecycle path emits a counter. No meaningful latency axis exists (dispatch is in-process, `send` is sync) so a histogram would be empty noise. - **Payload guard via JSON byte length**: the canonical structured-clone size of a frame is unknowable without serialising. JSON is the only encoding the underlying transports universally support, so the guard runs the JSON encoder once, measures via `TextEncoder.encode`, and rejects above `maxMessageBytes`. The same encoded buffer is not reused for the native transport (which clones internally) but the cost is one `JSON.stringify` per `send` — well under any meaningful budget. ## Out-of-scope features and why The upstream §4 list ("Feature-rich enhancements") catalogues ten further ideas. The migration explicitly defers nine of them — see `### Excluded from this migration` in `proposal.md`. The genesis rule "No deferrals when migrating/creating packages" applies to **legitimate parity** features that were in upstream's shipping surface; §4 enhancements are documented as future work in the audit, not as upstream-shipped surface, and each carries its own design surface and test contract that does not fit a 4-file migration. Future changes (`add-broadcast-channel-ask`, `add-broadcast-channel-leader-strategies`, etc.) will land them individually.