# 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 // 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 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).