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

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