Initial Commit
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user