Initial Commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
# OpenSpec Change: enterprise-events-transport
|
||||
|
||||
- **Change ID**: enterprise-events-transport
|
||||
- **Package**: `@suaveplan/events-transport`
|
||||
- **Category**: universal
|
||||
- **Target path**: `universal/events/events-transport`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/events-transport.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/events-transport` on the genesis branch from scratch at enterprise quality. The package provides cross-context event bridges for `@suaveplan/events`: bridges dispatches across `MessageChannel` (worker ↔ main thread) and `BroadcastChannel` (tab ↔ tab), with structured-clone-safe payload validation, W3C trace context propagation in envelopes, documented delivery guarantees per transport, idempotency via nonce dedup windows, and `AbortSignal` cancellation. Attaches to the event bus via the public interceptor API.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/events` (`workspace:^`) | Event bus and interceptor API |
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `EventsTransportError`; `defineCodes` for `EVENTS_TRANSPORT.*` codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `Rng`, `IdGenerator` injection interfaces |
|
||||
| `zod` (`catalog:forms`) | Zod schemas for transport envelope and option types |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Cross-boundary span linking, context propagation |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
### Transport interface
|
||||
|
||||
```ts
|
||||
interface EventTransport {
|
||||
readonly name: string
|
||||
readonly deliveryGuarantee: DeliveryGuarantee
|
||||
connect(bus: EventBusLike, options?: ConnectOptions): TransportHandle
|
||||
}
|
||||
|
||||
type DeliveryGuarantee = "at-most-once" | "at-least-once"
|
||||
|
||||
interface ConnectOptions {
|
||||
eventFilter?: (name: string) => boolean
|
||||
signal?: AbortSignal
|
||||
logger?: LoggerLike
|
||||
clock?: Clock
|
||||
rng?: Rng
|
||||
idGenerator?: IdGenerator
|
||||
}
|
||||
|
||||
interface TransportHandle {
|
||||
disconnect(): void
|
||||
readonly stats: TransportStats
|
||||
}
|
||||
|
||||
interface TransportStats {
|
||||
sent: number
|
||||
received: number
|
||||
dropped: number
|
||||
duplicatesFiltered: number
|
||||
}
|
||||
```
|
||||
|
||||
### MessageChannel transport
|
||||
|
||||
```ts
|
||||
// at-least-once delivery with ack protocol
|
||||
function createMessageChannelTransport(options: MessageChannelTransportOptions): EventTransport
|
||||
|
||||
interface MessageChannelTransportOptions {
|
||||
channel: MessageChannel
|
||||
ackTimeoutMs?: number // default: 5000
|
||||
maxRetries?: number // default: 3
|
||||
nonceWindowSize?: number // dedup window for received nonces; default: 1000
|
||||
}
|
||||
```
|
||||
|
||||
### BroadcastChannel transport
|
||||
|
||||
```ts
|
||||
// at-most-once delivery (no ack)
|
||||
function createBroadcastChannelTransport(options: BroadcastChannelTransportOptions): EventTransport
|
||||
|
||||
interface BroadcastChannelTransportOptions {
|
||||
channelName: string
|
||||
nonceWindowSize?: number // dedup window for received nonces; default: 1000
|
||||
origin?: string // filter by origin; default: "*"
|
||||
}
|
||||
```
|
||||
|
||||
### Transport envelope (wire format)
|
||||
|
||||
```ts
|
||||
interface TransportEnvelope {
|
||||
v: 1 // envelope version
|
||||
event: string // event name
|
||||
payload: unknown // structured-clone-safe payload
|
||||
nonce: string // unique per-send dedup key
|
||||
origin: string // sender identifier
|
||||
ts: number // sender timestamp (ms since epoch)
|
||||
traceContext?: Record<string, string> // W3C trace context headers
|
||||
}
|
||||
|
||||
const TransportEnvelopeSchema: ZodSchema
|
||||
```
|
||||
|
||||
### Structured-clone safety check
|
||||
|
||||
```ts
|
||||
// Returns true if the value can be transmitted via structured clone
|
||||
function isStructuredCloneable(value: unknown): boolean
|
||||
|
||||
// Throws EventsTransportError if value is not structured-clone-safe
|
||||
function assertStructuredCloneable(value: unknown): void
|
||||
```
|
||||
|
||||
### Error handling
|
||||
|
||||
```ts
|
||||
class EventsTransportError extends SuaveplanError<EventsTransportErrorCode>
|
||||
|
||||
type EventsTransportErrorCode =
|
||||
| "EVENTS_TRANSPORT_NOT_CLONEABLE"
|
||||
| "EVENTS_TRANSPORT_ACK_TIMEOUT"
|
||||
| "EVENTS_TRANSPORT_CONNECT_FAILED"
|
||||
| "EVENTS_TRANSPORT_DISCONNECTED"
|
||||
| "EVENTS_TRANSPORT_ENVELOPE_INVALID"
|
||||
|
||||
function registerEventsTransportErrorCodes(): void
|
||||
```
|
||||
|
||||
### Zod schemas
|
||||
|
||||
```ts
|
||||
const MessageChannelTransportOptionsSchema: ZodSchema
|
||||
const BroadcastChannelTransportOptionsSchema: ZodSchema
|
||||
const ConnectOptionsSchema: ZodSchema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### 1. W3C Trace Context in envelopes
|
||||
|
||||
When `@suaveplan/events-otel` is in use on the sending side, `serializeTraceContext()` is called and the result placed in `envelope.traceContext`. On the receiving side, `deserializeTraceContext(envelope.traceContext)` restores the context before re-dispatching, linking the spans across the transport boundary.
|
||||
|
||||
### 2. Nonce dedup window
|
||||
|
||||
Both transports maintain a sliding window of received nonces (LRU, configurable `nonceWindowSize`). Duplicate nonces within the window are silently dropped and counted in `TransportStats.duplicatesFiltered`.
|
||||
|
||||
### 3. Ack protocol for `MessageChannel`
|
||||
|
||||
The `MessageChannelTransport` sender waits for an `{ack: nonce}` reply message before incrementing the retry counter. If the ack doesn't arrive within `ackTimeoutMs`, the envelope is re-sent (up to `maxRetries`). The receiver sends the ack immediately after re-dispatching.
|
||||
|
||||
### 4. OTel span linking
|
||||
|
||||
On the sending side, a span `events_transport.send` is created linked to the current active span. On the receiving side, a span `events_transport.receive` is created with a `SpanLink` back to the sender span (via the propagated trace context), enabling cross-process Gantt chart visualisation.
|
||||
|
||||
### 5. Property-based tests (fast-check)
|
||||
|
||||
- Envelope round-trip: any structured-clone-safe payload survives `JSON.stringify` → `JSON.parse` round-trip unchanged
|
||||
- Nonce dedup: the same nonce delivered N times to a receiver results in exactly one re-dispatch
|
||||
- `BroadcastChannel` at-most-once: a dropped message is never delivered after disconnect
|
||||
- `MessageChannel` retry: message delivered after first ack timeout still reaches receiver exactly once
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/events/events-transport/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
core/
|
||||
transport-interface/
|
||||
transport-interface.ts
|
||||
transport-interface.md
|
||||
transport-interface.types.ts
|
||||
index.ts
|
||||
envelope/
|
||||
envelope.ts
|
||||
envelope.md
|
||||
envelope.types.ts
|
||||
envelope.test.ts
|
||||
index.ts
|
||||
nonce-window/
|
||||
nonce-window.ts
|
||||
nonce-window.md
|
||||
nonce-window.test.ts
|
||||
index.ts
|
||||
transports/
|
||||
message-channel/
|
||||
message-channel-transport.ts
|
||||
message-channel-transport.md
|
||||
message-channel-transport.types.ts
|
||||
message-channel-transport.test.ts
|
||||
index.ts
|
||||
broadcast-channel/
|
||||
broadcast-channel-transport.ts
|
||||
broadcast-channel-transport.md
|
||||
broadcast-channel-transport.types.ts
|
||||
broadcast-channel-transport.test.ts
|
||||
index.ts
|
||||
utils/
|
||||
structured-clone-check.ts
|
||||
structured-clone-check.md
|
||||
structured-clone-check.test.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream packages**: any application using Web Workers or multi-tab coordination via `BroadcastChannel` that needs `@suaveplan/events` to cross the context boundary.
|
||||
|
||||
**Workspace dep prerequisites**: `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types` must be published to Verdaccio. `@suaveplan/events-otel` is an optional peer for trace context injection.
|
||||
|
||||
**Breaking changes**: none (new package).
|
||||
@@ -0,0 +1,92 @@
|
||||
# Tasks: enterprise-events-transport
|
||||
|
||||
- **Change ID**: enterprise-events-transport
|
||||
- **Package**: `@suaveplan/events-transport`
|
||||
- **Date**: 2026-04-24
|
||||
|
||||
## Prerequisites (must be published to Verdaccio first)
|
||||
|
||||
- [x] `@suaveplan/events`
|
||||
- [x] `@suaveplan/error`
|
||||
- [x] `@suaveplan/types`
|
||||
|
||||
## Optional peer (for trace context injection)
|
||||
|
||||
- [x] `@suaveplan/events-otel` — if present, `serializeTraceContext`/`deserializeTraceContext` are used automatically
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1. Scaffold package
|
||||
|
||||
- [x] Create `universal/events/events-transport/` directory tree per file layout in `proposal.md`
|
||||
- [x] Write `package.json` (name `@suaveplan/events-transport`, version `0.1.0`, `sideEffects: false`, correct workspace deps and `@opentelemetry/api` catalog dep)
|
||||
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`
|
||||
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`
|
||||
|
||||
### 2. Error codes (`src/error-codes.ts`)
|
||||
|
||||
- [x] Define `EventsTransportErrorCode` union type
|
||||
- [x] Call `defineCodes` to register all `EVENTS_TRANSPORT.*` codes
|
||||
- [x] Export `registerEventsTransportErrorCodes()` and `EventsTransportError`
|
||||
|
||||
### 3. Types (`src/types/common.ts`)
|
||||
|
||||
- [x] Define and export `EventTransport`, `DeliveryGuarantee`, `ConnectOptions`, `TransportHandle`, `TransportStats`
|
||||
- [x] Write `common.md`
|
||||
|
||||
### 4. Transport envelope (`src/core/envelope/`)
|
||||
|
||||
- [x] Define `TransportEnvelope` interface and `TransportEnvelopeSchema` Zod schema
|
||||
- [x] Export stable envelope version constant `ENVELOPE_VERSION = 1`
|
||||
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
|
||||
|
||||
### 5. Nonce dedup window (`src/core/nonce-window/`)
|
||||
|
||||
- [x] Implement LRU sliding window of received nonces with configurable `windowSize`
|
||||
- [x] `has(nonce)` and `add(nonce)` methods
|
||||
- [x] Write `.md`, `.test.ts`, `index.ts`
|
||||
|
||||
### 6. Structured-clone safety check (`src/utils/structured-clone-check.ts`)
|
||||
|
||||
- [x] Implement `isStructuredCloneable(value)` — attempt `structuredClone(value)` and return false on error
|
||||
- [x] Implement `assertStructuredCloneable(value)` — throws `EventsTransportError("EVENTS_TRANSPORT_NOT_CLONEABLE")`
|
||||
- [x] Write `.md` and `.test.ts`
|
||||
|
||||
### 7. MessageChannel transport (`src/transports/message-channel/`)
|
||||
|
||||
- [x] Implement at-least-once delivery: send envelope, wait for ack within `ackTimeoutMs`, retry up to `maxRetries`
|
||||
- [x] Receiver sends ack immediately after re-dispatching
|
||||
- [x] Nonce dedup window prevents duplicate re-dispatch
|
||||
- [x] OTel span linking: `events_transport.send` and `events_transport.receive` linked via trace context in envelope
|
||||
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
|
||||
|
||||
### 8. BroadcastChannel transport (`src/transports/broadcast-channel/`)
|
||||
|
||||
- [x] Implement at-most-once delivery: fire-and-forget with nonce dedup on receive
|
||||
- [x] Optional origin filter
|
||||
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
|
||||
|
||||
### 9. Tests
|
||||
|
||||
- [x] Unit tests co-located (100% coverage target)
|
||||
- [x] `tests/integration.test.ts` — real MessageChannel and BroadcastChannel in Bun worker context; assert delivery and dedup behaviour
|
||||
- [x] `tests/property.test.ts` — fast-check properties per `proposal.md` §5
|
||||
|
||||
### 10. Docs
|
||||
|
||||
- [x] Every `.ts` implementation file has a sibling `.md`
|
||||
- [x] Root `README.md`
|
||||
|
||||
### 11. Pre-publish gate
|
||||
|
||||
- [x] `bunx turbo lint typecheck test --filter=@suaveplan/events-transport` green
|
||||
- [x] `bunx turbo test:coverage --filter=@suaveplan/events-transport` ≥ 100% on touched modules
|
||||
|
||||
### 12. Publish
|
||||
|
||||
- [x] `bun scripts/publish.ts --package @suaveplan/events-transport`
|
||||
|
||||
### 13. Archive
|
||||
|
||||
- [x] Fold spec delta into `openspec/specs/events/spec.md`
|
||||
- [x] `mv openspec/changes/enterprise-events-transport openspec/archive/2026-04-24-enterprise-events-transport`
|
||||
Reference in New Issue
Block a user