183 lines
6.3 KiB
Markdown
183 lines
6.3 KiB
Markdown
# 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
|
|
|
|
```ts
|
|
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
|
|
|
|
```text
|
|
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.
|