crossws 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,7 +62,7 @@ const StubRequest = /* @__PURE__ */ (() => {
62
62
  return Promise.resolve(new Blob());
63
63
  }
64
64
  bytes() {
65
- return Promise.resolve(new Uint8Array());
65
+ return Promise.resolve(/* @__PURE__ */ new Uint8Array());
66
66
  }
67
67
  formData() {
68
68
  return Promise.resolve(new FormData());
@@ -2,14 +2,378 @@ import { WebSocket } from "./web.mjs";
2
2
  declare class WSError extends Error {
3
3
  constructor(...args: any[]);
4
4
  }
5
+ /**
6
+ * A single pub/sub event relayed between crossws instances.
7
+ *
8
+ * This is the minimal unit a sync backplane needs to transport so that a
9
+ * `peer.publish()` (or `adapter.publish()`) on one instance reaches the
10
+ * subscribers connected to every other instance.
11
+ */
12
+ interface SyncMessage {
13
+ /**
14
+ * Pub/sub namespace (matches {@link Peer.namespace}).
15
+ *
16
+ * An empty string means "all namespaces" and mirrors a server-side
17
+ * `adapter.publish(topic, data)` call without an explicit `namespace`.
18
+ */
19
+ namespace: string;
20
+ /** Channel / topic name. */
21
+ topic: string;
22
+ /**
23
+ * Message payload.
24
+ *
25
+ * crossws normalizes payloads to a string or `Uint8Array` before handing
26
+ * them to the driver. Encoding for the wire (e.g. base64 for binary over a
27
+ * text transport) is the driver's responsibility.
28
+ */
29
+ data: string | Uint8Array;
30
+ }
31
+ /**
32
+ * A live connection to a sync backplane, scoped to one crossws instance.
33
+ *
34
+ * Created by a {@link SyncAdapter}. crossws calls {@link SyncDriver.publish}
35
+ * for every local publish and expects {@link SyncDriver.subscribe} to invoke
36
+ * the supplied `deliver` callback for every message originating from *other*
37
+ * instances.
38
+ */
39
+ interface SyncDriver {
40
+ /**
41
+ * Start receiving messages relayed from other instances.
42
+ *
43
+ * The driver MUST call `deliver` for every remote message; crossws then
44
+ * fans it out to local subscribers. The driver MUST NOT echo this
45
+ * instance's own publishes back (backplanes like Redis pub/sub echo by
46
+ * default — use the instance `id` from {@link SyncAdapter} to filter).
47
+ */
48
+ subscribe(deliver: (message: SyncMessage) => void): MaybePromise<void>;
49
+ /** Relay a locally-published message to the other instances. */
50
+ publish(message: SyncMessage): MaybePromise<void>;
51
+ /** Optional teardown when the adapter shuts down. */
52
+ close?(): MaybePromise<void>;
53
+ }
54
+ /**
55
+ * Factory for a {@link SyncDriver}.
56
+ *
57
+ * crossws calls it once per adapter instance and passes a stable random `id`
58
+ * the driver can use for echo suppression.
59
+ */
60
+ type SyncAdapter = (ctx: {
61
+ id: string;
62
+ }) => SyncDriver;
63
+ /**
64
+ * Zero-dependency sync driver built on [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).
65
+ *
66
+ * Bridges instances that share a `BroadcastChannel` registry. On Node.js, Deno
67
+ * and Bun that registry is scoped to a **single process** (it spans the main
68
+ * thread and its worker threads), so this is for in-process worker fan-out and
69
+ * tests — not separate OS processes (e.g. Node `cluster`/PM2 forks), which each
70
+ * have an isolated registry and will silently not sync. (Deno Deploy is the one
71
+ * exception: its `BroadcastChannel` spans isolates.)
72
+ *
73
+ * For multiple processes, hosts or regions you want a networked driver such as
74
+ * {@link redis} or {@link pgsql}.
75
+ *
76
+ * A `channel` name is required: it scopes the cluster, and a shared default
77
+ * would risk silently bridging unrelated servers running on the same host.
78
+ *
79
+ * @example
80
+ * ```js
81
+ * import { broadcastChannel } from "crossws/sync";
82
+ * const adapter = nodeAdapter({ hooks, sync: broadcastChannel({ channel: "my-app" }) });
83
+ * ```
84
+ */
85
+ declare function broadcastChannel(opts: {
86
+ channel: string;
87
+ }): SyncAdapter;
88
+ /**
89
+ * Structural subset of a Redis client used by {@link redis}, covering both
90
+ * [ioredis](https://github.com/redis/ioredis) and
91
+ * [node-redis](https://github.com/redis/node-redis). Kept structural so crossws
92
+ * stays dependency-free.
93
+ *
94
+ * The two clients shape pub/sub differently and {@link redis} bridges them:
95
+ * - **ioredis** — `subscribe(channel)` plus a shared `"message"` event whose
96
+ * listener receives `(channel, message)`; `duplicate()` returns a ready client.
97
+ * - **node-redis** — `subscribe(channel, listener)` with an inline listener that
98
+ * receives `(message, channel)`; `duplicate()` returns a client you must
99
+ * `connect()` first.
100
+ */
101
+ interface RedisClientLike {
102
+ publish(channel: string, message: string): unknown;
103
+ /**
104
+ * ioredis: `subscribe(channel)`; node-redis: `subscribe(channel, listener)`
105
+ * (the listener receives `(message, channel)`).
106
+ */
107
+ subscribe(channel: string, listener?: (message: string, channel: string) => void): unknown;
108
+ /** ioredis: shared `"message"` event (listener receives `(channel, message)`). */
109
+ on?(event: "message", listener: (channel: string, message: string) => void): unknown;
110
+ /** ioredis: detach the `"message"` listener on {@link SyncDriver.close}. */
111
+ off?(event: "message", listener: (channel: string, message: string) => void): unknown;
112
+ /** node-redis: a `duplicate()`d client starts disconnected and must `connect()`. */
113
+ connect?(): unknown;
114
+ /** Create a second connection for `SUBSCRIBE` (which blocks the connection). */
115
+ duplicate(): RedisClientLike;
116
+ /** Tear down the dedicated subscriber connection on {@link SyncDriver.close}. */
117
+ quit?(): unknown;
118
+ /** Present (camelCase) only on node-redis — used for auto-detection. */
119
+ pSubscribe?: unknown;
120
+ }
121
+ /**
122
+ * Networked sync driver over Redis pub/sub — the realistic multi-region
123
+ * backplane. Bring your own client; a dedicated `SUBSCRIBE` connection is
124
+ * derived from it via `duplicate()` (`SUBSCRIBE` blocks the connection it runs
125
+ * on, so it can't share the one used for `PUBLISH`).
126
+ *
127
+ * Works out of the box with both [ioredis](https://github.com/redis/ioredis)
128
+ * and [node-redis](https://github.com/redis/node-redis): the flavor is
129
+ * auto-detected (node-redis exposes camelCase commands such as `pSubscribe`),
130
+ * with an explicit `connector` escape hatch if detection ever guesses wrong.
131
+ *
132
+ * Binary payloads are base64-encoded so they survive Redis's text transport.
133
+ *
134
+ * A `channel` name is required: it scopes the cluster, and a shared default
135
+ * would risk silently bridging unrelated servers on the same Redis instance.
136
+ *
137
+ * Reconnect note: ioredis auto-resubscribes its channels after a dropped
138
+ * connection; node-redis does not restore subscriptions the same way, so a
139
+ * node-redis-backed instance may stop receiving relayed messages after a
140
+ * transient outage. Prefer ioredis where connection resilience matters.
141
+ *
142
+ * @example
143
+ * ```js
144
+ * // ioredis
145
+ * import Redis from "ioredis";
146
+ * import { redis } from "crossws/sync";
147
+ * const adapter = nodeAdapter({ hooks, sync: redis({ client: new Redis(), channel: "my-app" }) });
148
+ * ```
149
+ *
150
+ * @example
151
+ * ```js
152
+ * // node-redis
153
+ * import { createClient } from "redis";
154
+ * import { redis } from "crossws/sync";
155
+ * const client = await createClient().connect();
156
+ * const adapter = nodeAdapter({ hooks, sync: redis({ client, channel: "my-app" }) });
157
+ * ```
158
+ */
159
+ declare function redis(opts: {
160
+ /** Redis client used to `PUBLISH`; a subscriber is `duplicate()`d from it. */client: RedisClientLike; /** Pub/sub channel to relay over. */
161
+ channel: string;
162
+ /**
163
+ * Client flavor. Defaults to auto-detection (node-redis exposes the camelCase
164
+ * `pSubscribe` command; ioredis does not). Set explicitly to override.
165
+ */
166
+ connector?: "ioredis" | "node-redis";
167
+ }): SyncAdapter;
168
+ /**
169
+ * Structural subset of a PostgreSQL client used by {@link pgsql}, covering
170
+ * both [node-postgres](https://github.com/brianc/node-postgres) (`pg`) and
171
+ * [postgres.js](https://github.com/porsager/postgres). Kept structural so
172
+ * crossws stays dependency-free.
173
+ *
174
+ * The two clients shape `LISTEN`/`NOTIFY` differently and {@link pgsql}
175
+ * bridges them:
176
+ * - **node-postgres** — issue raw `LISTEN`/`pg_notify` via `query()` and receive
177
+ * a shared `"notification"` event whose listener gets `{ channel, payload }`.
178
+ * - **postgres.js** — dedicated `listen(channel, onnotify)` (resolves to a
179
+ * handle with `unlisten()`) and `notify(channel, payload)` helpers.
180
+ */
181
+ interface PostgresClientLike {
182
+ /** node-postgres: run `LISTEN` / `SELECT pg_notify(...)` / `UNLISTEN`. */
183
+ query?(sql: string, values?: unknown[]): unknown;
184
+ /** node-postgres: shared `"notification"` event for inbound `NOTIFY`s. */
185
+ on?(event: "notification", listener: (msg: {
186
+ channel: string;
187
+ payload?: string;
188
+ }) => void): unknown;
189
+ /** node-postgres: detach the `"notification"` listener on {@link SyncDriver.close}. */
190
+ removeListener?(event: "notification", listener: (msg: {
191
+ channel: string;
192
+ payload?: string;
193
+ }) => void): unknown;
194
+ /**
195
+ * postgres.js: subscribe to a channel; resolves to a handle exposing
196
+ * `unlisten()`. Present only on postgres.js — used for auto-detection.
197
+ */
198
+ listen?(channel: string, onnotify: (payload: string) => void): unknown;
199
+ /** postgres.js: send a `NOTIFY` to a channel. */
200
+ notify?(channel: string, payload: string): unknown;
201
+ }
202
+ /**
203
+ * Networked sync driver over PostgreSQL [`LISTEN`/`NOTIFY`](https://www.postgresql.org/docs/current/sql-notify.html)
204
+ * — a backplane for clusters that already run Postgres and would rather not add
205
+ * Redis. Bring your own client; unlike Redis `SUBSCRIBE`, Postgres `LISTEN` does
206
+ * not block the connection, so no `duplicate()` is needed: for node-postgres the
207
+ * *same* client both listens and notifies. (postgres.js `listen()` internally
208
+ * reserves its own dedicated connection — that's the client's concern, not ours.)
209
+ *
210
+ * Pass a single, dedicated [`Client`](https://node-postgres.com/apis/client), not
211
+ * a [`Pool`](https://node-postgres.com/apis/pool): pool `query()` runs `LISTEN`
212
+ * on an arbitrary backend that is then returned to the pool, so notifications
213
+ * would never reach a stable listener. A `Pool` is detected and rejected. For the
214
+ * same reason, don't share one client across two `pgsql()` drivers on the same
215
+ * channel — `close()` issues a single `UNLISTEN` that would silence the others.
216
+ *
217
+ * Works out of the box with both [node-postgres](https://github.com/brianc/node-postgres)
218
+ * (`pg`) and [postgres.js](https://github.com/porsager/postgres): the flavor is
219
+ * auto-detected (postgres.js exposes a `listen()` helper; node-postgres does
220
+ * not), with an explicit `connector` escape hatch if detection ever guesses
221
+ * wrong.
222
+ *
223
+ * Binary payloads are base64-encoded so they survive the text `NOTIFY` payload.
224
+ * Note Postgres caps a `NOTIFY` payload at 8000 bytes — keep relayed messages
225
+ * small (this is a transport limit, not a crossws one).
226
+ *
227
+ * A `channel` name is required: it scopes the cluster, so unrelated servers
228
+ * don't silently bridge through the same database. It is used verbatim as the
229
+ * notification channel name.
230
+ *
231
+ * @example
232
+ * ```js
233
+ * // node-postgres (pg)
234
+ * import { Client } from "pg";
235
+ * import { pgsql } from "crossws/sync";
236
+ * const client = new Client();
237
+ * await client.connect();
238
+ * const adapter = nodeAdapter({ hooks, sync: pgsql({ client, channel: "my-app" }) });
239
+ * ```
240
+ *
241
+ * @example
242
+ * ```js
243
+ * // postgres.js
244
+ * import postgresjs from "postgres";
245
+ * import { pgsql } from "crossws/sync";
246
+ * const sql = postgresjs();
247
+ * const adapter = nodeAdapter({ hooks, sync: pgsql({ client: sql, channel: "my-app" }) });
248
+ * ```
249
+ */
250
+ declare function pgsql(opts: {
251
+ /** Connected Postgres client used to both `LISTEN` and `NOTIFY`. */client: PostgresClientLike; /** Notification channel to relay over. */
252
+ channel: string;
253
+ /**
254
+ * Client flavor. Defaults to auto-detection (postgres.js exposes a `listen()`
255
+ * helper; node-postgres does not). Set explicitly to override.
256
+ */
257
+ connector?: "pg" | "postgres.js";
258
+ }): SyncAdapter;
259
+ /**
260
+ * Install the cluster relay in the **primary** process.
261
+ *
262
+ * Node `cluster` workers can't message each other directly — IPC only flows
263
+ * between each worker and the primary — so the primary must rebroadcast every
264
+ * worker's relay message to the others. Call this once in your primary process
265
+ * (before or after forking) so {@link cluster} drivers running in the workers
266
+ * can reach one another. It is a no-op when called from a worker, so guarding
267
+ * with `cluster.isPrimary` is optional.
268
+ *
269
+ * `node:cluster` is imported lazily so merely importing `crossws/sync` stays
270
+ * runtime-agnostic (it never loads in workerd/Deno where the module is unused).
271
+ *
272
+ * @example
273
+ * ```js
274
+ * import cluster from "node:cluster";
275
+ * import { availableParallelism } from "node:os";
276
+ * import { setupPrimaryCluster } from "crossws/sync";
277
+ *
278
+ * if (cluster.isPrimary) {
279
+ * setupPrimaryCluster();
280
+ * for (let i = 0; i < availableParallelism(); i++) cluster.fork();
281
+ * } else {
282
+ * // ... start your server with `sync: cluster({ channel: "my-app" })`
283
+ * }
284
+ * ```
285
+ */
286
+ declare function setupPrimaryCluster(): Promise<void>;
287
+ /**
288
+ * Zero-dependency sync driver over Node.js [`cluster`](https://nodejs.org/api/cluster.html)
289
+ * worker IPC — bridges forked processes on a **single host** (e.g. Node
290
+ * `cluster` or PM2 `instances`) without a network backplane.
291
+ *
292
+ * This fills the gap left by {@link broadcastChannel}, whose registry is scoped
293
+ * to one process and silently won't sync across forks. For multiple hosts or
294
+ * regions you still want a networked driver such as {@link redis} or
295
+ * {@link pgsql}.
296
+ *
297
+ * Requires the relay to be installed in the primary via
298
+ * {@link setupPrimaryCluster}; the driver itself runs in the workers. Workers
299
+ * can't message each other directly, so all relay flows worker → primary →
300
+ * workers. Binary payloads are base64-encoded so they survive default JSON IPC
301
+ * serialization (no `serialization: "advanced"` needed).
302
+ *
303
+ * A `channel` name is required: it scopes the cluster and lets multiple apps
304
+ * share one process tree without bridging into each other.
305
+ *
306
+ * @example
307
+ * ```js
308
+ * import cluster from "node:cluster";
309
+ * import { setupPrimaryCluster, cluster as clusterSync } from "crossws/sync";
310
+ *
311
+ * if (cluster.isPrimary) {
312
+ * setupPrimaryCluster();
313
+ * cluster.fork();
314
+ * cluster.fork();
315
+ * } else {
316
+ * const ws = nodeAdapter({ hooks, sync: clusterSync({ channel: "my-app" }) });
317
+ * // ... start the server
318
+ * }
319
+ * ```
320
+ */
321
+ declare function cluster(opts: {
322
+ channel: string;
323
+ }): SyncAdapter;
324
+ declare function encodeEnvelope(id: string, msg: SyncMessage): string;
325
+ declare function decodeEnvelope(raw: string): {
326
+ id: string;
327
+ msg: SyncMessage;
328
+ } | undefined;
329
+ /**
330
+ * Names of the built-in sync drivers exported from `crossws/sync`.
331
+ *
332
+ * Useful for automatic integration (e.g. Nitro) that needs to enumerate the
333
+ * available drivers without importing each one.
334
+ */
335
+ declare const syncDrivers: readonly ["broadcastChannel", "redis", "pgsql", "cluster"];
336
+ /** Name of a built-in sync driver (see {@link syncDrivers}). */
337
+ type SyncDriverName = (typeof syncDrivers)[number];
338
+ /**
339
+ * Map from a {@link SyncDriverName} to the options object its driver factory
340
+ * accepts — derived from the factory signatures so it stays in sync.
341
+ */
342
+ type SyncDriverOptions = {
343
+ broadcastChannel: Parameters<typeof broadcastChannel>[0];
344
+ redis: Parameters<typeof redis>[0];
345
+ pgsql: Parameters<typeof pgsql>[0];
346
+ cluster: Parameters<typeof cluster>[0];
347
+ };
5
348
  declare const kNodeInspect: unique symbol;
6
349
  interface PeerContext extends Record<string, unknown> {}
350
+ interface WaitForDrainOptions {
351
+ /**
352
+ * Resolve once {@link Peer.bufferedAmount} drops to or below this many bytes.
353
+ *
354
+ * @default 0
355
+ */
356
+ threshold?: number;
357
+ /**
358
+ * Polling interval (in milliseconds) used to re-check {@link Peer.bufferedAmount}.
359
+ *
360
+ * @default 100
361
+ */
362
+ pollInterval?: number;
363
+ /**
364
+ * Abort the wait (e.g. `AbortSignal.timeout(ms)`). The returned promise
365
+ * rejects with the signal's `reason`.
366
+ */
367
+ signal?: AbortSignal;
368
+ }
7
369
  interface AdapterInternal {
8
370
  ws: unknown;
9
371
  request: Request;
10
372
  namespace: string;
11
373
  peers?: Set<Peer>;
12
374
  context?: PeerContext;
375
+ /** Optional sync backplane used to relay publishes to other instances. */
376
+ sync?: SyncDriver;
13
377
  }
14
378
  declare abstract class Peer<Internal extends AdapterInternal = AdapterInternal> {
15
379
  #private;
@@ -40,6 +404,34 @@ declare abstract class Peer<Internal extends AdapterInternal = AdapterInternal>
40
404
  get peers(): Set<Peer>;
41
405
  /** All topics, this peer has been subscribed to. */
42
406
  get topics(): Set<string>;
407
+ /**
408
+ * Number of bytes queued for transmission but not yet flushed to the client.
409
+ *
410
+ * Use this to apply backpressure: pause sending while it grows past a high
411
+ * watermark and resume once it drops (or on the `drain` hook). Returns `0` on
412
+ * adapters that do not expose a buffer signal. Refer to the
413
+ * [compatibility table](https://crossws.h3.dev/guide/peer#compatibility).
414
+ */
415
+ get bufferedAmount(): number;
416
+ /**
417
+ * Wait until the send buffer drains to `threshold` bytes (default `0`).
418
+ *
419
+ * Resolves immediately when there is no backpressure (or on adapters that do
420
+ * not expose {@link Peer.bufferedAmount}). Otherwise it polls every
421
+ * `pollInterval` milliseconds until the buffer drains, also resolving early if
422
+ * the connection is no longer open so a send loop never hangs on a dropped
423
+ * client.
424
+ *
425
+ * ```ts
426
+ * for (const chunk of stream) {
427
+ * peer.send(chunk);
428
+ * if (peer.bufferedAmount > 1024 * 1024) {
429
+ * await peer.waitForDrain({ threshold: 256 * 1024 });
430
+ * }
431
+ * }
432
+ * ```
433
+ */
434
+ waitForDrain(opts?: WaitForDrainOptions): Promise<void>;
43
435
  abstract close(code?: number, reason?: string): void;
44
436
  /** Abruptly close the connection */
45
437
  terminate(): void;
@@ -51,8 +443,23 @@ declare abstract class Peer<Internal extends AdapterInternal = AdapterInternal>
51
443
  abstract send(data: unknown, options?: {
52
444
  compress?: boolean;
53
445
  }): number | void | undefined;
54
- /** Send message to subscribes of topic */
55
- abstract publish(topic: string, data: unknown, options?: {
446
+ /**
447
+ * Send a message to subscribers of a topic.
448
+ *
449
+ * When a sync backplane is configured, the message is also relayed to the
450
+ * other crossws instances so their subscribers receive it too.
451
+ */
452
+ publish(topic: string, data: unknown, options?: {
453
+ compress?: boolean;
454
+ }): void;
455
+ /**
456
+ * Adapter-specific, relay-free local fan-out to subscribers of a topic
457
+ * (excludes this peer). Implemented by each adapter; used both by
458
+ * {@link Peer.publish} and by the internal cross-instance delivery path.
459
+ *
460
+ * @internal Adapter extension point, not part of the stable public API.
461
+ */
462
+ abstract _publish(topic: string, data: unknown, options?: {
56
463
  compress?: boolean;
57
464
  }): void;
58
465
  toString(): string;
@@ -146,6 +553,14 @@ interface Hooks {
146
553
  code?: number;
147
554
  reason?: string;
148
555
  }) => MaybePromise<void>;
556
+ /**
557
+ * The send buffer has drained after backpressure, so it is safe to resume
558
+ * sending. Pair with {@link Peer.bufferedAmount} to throttle senders.
559
+ *
560
+ * **Note:** Only emitted by adapters that expose a drain signal. Refer to the
561
+ * [compatibility table](https://crossws.h3.dev/guide/peer#compatibility).
562
+ */
563
+ drain: (peer: Peer) => MaybePromise<void>;
149
564
  /** An error occurs */
150
565
  error: (peer: Peer, error: WSError) => MaybePromise<void>;
151
566
  }
@@ -155,12 +570,51 @@ interface AdapterInstance {
155
570
  compress?: boolean;
156
571
  namespace?: string;
157
572
  }) => void;
573
+ /**
574
+ * Gracefully shut the adapter down: close every connected peer (with the
575
+ * optional `code` / `reason`) and tear down the {@link AdapterInstance.sync}
576
+ * backplane. Any underlying server you created (e.g. an `http.Server` or a
577
+ * `WebSocketServer` passed via options) stays yours to close.
578
+ */
579
+ readonly close: (code?: number, reason?: string) => Promise<void>;
580
+ /**
581
+ * Sync backplane driver, present when an adapter is created with `sync`.
582
+ *
583
+ * Closed automatically by {@link AdapterInstance.close}; it leaves any
584
+ * user-owned client (Redis/Postgres) connected.
585
+ */
586
+ readonly sync?: SyncDriver;
587
+ }
588
+ /** Context passed to {@link AdapterOptions.onError} describing what failed. */
589
+ interface SyncErrorContext {
590
+ /**
591
+ * Which backplane operation failed:
592
+ * - `subscribe` — the initial subscription to the backplane.
593
+ * - `publish` — relaying a local publish out to the other instances.
594
+ * - `delivery` — fanning an inbound remote message out to local subscribers.
595
+ */
596
+ stage: "subscribe" | "publish" | "delivery";
158
597
  }
159
598
  interface AdapterOptions {
160
599
  resolve?: ResolveHooks;
161
600
  getNamespace?: (request: Request) => string;
162
601
  hooks?: Partial<Hooks>;
602
+ /**
603
+ * Optional sync backplane to relay pub/sub between multiple crossws
604
+ * instances (e.g. across regions/processes). Opt-in: when absent, pub/sub
605
+ * stays local to the instance, exactly as before.
606
+ */
607
+ sync?: SyncAdapter;
608
+ /**
609
+ * Called when a {@link AdapterOptions.sync} backplane operation fails.
610
+ *
611
+ * Relay is fire-and-forget by design — a flaky backplane never throws into
612
+ * your `publish` call or crashes the process — so this callback is the only
613
+ * way to observe a degraded backplane (for logging, metrics or alerting).
614
+ * Defaults to `console.error`. Has no effect without `sync`.
615
+ */
616
+ onError?: (error: unknown, context: SyncErrorContext) => void;
163
617
  }
164
618
  type Adapter<AdapterT extends AdapterInstance = AdapterInstance, Options extends AdapterOptions = AdapterOptions> = (options?: Options) => AdapterT;
165
619
  declare function defineWebSocketAdapter<AdapterT extends AdapterInstance = AdapterInstance, Options extends AdapterOptions = AdapterOptions>(factory: Adapter<AdapterT, Options>): Adapter<AdapterT, Options>;
166
- export { Adapter, AdapterInstance, AdapterInternal, AdapterOptions, Hooks, Message, Peer, PeerContext, ResolveHooks, WSError, defineHooks, defineWebSocketAdapter };
620
+ export { Adapter, AdapterInstance, AdapterInternal, AdapterOptions, Hooks, Message, Peer, PeerContext, PostgresClientLike, RedisClientLike, ResolveHooks, SyncAdapter, SyncDriver, SyncDriverName, SyncDriverOptions, SyncErrorContext, SyncMessage, WSError, WaitForDrainOptions, broadcastChannel, cluster, decodeEnvelope, defineHooks, defineWebSocketAdapter, encodeEnvelope, pgsql, redis, setupPrimaryCluster, syncDrivers };
@@ -59,21 +59,98 @@ var AdapterHookable = class {
59
59
  function defineHooks(hooks) {
60
60
  return hooks;
61
61
  }
62
- function adapterUtils(globalPeers) {
62
+ const kNodeInspect = /*#__PURE__*/ Symbol.for("nodejs.util.inspect.custom");
63
+ function toBufferLike(val) {
64
+ if (val === void 0 || val === null) return "";
65
+ const type = typeof val;
66
+ if (type === "string") return val;
67
+ if (type === "number" || type === "boolean" || type === "bigint") return val.toString();
68
+ if (type === "function" || type === "symbol") return "{}";
69
+ if (val instanceof Uint8Array || val instanceof ArrayBuffer) return val;
70
+ if (isPlainObject(val)) return JSON.stringify(val);
71
+ return val;
72
+ }
73
+ function serializeMessage(val) {
74
+ const data = toBufferLike(val);
75
+ if (typeof data === "string") return data;
76
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
77
+ }
78
+ function toString(val) {
79
+ if (typeof val === "string") return val;
80
+ const data = toBufferLike(val);
81
+ if (typeof data === "string") return data;
82
+ let binary = "";
83
+ for (const byte of new Uint8Array(data)) binary += String.fromCharCode(byte);
84
+ return `data:application/octet-stream;base64,${btoa(binary)}`;
85
+ }
86
+ function isPlainObject(value) {
87
+ if (value === null || typeof value !== "object") return false;
88
+ const prototype = Object.getPrototypeOf(value);
89
+ if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) return false;
90
+ if (Symbol.iterator in value) return false;
91
+ if (Symbol.toStringTag in value) return Object.prototype.toString.call(value) === "[object Module]";
92
+ return true;
93
+ }
94
+ function adapterUtils(globalPeers, options, caps) {
95
+ const localPublish = (topic, message, pubOptions) => {
96
+ for (const peers of pubOptions?.namespace ? [globalPeers.get(pubOptions.namespace) || []] : globalPeers.values()) {
97
+ let firstPeerWithTopic;
98
+ for (const peer of peers) if (peer.topics.has(topic)) {
99
+ firstPeerWithTopic = peer;
100
+ break;
101
+ }
102
+ if (firstPeerWithTopic) {
103
+ firstPeerWithTopic.send(message, pubOptions);
104
+ firstPeerWithTopic._publish(topic, message, pubOptions);
105
+ if (caps?.nativePubSub && !pubOptions?.namespace) break;
106
+ }
107
+ }
108
+ };
109
+ let sync;
110
+ if (options?.sync) {
111
+ const report = (stage, error) => {
112
+ if (options.onError) options.onError(error, { stage });
113
+ else console.error(`[crossws] sync ${stage} failed:`, error);
114
+ };
115
+ const driver = options.sync({ id: crypto.randomUUID() });
116
+ const deliver = (msg) => {
117
+ try {
118
+ localPublish(msg.topic, msg.data, { namespace: msg.namespace || void 0 });
119
+ } catch (error) {
120
+ report("delivery", error);
121
+ }
122
+ };
123
+ try {
124
+ Promise.resolve(driver.subscribe(deliver)).catch((error) => report("subscribe", error));
125
+ } catch (error) {
126
+ report("subscribe", error);
127
+ }
128
+ sync = {
129
+ subscribe: (deliver) => driver.subscribe(deliver),
130
+ publish: (msg) => {
131
+ try {
132
+ return Promise.resolve(driver.publish(msg)).catch((e) => report("publish", e));
133
+ } catch (error) {
134
+ report("publish", error);
135
+ }
136
+ },
137
+ close: driver.close ? () => driver.close() : void 0
138
+ };
139
+ }
63
140
  return {
64
141
  peers: globalPeers,
142
+ sync,
65
143
  publish(topic, message, options) {
66
- for (const peers of options?.namespace ? [globalPeers.get(options.namespace) || []] : globalPeers.values()) {
67
- let firstPeerWithTopic;
68
- for (const peer of peers) if (peer.topics.has(topic)) {
69
- firstPeerWithTopic = peer;
70
- break;
71
- }
72
- if (firstPeerWithTopic) {
73
- firstPeerWithTopic.send(message, options);
74
- firstPeerWithTopic.publish(topic, message, options);
75
- }
76
- }
144
+ localPublish(topic, message, options);
145
+ sync?.publish({
146
+ namespace: options?.namespace || "",
147
+ topic,
148
+ data: serializeMessage(message)
149
+ });
150
+ },
151
+ async close(code, reason) {
152
+ for (const peers of globalPeers.values()) for (const peer of peers) peer.close(code, reason);
153
+ await sync?.close?.();
77
154
  }
78
155
  };
79
156
  }
@@ -89,4 +166,4 @@ function getPeers(globalPeers, namespace) {
89
166
  function defineWebSocketAdapter(factory) {
90
167
  return factory;
91
168
  }
92
- export { AdapterHookable, adapterUtils, defineHooks, defineWebSocketAdapter, getPeers };
169
+ export { AdapterHookable, adapterUtils, defineHooks, defineWebSocketAdapter, getPeers, kNodeInspect, serializeMessage, toBufferLike, toString };
@@ -1,4 +1,4 @@
1
- import { Adapter, AdapterInstance, AdapterOptions, Peer, PeerContext } from "./adapter.mjs";
1
+ import { Adapter, AdapterInstance, AdapterOptions, Peer, PeerContext, SyncDriver } from "./adapter.mjs";
2
2
  import { Server, ServerWebSocket, WebSocketHandler } from "bun";
3
3
  interface BunAdapter extends AdapterInstance {
4
4
  websocket: WebSocketHandler<ContextData>;
@@ -18,13 +18,15 @@ declare class BunPeer extends Peer<{
18
18
  namespace: string;
19
19
  request: Request;
20
20
  peers: Set<BunPeer>;
21
+ sync?: SyncDriver;
21
22
  }> {
22
23
  get remoteAddress(): string;
23
24
  get context(): PeerContext;
25
+ get bufferedAmount(): number;
24
26
  send(data: unknown, options?: {
25
27
  compress?: boolean;
26
28
  }): number;
27
- publish(topic: string, data: unknown, options?: {
29
+ _publish(topic: string, data: unknown, options?: {
28
30
  compress?: boolean;
29
31
  }): number;
30
32
  subscribe(topic: string): void;
@@ -1,4 +1,26 @@
1
- import { __commonJSMin, __require, __toESM } from "../rolldown-runtime.mjs";
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
2
24
  var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
3
25
  const BINARY_TYPES = [
4
26
  "nodebuffer",