Files
suaveplan/openspec/changes/enterprise-browser-broadcast-channel/proposal.md
2026-05-27 23:29:53 +01:00

6.3 KiB

OpenSpec Change: enterprise-browser-broadcast-channel

  • Change ID: enterprise-browser-broadcast-channel
  • Package: @suaveplan/broadcast-channel
  • Category: browser
  • Target path: browser/platform/broadcast-channel
  • Status: IN PROGRESS
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/broadcast-channel.md

Summary

Deliver @suaveplan/broadcast-channel on the genesis branch at enterprise quality. The package provides TypedBroadcastChannel with message routing, tab presence tracking, heartbeats, stale-tab cleanup, and leader election. Genesis adds: localStorage-backed fallback transport (Safari < 15.4), Zod runtime validation on incoming frames, typed MessageEvent (removes any), ask<T,R>() request/response primitive, enhanced leader-election strategies, payload-size guard, shared ID generator, and OTel metrics.


Current-state audit (genesis)

Fresh build — does not exist on genesis yet.


Available workspace deps on genesis

Package Role
@suaveplan/events (workspace:^) EventEmitter base for lifecycle events
@suaveplan/error (workspace:^) CHANNEL_CLOSED, MESSAGE_MALFORMED, PAYLOAD_TOO_LARGE
@suaveplan/types (workspace:^) LoggerLike
@suaveplan/logger (workspace:^) Structured logging
@suaveplan/crypto (workspace:^) generateId() (replaces duplicate local implementations)
@suaveplan/schema-registry (workspace:^) ChannelMessage schema
zod (catalog:forms) ChannelMessageSchema
@opentelemetry/api (catalog:observability) Message counters, handler latency
tslib (catalog:build) Runtime helpers

Dev: vitest, @vitest/coverage-istanbul, happy-dom, @playwright/test, @suaveplan/testing.


Preserved public API surface

class TypedBroadcastChannel extends EventEmitter<BroadcastChannelEventMap> {
  constructor(config: ChannelConfig)
  send<T>(type: string, payload: T): void
  on<T>(type: string, handler: ChannelHandler<T>): () => void
  ask<T, R>(type: string, payload: T, options?: AskOptions): Promise<R>   // new
  close(): void
  getTabId(): string
  getLeaderId(): string | null
  getTabs(): TabInfo[]
}

// Transport seam (new)
interface ChannelTransport { send(msg: ChannelMessage): void; onMessage(cb: (msg: ChannelMessage) => void): () => void; close(): void }
class BroadcastTransport implements ChannelTransport { constructor(name: string) }
class LocalStorageTransport implements ChannelTransport { constructor(name: string) }    // fallback

// Leader strategies (new)
interface LeaderStrategy { elect(tabs: TabInfo[]): string }
class LowestIdStrategy implements LeaderStrategy { }
class ActiveTabStrategy implements LeaderStrategy { }   // visibilitychange-aware

// Types
interface ChannelConfig { name: string; heartbeatInterval?: number; staleThreshold?: number; maxMessageBytes?: number; transport?: ChannelTransport; leaderStrategy?: LeaderStrategy }
interface ChannelMessage { id: string; type: string; tabId: string; payload: unknown; timestamp: number }
interface TabInfo { id: string; joinedAt: number; active: boolean; leader: boolean }

Subpath exports: ".", "./transport", "./leader", "./types".


New additions for genesis

1. localStorage fallback transport

LocalStorageTransport implements ChannelTransport using localStorage + storage events; auto-selected when BroadcastChannel API is absent (Safari < 15.4).

2. Zod runtime validation

ChannelMessageSchema validates every incoming frame in handleIncoming; malformed frames are dropped and error event emitted with MESSAGE_MALFORMED.

3. Typed MessageEvent

ChannelLike.addEventListener typed to (event: MessageEvent) => void — no any.

4. ask<T, R>() request/response

Correlation-ID request/response layered on send/on; configurable timeout; reject on channel close.

5. Pluggable leader-election strategies

LowestIdStrategy (default), ActiveTabStrategy (prefers the currently visible tab via visibilitychange). Custom strategies via LeaderStrategy interface.

6. Payload-size guard

send() throws PAYLOAD_TOO_LARGE when serialized payload exceeds maxMessageBytes (default 1 MB).

7. Shared ID generator

src/internal/id.ts using @suaveplan/crypto; replaces the two duplicate generateTabId/generateMsgId in the original.

8. Error codes

CHANNEL_CLOSED, MESSAGE_MALFORMED, PAYLOAD_TOO_LARGE.

9. OTel metrics

Counter channel.messages_sent_total, counter channel.messages_received_total, counter channel.messages_dropped_total, gauge channel.tabs_tracked, histogram channel.handler_latency_ms.


File layout

browser/platform/broadcast-channel/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    channel/
      channel/
        channel.ts            (TypedBroadcastChannel, ≤250 lines)
        channel.test.ts
        channel.md
        index.ts
    transport/
      broadcast/
        broadcast.ts
        broadcast.test.ts
        broadcast.md
        index.ts
      local-storage/
        local-storage.ts
        local-storage.test.ts
        local-storage.md
        index.ts
    ask/
      ask/
        ask.ts
        ask.test.ts
        ask.md
        index.ts
    leader/
      leader/
        leader.ts             (LeaderStrategy interface + LowestIdStrategy + ActiveTabStrategy)
        leader.test.ts
        leader.md
        index.ts
    schema/
      channel-message-schema/
        channel-message-schema.ts
        channel-message-schema.md
        index.ts
    internal/
      id.ts                   (generateId via @suaveplan/crypto)
    types/
      broadcast-channel.types.ts
      broadcast-channel.types.md
    constants/
      index.ts
  tests/
    integration.test.ts
  e2e/
    broadcast-channel.spec.ts (cross-tab: Chromium; fallback transport: WebKit)

Impact

Downstream: @suaveplan/cache-browser, @suaveplan/storage-browser, @suaveplan/analytics-browser, any package using cross-tab communication.

Breaking changes: None — all additions are additive. ChannelLike type tightened (removes any); consumers may need to update handler signatures.

Prerequisites: @suaveplan/events, @suaveplan/error, @suaveplan/crypto on Verdaccio.