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

104 lines
4.4 KiB
Markdown

# 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<TMap>` uses
`Record<string, unknown>` (one payload per emit). Upstream used a tuple-args
shape (`EventMap = Record<string, unknown[]>`). 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.