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).
|
||||
Reference in New Issue
Block a user