first commit
This commit is contained in:
53
docs/networking/events/events-transport/README.md
Normal file
53
docs/networking/events/events-transport/README.md
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
||||
# envelope
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the `TransportEnvelope` wire format and provides `buildEnvelope` / `parseEnvelope` helpers for `@suaveplan/events-transport`. The envelope is the serialisable container that crosses context boundaries (worker threads, browser tabs). Every envelope carries a version tag, event name, structured-clone-safe payload, a dedup nonce, sender origin, timestamp, and optional W3C trace context headers.
|
||||
|
||||
The Zod schema validates all incoming envelopes at the receive boundary, throwing `EventsTransportError(EVENTS_TRANSPORT.ENVELOPE_INVALID)` for malformed data. This prevents processing corrupt or malicious messages from untrusted channels.
|
||||
|
||||
## Features
|
||||
|
||||
- `TransportEnvelope` — wire format type with v1 literal version tag
|
||||
- `TransportEnvelopeSchema` — Zod schema for validation and coercion
|
||||
- `buildEnvelope(event, payload, options)` — creates a typed envelope
|
||||
- `parseEnvelope(raw)` — validates and returns `TransportEnvelope` or throws
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { buildEnvelope, parseEnvelope } from "@suaveplan/events-transport";
|
||||
|
||||
const env = buildEnvelope("order.placed", { orderId: "o-1" }, {
|
||||
nonce: crypto.randomUUID(),
|
||||
origin: "tab-main",
|
||||
ts: Date.now(),
|
||||
});
|
||||
|
||||
const parsed = parseEnvelope(JSON.parse(JSON.stringify(env)));
|
||||
console.log(parsed.event); // "order.placed"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { buildEnvelope, parseEnvelope, TransportEnvelopeSchema } from "@suaveplan/events-transport";
|
||||
|
||||
// Build with trace context
|
||||
const env = buildEnvelope("user.created", { id: "u-1" }, {
|
||||
nonce: "abc-123",
|
||||
origin: "worker-1",
|
||||
ts: Date.now(),
|
||||
traceContext: { traceparent: "00-abc...123-01" },
|
||||
});
|
||||
|
||||
// Validate manually
|
||||
const result = TransportEnvelopeSchema.safeParse(env);
|
||||
if (!result.success) {
|
||||
console.error("invalid envelope");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `buildEnvelope(event, payload, options): TransportEnvelope`
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `event` | `string` | Event name |
|
||||
| `payload` | `unknown` | Structured-clone-safe payload |
|
||||
| `options.nonce` | `string` | Unique dedup key |
|
||||
| `options.origin` | `string` | Sender identifier |
|
||||
| `options.ts` | `number` | Sender timestamp (ms) |
|
||||
| `options.traceContext` | `Record<string,string>?` | W3C trace context headers |
|
||||
|
||||
### `parseEnvelope(raw): TransportEnvelope`
|
||||
|
||||
**Throws**: `EventsTransportError(EVENTS_TRANSPORT.ENVELOPE_INVALID)` if validation fails.
|
||||
|
||||
### `TransportEnvelopeSchema`
|
||||
|
||||
Zod schema for `TransportEnvelope`. Use directly when integrating with other Zod schemas.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`payload: z.unknown()` in the schema means the payload is not validated structurally — that is left to the event schema validation layer (`@suaveplan/events-schema`). The envelope only validates the envelope _frame_ (version, nonce, origin, ts), not the payload content. This separation keeps the transport layer payload-agnostic.
|
||||
@@ -0,0 +1,66 @@
|
||||
# nonce-window
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `NonceWindow`, a sliding-window LRU set for deduplicating transport envelopes by nonce. Both `MessageChannelTransport` and `BroadcastChannelTransport` use it to silently drop duplicate envelopes that arrive within the dedup window, which prevents double-processing under at-least-once delivery (retries) or network-level message duplication.
|
||||
|
||||
The window size is configurable — larger windows catch duplicates over longer time horizons at the cost of memory; smaller windows are cheaper but may miss duplicates that arrive after the window has evicted them.
|
||||
|
||||
## Features
|
||||
|
||||
- `NonceWindow(windowSize?)` — LRU sliding window; default size 1000
|
||||
- `check(nonce): boolean` — returns `true` (duplicate) or `false` (first-seen); records the nonce if not a duplicate
|
||||
- `size` getter — current number of tracked nonces
|
||||
- `clear()` — resets the window
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NonceWindow } from "@suaveplan/events-transport";
|
||||
|
||||
const window = new NonceWindow(100);
|
||||
window.check("nonce-1"); // false (new)
|
||||
window.check("nonce-1"); // true (duplicate)
|
||||
window.check("nonce-2"); // false (new)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NonceWindow } from "@suaveplan/events-transport";
|
||||
|
||||
// Fill to capacity and verify LRU eviction
|
||||
const window = new NonceWindow(2);
|
||||
window.check("a"); // false
|
||||
window.check("b"); // false
|
||||
window.check("c"); // false — evicts "a"
|
||||
window.check("a"); // false — "a" was evicted, treated as new
|
||||
window.check("b"); // true — "b" still in window
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new NonceWindow(windowSize?: number)`
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `windowSize` | `number?` | Max nonces to track (default: 1000) |
|
||||
|
||||
### `check(nonce: string): boolean`
|
||||
|
||||
Records `nonce` if not seen. Returns `true` if already in the window (duplicate), `false` if new.
|
||||
|
||||
### `size: number`
|
||||
|
||||
Current count of tracked nonces.
|
||||
|
||||
### `clear(): void`
|
||||
|
||||
Empties the window; all subsequent nonces will be treated as new.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation uses a `string[]` ring-buffer and a `Set<string>` for O(1) lookup. On eviction, the oldest entry is `shift()`ed from the array and deleted from the set. This gives O(1) amortized `check` for the common case. A more memory-efficient LRU cache could use a doubly-linked list + Map for O(1) worst-case, but for window sizes of 1000 the array approach is faster in practice due to cache locality.
|
||||
@@ -0,0 +1,79 @@
|
||||
# transport-interface
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the core `EventTransport` interface and supporting types (`ConnectOptions`, `TransportHandle`, `TransportStats`, `DeliveryGuarantee`) that all transport implementations in `@suaveplan/events-transport` must satisfy. This interface layer is pure types — no runtime code — enabling consumers to depend on it for typing without pulling in any implementation.
|
||||
|
||||
The design follows a connect-and-disconnect pattern: `transport.connect(bus, options?)` returns a `TransportHandle` with `disconnect()` and a `stats` snapshot. The `deliveryGuarantee` property documents the transport's contract so callers can make informed decisions about idempotency and retry at the application layer.
|
||||
|
||||
## Features
|
||||
|
||||
- `EventTransport` interface with `name`, `deliveryGuarantee`, and `connect`
|
||||
- `TransportHandle` with `disconnect()` and `stats` snapshot
|
||||
- `TransportStats` with `sent`, `received`, `dropped`, `duplicatesFiltered` counters
|
||||
- `DeliveryGuarantee` — `"at-most-once"` or `"at-least-once"`
|
||||
- `ConnectOptions` — `eventFilter`, `signal`, `logger`, `clock`, `rng`, `idGenerator`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { EventTransport, TransportHandle } from "@suaveplan/events-transport";
|
||||
|
||||
function withTransport(bus: EventBus, transport: EventTransport): () => void {
|
||||
const handle: TransportHandle = transport.connect(bus);
|
||||
console.log("guarantee:", transport.deliveryGuarantee);
|
||||
return () => handle.disconnect();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { EventTransport, ConnectOptions } from "@suaveplan/events-transport";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const bus = new EventBus();
|
||||
const transport: EventTransport = createMessageChannelTransport({ channel: new MessageChannel() });
|
||||
|
||||
const handle = transport.connect(bus, {
|
||||
eventFilter: (name) => name.startsWith("order."),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
|
||||
console.log(handle.stats.sent); // 0 initially
|
||||
console.log(handle.stats.received); // 0 initially
|
||||
handle.disconnect();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EventTransport`
|
||||
|
||||
| Member | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | `string` | Transport identifier |
|
||||
| `deliveryGuarantee` | `DeliveryGuarantee` | Delivery contract |
|
||||
| `connect(bus, options?)` | `TransportHandle` | Attach transport to the bus |
|
||||
|
||||
### `TransportHandle`
|
||||
|
||||
| Member | Type | Description |
|
||||
|---|---|---|
|
||||
| `disconnect()` | `void` | Detach from the bus |
|
||||
| `stats` | `TransportStats` | Live statistics |
|
||||
|
||||
### `TransportStats`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `sent` | `number` | Envelopes sent |
|
||||
| `received` | `number` | Envelopes received |
|
||||
| `dropped` | `number` | Envelopes dropped (non-cloneable, etc.) |
|
||||
| `duplicatesFiltered` | `number` | Duplicate nonces filtered |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `EventTransport.connect` method accepts an `EventBus<EventMap>` (the widest type) rather than a specific generic type. This avoids TypeScript covariance issues between different `EventBus<TMap>` types at the cost of losing per-event-type safety at the transport boundary. Type safety is enforced by the event bus itself when re-dispatching received payloads.
|
||||
@@ -0,0 +1,124 @@
|
||||
# broadcast-channel-transport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements an `EventTransport` over the browser `BroadcastChannel` API, enabling
|
||||
**at-most-once** event fan-out to every browser context (tab, worker, iframe) that has opened
|
||||
a `BroadcastChannel` with the same channel name. Unlike the `MessageChannel` transport, no
|
||||
acknowledgement is exchanged — delivery is fire-and-forget. This makes it ideal for cache
|
||||
invalidation, UI refresh signals, and other scenarios where occasional loss is acceptable and
|
||||
simplicity is preferred over reliability.
|
||||
|
||||
The transport filters out messages it sent itself (`origin === "local"`) to prevent event
|
||||
loops, and uses a `NonceWindow` ring buffer to deduplicate retransmissions from other
|
||||
transports bridged to the same bus.
|
||||
|
||||
## Features
|
||||
|
||||
- At-most-once delivery — no ack/retry overhead
|
||||
- Same-origin loop prevention via `origin` tag in envelope
|
||||
- Nonce-based duplicate filtering (configurable sliding-window)
|
||||
- Payload safety check — non-structured-cloneable values are dropped rather than throwing
|
||||
- AbortSignal support for clean shutdown
|
||||
- Injectable clock, ID generator, and event filter via `ConnectOptions`
|
||||
- Per-transport statistics: `sent`, `received`, `dropped`, `duplicatesFiltered`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createBroadcastChannelTransport } from "@suaveplan/events-transport/transports/broadcast-channel/broadcast-channel-transport";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const channel = new BroadcastChannel("app-events");
|
||||
const bus = new EventBus<{ "cache.invalidated": { key: string } }>();
|
||||
|
||||
const transport = createBroadcastChannelTransport({ channel });
|
||||
const handle = transport.connect(bus);
|
||||
|
||||
bus.on("cache.invalidated", (p) => console.log("invalidate", p.key));
|
||||
|
||||
await bus.emitAsync("cache.invalidated", { key: "user:42" });
|
||||
// Event is broadcast to all other contexts on the same channel name
|
||||
|
||||
handle.disconnect();
|
||||
channel.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createBroadcastChannelTransport } from "@suaveplan/events-transport/transports/broadcast-channel/broadcast-channel-transport";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const channel = new BroadcastChannel("notifications");
|
||||
const bus = new EventBus<{ "notification.show": { msg: string } }>();
|
||||
const ac = new AbortController();
|
||||
|
||||
const transport = createBroadcastChannelTransport({
|
||||
channel,
|
||||
nonceWindowSize: 200,
|
||||
});
|
||||
|
||||
const handle = transport.connect(bus, {
|
||||
signal: ac.signal,
|
||||
eventFilter: (name) => name.startsWith("notification."),
|
||||
clock: { now: () => performance.now() },
|
||||
idGenerator: { generate: () => crypto.randomUUID() },
|
||||
});
|
||||
|
||||
bus.on("notification.show", (p) => console.log(p.msg));
|
||||
|
||||
await bus.emitAsync("notification.show", { msg: "Hello from another tab!" });
|
||||
console.log(handle.stats.sent); // 1
|
||||
|
||||
ac.abort();
|
||||
handle.disconnect();
|
||||
channel.close();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createBroadcastChannelTransport(options): EventTransport`
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `options.channel` | `BroadcastChannel` | The broadcast channel to use |
|
||||
| `options.nonceWindowSize` | `number?` | Dedup window size. Default: `1000` |
|
||||
|
||||
**Returns**: `EventTransport` with `deliveryGuarantee: "at-most-once"`
|
||||
|
||||
### `EventTransport.connect(bus, options?): TransportHandle`
|
||||
|
||||
Connects the transport to an `EventBus`. Installs a `bus.use()` interceptor to capture
|
||||
outbound dispatches and a `channel.onmessage` listener for inbound envelopes.
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `bus` | `EventBus` | The bus to bridge |
|
||||
| `options.eventFilter` | `(name: string) => boolean` | Filter events. Default: all events |
|
||||
| `options.signal` | `AbortSignal?` | Cancellation |
|
||||
| `options.clock` | `Clock?` | Injectable clock |
|
||||
| `options.idGenerator` | `IdGenerator?` | Injectable nonce generator |
|
||||
|
||||
### `TransportHandle`
|
||||
|
||||
| Member | Type | Description |
|
||||
|---|---|---|
|
||||
| `disconnect()` | `() => void` | Uninstall interceptor and listener |
|
||||
| `stats` | `TransportStats` | Snapshot of sent/received/dropped/duplicatesFiltered counts |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Because `BroadcastChannel` delivers messages to every context **except** the sender, this
|
||||
transport does not need to track pending acks. However, the `origin: "local"` tag is still
|
||||
set on outbound envelopes so that if two tabs have the same logical bus connected via the
|
||||
same `BroadcastChannel`, a message sent from tab A will not be re-dispatched by tab A's
|
||||
own `onMessage` listener (which would cause a local loop if the same channel name were
|
||||
somehow looped back).
|
||||
|
||||
The dedup window guards against scenarios where multiple transports are bridged together —
|
||||
for example, a `MessageChannel` transport forwarding into a `BroadcastChannel` transport —
|
||||
where the same nonce could arrive from multiple paths.
|
||||
@@ -0,0 +1,127 @@
|
||||
# message-channel-transport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements an `EventTransport` over the browser and Node.js `MessageChannel` API, providing
|
||||
bidirectional, structured-cloneable event forwarding between two `MessagePort` endpoints with
|
||||
**at-least-once delivery** semantics. Every outbound event envelope is posted to `port1` and
|
||||
the sender waits for an explicit acknowledgement from the receiver on `port2` before recording
|
||||
the send as successful. If the ack does not arrive within the configured timeout, the message
|
||||
is retried up to `maxRetries` times; unacked messages are tracked as dropped in the transport
|
||||
statistics.
|
||||
|
||||
## Features
|
||||
|
||||
- At-least-once delivery via ack/retry protocol
|
||||
- Nonce-based duplicate filtering on the receive side (configurable sliding-window size)
|
||||
- Payload safety check — non-structured-cloneable values are dropped rather than throwing
|
||||
- Per-transport statistics: `sent`, `received`, `dropped`, `duplicatesFiltered`
|
||||
- AbortSignal support: stops outbound sends and inbound processing after abort
|
||||
- Injectable clock, ID generator, and event filter via `ConnectOptions`
|
||||
- Clean disconnect with pending ack cleanup
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createMessageChannelTransport } from "@suaveplan/events-transport/transports/message-channel/message-channel-transport";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const channel = new MessageChannel();
|
||||
const bus = new EventBus<{ "user.created": { id: string } }>();
|
||||
const transport = createMessageChannelTransport({ channel });
|
||||
const handle = transport.connect(bus);
|
||||
|
||||
bus.on("user.created", (p) => console.log("received", p.id));
|
||||
|
||||
await bus.emitAsync("user.created", { id: "u-1" });
|
||||
console.log(handle.stats.sent); // 1
|
||||
|
||||
handle.disconnect();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createMessageChannelTransport } from "@suaveplan/events-transport/transports/message-channel/message-channel-transport";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
|
||||
const channel = new MessageChannel();
|
||||
const busA = new EventBus<{ ping: { seq: number } }>();
|
||||
const busB = new EventBus<{ ping: { seq: number } }>();
|
||||
|
||||
const ac = new AbortController();
|
||||
|
||||
// Wire busA's port1 to busB's port2 via a shared channel
|
||||
const transportA = createMessageChannelTransport({
|
||||
channel,
|
||||
ackTimeoutMs: 2000,
|
||||
maxRetries: 5,
|
||||
nonceWindowSize: 500,
|
||||
});
|
||||
|
||||
const handleA = transportA.connect(busA, {
|
||||
signal: ac.signal,
|
||||
eventFilter: (name) => name.startsWith("ping"),
|
||||
});
|
||||
|
||||
// busB receives events
|
||||
busB.on("ping", (p) => console.log("busB got ping", p.seq));
|
||||
|
||||
// busA emits, arrives on busB
|
||||
await busA.emitAsync("ping", { seq: 1 });
|
||||
|
||||
console.log(handleA.stats); // { sent: 1, received: 0, dropped: 0, duplicatesFiltered: 0 }
|
||||
|
||||
ac.abort();
|
||||
handleA.disconnect();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createMessageChannelTransport(options): EventTransport`
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `options.channel` | `MessageChannel` | The channel to communicate over |
|
||||
| `options.ackTimeoutMs` | `number?` | Ack wait timeout in ms. Default: `5000` |
|
||||
| `options.maxRetries` | `number?` | Max retries before drop. Default: `3` |
|
||||
| `options.nonceWindowSize` | `number?` | Dedup window size. Default: `1000` |
|
||||
|
||||
**Returns**: `EventTransport` with `deliveryGuarantee: "at-least-once"`
|
||||
|
||||
### `EventTransport.connect(bus, options?): TransportHandle`
|
||||
|
||||
Connects the transport to an `EventBus`. Installs a `bus.use()` interceptor to capture outbound
|
||||
dispatches and a `port2` message listener to receive inbound envelopes.
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `bus` | `EventBus` | The bus to bridge |
|
||||
| `options.eventFilter` | `(name: string) => boolean` | Filter events. Default: all events |
|
||||
| `options.signal` | `AbortSignal?` | Cancellation |
|
||||
| `options.clock` | `Clock?` | Injectable clock |
|
||||
| `options.idGenerator` | `IdGenerator?` | Injectable nonce generator |
|
||||
|
||||
### `TransportHandle`
|
||||
|
||||
| Member | Type | Description |
|
||||
|---|---|---|
|
||||
| `disconnect()` | `() => void` | Uninstall interceptor, stop listener, clear pending acks |
|
||||
| `stats` | `TransportStats` | Snapshot of sent/received/dropped/duplicatesFiltered counts |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The ack protocol uses a `Map<string, () => void>` of pending ack resolvers keyed by nonce.
|
||||
When `port1.postMessage(envelope)` is sent, a `Promise<boolean>` is created that resolves
|
||||
`true` when the matching `{ ack: nonce }` reply arrives on `port2`, or `false` when the
|
||||
timeout fires. The retry loop retries the full postMessage on each `false` result.
|
||||
|
||||
Non-structured-cloneable payloads are silently dropped (incrementing `stats.dropped`) rather
|
||||
than throwing because the interceptor runs after `next()` — the local dispatch has already
|
||||
succeeded. Throwing here would be misleading since the event was processed locally.
|
||||
|
||||
Duplicate filtering on receive uses a `NonceWindow` ring buffer so the sender can safely retry
|
||||
without the receiver processing the same event twice.
|
||||
@@ -0,0 +1,64 @@
|
||||
# structured-clone-check
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `isStructuredCloneable` and `assertStructuredCloneable` utilities for `@suaveplan/events-transport`. These functions validate that an event payload can be safely transmitted across context boundaries via the structured clone algorithm used by `MessageChannel` and `BroadcastChannel`. Payloads that fail the check are dropped before transmission to prevent runtime `DataCloneError` exceptions.
|
||||
|
||||
The check is conservative — it allows all primitives, plain objects, arrays, and well-known cloneable builtins (Date, RegExp, Map, Set, ArrayBuffer, TypedArrays, Error), and rejects functions, symbols, WeakMap, WeakSet, and class instances with custom prototypes.
|
||||
|
||||
## Features
|
||||
|
||||
- `isStructuredCloneable(value)` — recursive check for clone-safety
|
||||
- `assertStructuredCloneable(value)` — throws `EventsTransportError(NOT_CLONEABLE)` on failure
|
||||
- Handles nested objects and arrays
|
||||
- Allows Date, RegExp, Map, Set, ArrayBuffer, TypedArray, Error
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { isStructuredCloneable } from "@suaveplan/events-transport";
|
||||
|
||||
isStructuredCloneable({ id: "x", count: 1 }); // true
|
||||
isStructuredCloneable(() => console.log("fn")); // false
|
||||
isStructuredCloneable(Symbol("sym")); // false
|
||||
isStructuredCloneable(new Date()); // true
|
||||
isStructuredCloneable(new Map([["k", "v"]])); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { assertStructuredCloneable, isStructuredCloneable } from "@suaveplan/events-transport";
|
||||
import { EventsTransportError } from "@suaveplan/events-transport";
|
||||
|
||||
// Assert before transmission
|
||||
try {
|
||||
assertStructuredCloneable({ fn: () => {} });
|
||||
} catch (err) {
|
||||
if (err instanceof EventsTransportError) {
|
||||
console.log(err.code); // "EVENTS_TRANSPORT.NOT_CLONEABLE"
|
||||
}
|
||||
}
|
||||
|
||||
// Safe payloads
|
||||
isStructuredCloneable(new Uint8Array([1, 2, 3])); // true (TypedArray)
|
||||
isStructuredCloneable(new Set([1, 2, 3])); // true (Set)
|
||||
isStructuredCloneable(new WeakMap()); // false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `isStructuredCloneable(value: unknown): boolean`
|
||||
|
||||
Returns `true` if the value can be transmitted via structured clone.
|
||||
|
||||
### `assertStructuredCloneable(value: unknown): void`
|
||||
|
||||
**Throws**: `EventsTransportError(EVENTS_TRANSPORT.NOT_CLONEABLE)` if value is not cloneable.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The check uses `instanceof` for known builtins and `Object.getPrototypeOf` to detect class instances. It is recursive for arrays and plain objects. Custom class instances (those with a non-Object, non-null prototype that are not whitelisted) are rejected as non-cloneable. This is a conservative heuristic — some custom classes may be cloneable if they implement the `[Symbol.for("nodejs.util.inspect.custom")]` or structuredClone algorithm, but the safe default is to reject them. The actual `structuredClone()` call is not used for the check to avoid the cost of deep-cloning on every dispatch.
|
||||
Reference in New Issue
Block a user