Initial Commit
This commit is contained in:
103
openspec/archive/2026-05-19-add-broadcast-channel/design.md
Normal file
103
openspec/archive/2026-05-19-add-broadcast-channel/design.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user