4.4 KiB
4.4 KiB
Design — add-broadcast-channel
Module layout
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>usesRecord<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 registeron("tab:joined", e => e.tabId)instead ofon("tab:joined", tabId => ...). This is documented inproposal.md→BroadcastChannelEventMap. ChannelLikelistener fixed toMessageEvent: upstream had(event: any) => void— the migration narrows to(event: MessageEvent) => voidon theChannelLikeinterface and propagates the change through both transports. This is the genesis-mandated gap fix.StorageLikerather than rawwindow.localStorage: the fallback transport takes anyStorageLikefrom@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 injectedLoggerLike.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,
sendis 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 abovemaxMessageBytes. The same encoded buffer is not reused for the native transport (which clones internally) but the cost is oneJSON.stringifypersend— 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.