243 lines
7.8 KiB
Markdown
243 lines
7.8 KiB
Markdown
# 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).
|