Files
2026-05-27 23:29:53 +01:00

7.8 KiB

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

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

// 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

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

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

// 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

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

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

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