Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,122 @@
# Tasks: enterprise-browser-broadcast-channel
**Change ID**: enterprise-browser-broadcast-channel
**Package**: `@suaveplan/broadcast-channel`
**Target path**: `browser/platform/broadcast-channel`
**Prerequisite packages on Verdaccio**: `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/crypto`, `@suaveplan/logger`, `@suaveplan/schema-registry`
---
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Scaffold
- [ ] Create directory tree per `proposal.md`.
- [ ] `package.json` v0.0.0, `"sideEffects": false`.
- `dependencies`: `@suaveplan/events workspace:^`, `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/crypto workspace:^`, `@suaveplan/schema-registry workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `happy-dom catalog:testing`, `@playwright/test catalog:testing`, `@suaveplan/testing workspace:^`
- Subpath exports: `"."`, `"./transport"`, `"./leader"`, `"./types"`
- [ ] `tsconfig.json`, `vitest.config.ts` (pool: forks, happy-dom environment, `fileParallelism: false`).
---
## 2. Error codes
- [ ] `CHANNEL_CLOSED`, `MESSAGE_MALFORMED`, `PAYLOAD_TOO_LARGE`.
- [ ] `registerBroadcastChannelErrorCodes()` — call eagerly.
---
## 3. Types (`src/types/broadcast-channel.types.ts`)
- [ ] `ChannelConfig`, `ChannelMessage`, `TabInfo`, `BroadcastChannelEventMap`, `ChannelHandler`, `ChannelLike` (typed `MessageEvent`), `ChannelTransport`, `AskOptions`.
- [ ] Write `broadcast-channel.types.md`.
---
## 4. Internal ID generator (`src/internal/id.ts`)
- [ ] `generateId(): string` — wraps `@suaveplan/crypto` random bytes → hex string.
- [ ] Replaces duplicate `generateTabId`/`generateMsgId` that existed in the original.
---
## 5. `ChannelMessage` schema (`src/schema/channel-message-schema/`)
- [ ] `channel-message-schema.ts` — Zod `ChannelMessageSchema`; `registerChannelMessageSchema()`.
- [ ] Write `channel-message-schema.md`.
---
## 6. `BroadcastTransport` (`src/transport/broadcast/`)
- [ ] `broadcast.ts` — wraps native `BroadcastChannel`; implements `ChannelTransport`.
- [ ] Write `broadcast.test.ts` (send, receive, close). Write `broadcast.md`.
---
## 7. `LocalStorageTransport` (`src/transport/local-storage/`)
- [ ] `local-storage.ts` — `localStorage` + `storage` event fallback; same `ChannelTransport` interface.
- [ ] Auto-polyfill detection: `typeof BroadcastChannel === 'undefined'`.
- [ ] Write `local-storage.test.ts` (send, receive, close). Write `local-storage.md`.
---
## 8. Leader strategies (`src/leader/leader/`)
- [ ] `leader.ts` — `LeaderStrategy` interface; `LowestIdStrategy` (lowest tab ID wins); `ActiveTabStrategy` (prefers visible tab via `visibilitychange`, tie-break by `joinedAt`).
- [ ] Write `leader.test.ts` (LowestId election, ActiveTab election, disconnect scenario). Write `leader.md`.
---
## 9. `ask` primitive (`src/ask/ask/`)
- [ ] `ask.ts` — `ask<T, R>(channel, type, payload, options): Promise<R>` using correlation IDs and timeout.
- [ ] Reject with `CHANNEL_CLOSED` on close; reject with `WORKER_REQUEST_TIMEOUT` on timeout.
- [ ] Write `ask.test.ts` (success, timeout, channel-close rejection, collision). Write `ask.md`.
---
## 10. `TypedBroadcastChannel` (`src/channel/channel/`)
- [ ] `channel.ts` — composing transport + leader + ask + schema validation.
- [ ] `handleIncoming` validates via `ChannelMessageSchema`; drops + emits `error` on malformed.
- [ ] `send()` guards against `maxMessageBytes`; throws `PAYLOAD_TOO_LARGE`.
- [ ] Wire OTel metrics.
- [ ] Wire `@suaveplan/logger` for dropped frames and stale-tab cleanup.
- [ ] Write `channel.test.ts` (send, receive, malformed drop, payload-too-large, leader election, stale cleanup). Write `channel.md`.
---
## 11. Integration test (`tests/integration.test.ts`)
- [ ] `BroadcastTransport` → `TypedBroadcastChannel` round-trip.
- [ ] `LocalStorageTransport` same API round-trip.
- [ ] `ask` with timeout: verify rejection.
- [ ] Leader election: two channels, close leader, verify re-election.
---
## 12. Playwright E2E (`e2e/broadcast-channel.spec.ts`)
- [ ] Chromium: two browser tabs on same origin; one sends, other receives.
- [ ] WebKit: `LocalStorageTransport` fallback verified (inject config to skip `BroadcastChannel`).
- [ ] Leader election: three tabs, close leader, verify re-election in remaining two.
---
## 13. Docs + verify
- [ ] Write `.md` per module (≥200 words, API reference, examples).
- [ ] Write `README.md` with subpath table, transport guide, leader-election guide, `ask` example.
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/broadcast-channel` — green.
- [ ] `bunx turbo test:coverage` — 100%.
- [ ] No `@ts-expect-error` in `src/` or tests. No `any` in `ChannelLike`.
- [ ] Playwright E2E green on Chromium and WebKit.
- [ ] `bun scripts/publish.ts --package @suaveplan/broadcast-channel`.
- [ ] Archive to `openspec/archive/2026-04-24-enterprise-browser-broadcast-channel/`.