react-websocket 3.5.0 → 4.0.0

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.
package/dist/core.d.cts CHANGED
@@ -29,7 +29,10 @@ interface Snapshot<In> {
29
29
  readonly status: Status;
30
30
  /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
31
31
  readonly lastMessage: In | undefined;
32
- /** Consecutive failed attempts since the last successful open. `0` while open. */
32
+ /**
33
+ * Consecutive failed attempts since the last stable connection. A connection that closes
34
+ * within `reconnect.minUptime` of opening counts as failed. `0` while open.
35
+ */
33
36
  readonly attempt: number;
34
37
  }
35
38
  /** Details of a closed connection, from the browser's `CloseEvent`. */
@@ -51,6 +54,14 @@ interface ReconnectOptions {
51
54
  * @default Infinity
52
55
  */
53
56
  maxAttempts?: number;
57
+ /**
58
+ * How long a connection must stay open, in ms, before the backoff resets. One that closes
59
+ * sooner counts as a failed attempt, so a server that accepts and then closes right away
60
+ * (e.g. rejecting the credentials sent on open) is retried with a growing delay, and
61
+ * `maxAttempts` still applies. `0` resets on every open.
62
+ * @default 5000
63
+ */
64
+ minUptime?: number;
54
65
  /**
55
66
  * Delay before the first retry, in ms.
56
67
  * @default 1000
@@ -68,19 +79,26 @@ interface ReconnectOptions {
68
79
  factor?: number;
69
80
  /**
70
81
  * Randomly shave up to this fraction (0–1) off each delay, so that many clients
71
- * dropped at once don't all reconnect at the same moment.
72
- * @default 0.5
82
+ * dropped at once don't all reconnect at the same moment. The default `1` picks each
83
+ * delay at random between 0 and the backoff ("full jitter").
84
+ * @default 1
73
85
  */
74
86
  jitter?: number;
75
87
  /**
76
88
  * Called on every unexpected close. Return `false` to stop reconnecting.
77
89
  *
90
+ * The default, `retryableClose`, retries everything except `1002` (protocol error), `1003`
91
+ * (unsupported data) and `1008` (policy violation). Your own function replaces it; call
92
+ * `retryableClose` from it to keep that rule, or return `true` to retry every close.
93
+ *
78
94
  * @example
79
95
  * ```ts
80
- * // Don't retry when the server rejects our credentials.
81
- * shouldReconnect: close => close.code !== 4001
96
+ * import { retryableClose } from 'react-websocket';
97
+ *
98
+ * // Also stop when our server rejects the credentials.
99
+ * shouldReconnect: close => close.code !== 4001 && retryableClose(close)
82
100
  * ```
83
- * @default () => true
101
+ * @default retryableClose
84
102
  */
85
103
  shouldReconnect?: (close: CloseInfo) => boolean;
86
104
  }
@@ -155,7 +173,8 @@ interface QueueOptions {
155
173
  /**
156
174
  * Maximum number of messages held while not connected. When full, the oldest
157
175
  * message is dropped, skipping `priority` messages unless nothing else is queued.
158
- * @default Infinity
176
+ * `Infinity` keeps every message.
177
+ * @default 100
159
178
  */
160
179
  limit?: number;
161
180
  }
@@ -231,8 +250,8 @@ interface WebSocketOptions<In = unknown, Out = unknown> {
231
250
  heartbeat?: HeartbeatOptions;
232
251
  /**
233
252
  * Buffer `send()` calls made while not connected and send them, in order, on open
234
- * (right after `open` listeners run).
235
- * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
253
+ * (right after `open` listeners run). It holds up to 100 messages, dropping the oldest;
254
+ * `{ limit }` changes that. `false` drops messages instead of queueing them.
236
255
  * The queue is cleared by `close()` or when the client gives up.
237
256
  * @default true
238
257
  */
@@ -374,6 +393,20 @@ interface WebSocketClient<In = unknown, Out = unknown> {
374
393
  }
375
394
  //#endregion
376
395
  //#region src/client.d.ts
396
+ /**
397
+ * The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
398
+ * `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
399
+ * retrying the same way can't fix. Call it from your own `shouldReconnect` to keep that rule.
400
+ *
401
+ * @example
402
+ * ```ts
403
+ * createWebSocket(url, {
404
+ * // Also stop on our server's "invalid token" code.
405
+ * reconnect: { shouldReconnect: close => close.code !== 4001 && retryableClose(close) }
406
+ * });
407
+ * ```
408
+ */
409
+ export declare function retryableClose(close: CloseInfo): boolean;
377
410
  /**
378
411
  * Create a WebSocket client, usually at module level, to share across components.
379
412
  *
@@ -487,20 +520,36 @@ interface WebSocketRequestsOptions<Req, In, Out> {
487
520
  reply: (message: In) => number | string | null | undefined;
488
521
  /**
489
522
  * Called on a reply. Return something other than `undefined` to reject the request with
490
- * it instead of resolving, e.g. for a server-side error reply.
523
+ * it instead of resolving, e.g. for a server-side error reply. A stream throws it.
491
524
  */
492
525
  error?: (message: In) => unknown;
493
526
  /**
494
- * Default time to wait for a reply, in ms. `Infinity` waits forever.
527
+ * Whether a reply is the last one of a stream. `stream()` needs it. That reply is yielded
528
+ * too, then the stream ends.
529
+ */
530
+ done?: (message: In) => boolean;
531
+ /**
532
+ * Build the message that tells the server to stop working on request `id`. Sent when this
533
+ * side gives up: the request's `signal` aborts, it times out, a loop over a stream exits
534
+ * early, or the manager is disposed. Not sent after the last reply, an error reply, a lost
535
+ * connection or `close()`.
536
+ */
537
+ cancel?: (id: number) => Out;
538
+ /**
539
+ * Default time to wait for a reply, in ms. For a stream, the time to wait for each reply,
540
+ * including the first. `Infinity` waits forever.
495
541
  * @default 10000
496
542
  */
497
543
  timeout?: number;
498
544
  }
499
- /** Per-call options for {@link WebSocketRequests.request}. */
545
+ /** Per-call options for {@link WebSocketRequests.request} and {@link WebSocketRequests.stream}. */
500
546
  interface RequestOptions extends SendOptions {
501
- /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
547
+ /**
548
+ * Time to wait for a reply (for a stream, for each reply), in ms. Overrides the manager's
549
+ * `timeout`.
550
+ */
502
551
  timeout?: number;
503
- /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
552
+ /** Abort the request. It rejects (a stream throws) with `signal.reason`. A late reply is ignored. */
504
553
  signal?: AbortSignal;
505
554
  }
506
555
  /** Request/response over one client. Create with {@link createWebSocketRequests}. */
@@ -517,7 +566,26 @@ interface WebSocketRequests<Req, In> {
517
566
  * queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
518
567
  */
519
568
  request(message: Req, options?: RequestOptions): Promise<In>;
520
- /** Number of requests waiting for a reply. */
569
+ /**
570
+ * Send a request and iterate over its replies, up to and including the one `done` matches.
571
+ * Holds the connection until the stream ends. Needs the `done` option.
572
+ *
573
+ * `timeout` is the time to wait for each reply. Replies that arrive before the loop reads
574
+ * them are buffered. The stream is one-shot: iterate it once.
575
+ *
576
+ * Fails where `request()` rejects, with the same errors, after yielding the replies already
577
+ * received. Aborting `signal` throws `signal.reason` right away and discards them. Leaving
578
+ * the loop early, aborting and timing out send `cancel`.
579
+ *
580
+ * @example
581
+ * ```ts
582
+ * for await (const reply of rpc.stream({ type: 'ask', prompt }, { signal })) {
583
+ * text += reply.delta;
584
+ * }
585
+ * ```
586
+ */
587
+ stream(message: Req, options?: RequestOptions): AsyncIterableIterator<In>;
588
+ /** Number of requests and streams waiting for a reply. */
521
589
  pending(): number;
522
590
  /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
523
591
  dispose(): void;
package/dist/core.d.ts CHANGED
@@ -29,7 +29,10 @@ interface Snapshot<In> {
29
29
  readonly status: Status;
30
30
  /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
31
31
  readonly lastMessage: In | undefined;
32
- /** Consecutive failed attempts since the last successful open. `0` while open. */
32
+ /**
33
+ * Consecutive failed attempts since the last stable connection. A connection that closes
34
+ * within `reconnect.minUptime` of opening counts as failed. `0` while open.
35
+ */
33
36
  readonly attempt: number;
34
37
  }
35
38
  /** Details of a closed connection, from the browser's `CloseEvent`. */
@@ -51,6 +54,14 @@ interface ReconnectOptions {
51
54
  * @default Infinity
52
55
  */
53
56
  maxAttempts?: number;
57
+ /**
58
+ * How long a connection must stay open, in ms, before the backoff resets. One that closes
59
+ * sooner counts as a failed attempt, so a server that accepts and then closes right away
60
+ * (e.g. rejecting the credentials sent on open) is retried with a growing delay, and
61
+ * `maxAttempts` still applies. `0` resets on every open.
62
+ * @default 5000
63
+ */
64
+ minUptime?: number;
54
65
  /**
55
66
  * Delay before the first retry, in ms.
56
67
  * @default 1000
@@ -68,19 +79,26 @@ interface ReconnectOptions {
68
79
  factor?: number;
69
80
  /**
70
81
  * Randomly shave up to this fraction (0–1) off each delay, so that many clients
71
- * dropped at once don't all reconnect at the same moment.
72
- * @default 0.5
82
+ * dropped at once don't all reconnect at the same moment. The default `1` picks each
83
+ * delay at random between 0 and the backoff ("full jitter").
84
+ * @default 1
73
85
  */
74
86
  jitter?: number;
75
87
  /**
76
88
  * Called on every unexpected close. Return `false` to stop reconnecting.
77
89
  *
90
+ * The default, `retryableClose`, retries everything except `1002` (protocol error), `1003`
91
+ * (unsupported data) and `1008` (policy violation). Your own function replaces it; call
92
+ * `retryableClose` from it to keep that rule, or return `true` to retry every close.
93
+ *
78
94
  * @example
79
95
  * ```ts
80
- * // Don't retry when the server rejects our credentials.
81
- * shouldReconnect: close => close.code !== 4001
96
+ * import { retryableClose } from 'react-websocket';
97
+ *
98
+ * // Also stop when our server rejects the credentials.
99
+ * shouldReconnect: close => close.code !== 4001 && retryableClose(close)
82
100
  * ```
83
- * @default () => true
101
+ * @default retryableClose
84
102
  */
85
103
  shouldReconnect?: (close: CloseInfo) => boolean;
86
104
  }
@@ -155,7 +173,8 @@ interface QueueOptions {
155
173
  /**
156
174
  * Maximum number of messages held while not connected. When full, the oldest
157
175
  * message is dropped, skipping `priority` messages unless nothing else is queued.
158
- * @default Infinity
176
+ * `Infinity` keeps every message.
177
+ * @default 100
159
178
  */
160
179
  limit?: number;
161
180
  }
@@ -231,8 +250,8 @@ interface WebSocketOptions<In = unknown, Out = unknown> {
231
250
  heartbeat?: HeartbeatOptions;
232
251
  /**
233
252
  * Buffer `send()` calls made while not connected and send them, in order, on open
234
- * (right after `open` listeners run).
235
- * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
253
+ * (right after `open` listeners run). It holds up to 100 messages, dropping the oldest;
254
+ * `{ limit }` changes that. `false` drops messages instead of queueing them.
236
255
  * The queue is cleared by `close()` or when the client gives up.
237
256
  * @default true
238
257
  */
@@ -374,6 +393,20 @@ interface WebSocketClient<In = unknown, Out = unknown> {
374
393
  }
375
394
  //#endregion
376
395
  //#region src/client.d.ts
396
+ /**
397
+ * The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
398
+ * `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
399
+ * retrying the same way can't fix. Call it from your own `shouldReconnect` to keep that rule.
400
+ *
401
+ * @example
402
+ * ```ts
403
+ * createWebSocket(url, {
404
+ * // Also stop on our server's "invalid token" code.
405
+ * reconnect: { shouldReconnect: close => close.code !== 4001 && retryableClose(close) }
406
+ * });
407
+ * ```
408
+ */
409
+ export declare function retryableClose(close: CloseInfo): boolean;
377
410
  /**
378
411
  * Create a WebSocket client, usually at module level, to share across components.
379
412
  *
@@ -487,20 +520,36 @@ interface WebSocketRequestsOptions<Req, In, Out> {
487
520
  reply: (message: In) => number | string | null | undefined;
488
521
  /**
489
522
  * Called on a reply. Return something other than `undefined` to reject the request with
490
- * it instead of resolving, e.g. for a server-side error reply.
523
+ * it instead of resolving, e.g. for a server-side error reply. A stream throws it.
491
524
  */
492
525
  error?: (message: In) => unknown;
493
526
  /**
494
- * Default time to wait for a reply, in ms. `Infinity` waits forever.
527
+ * Whether a reply is the last one of a stream. `stream()` needs it. That reply is yielded
528
+ * too, then the stream ends.
529
+ */
530
+ done?: (message: In) => boolean;
531
+ /**
532
+ * Build the message that tells the server to stop working on request `id`. Sent when this
533
+ * side gives up: the request's `signal` aborts, it times out, a loop over a stream exits
534
+ * early, or the manager is disposed. Not sent after the last reply, an error reply, a lost
535
+ * connection or `close()`.
536
+ */
537
+ cancel?: (id: number) => Out;
538
+ /**
539
+ * Default time to wait for a reply, in ms. For a stream, the time to wait for each reply,
540
+ * including the first. `Infinity` waits forever.
495
541
  * @default 10000
496
542
  */
497
543
  timeout?: number;
498
544
  }
499
- /** Per-call options for {@link WebSocketRequests.request}. */
545
+ /** Per-call options for {@link WebSocketRequests.request} and {@link WebSocketRequests.stream}. */
500
546
  interface RequestOptions extends SendOptions {
501
- /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
547
+ /**
548
+ * Time to wait for a reply (for a stream, for each reply), in ms. Overrides the manager's
549
+ * `timeout`.
550
+ */
502
551
  timeout?: number;
503
- /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
552
+ /** Abort the request. It rejects (a stream throws) with `signal.reason`. A late reply is ignored. */
504
553
  signal?: AbortSignal;
505
554
  }
506
555
  /** Request/response over one client. Create with {@link createWebSocketRequests}. */
@@ -517,7 +566,26 @@ interface WebSocketRequests<Req, In> {
517
566
  * queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
518
567
  */
519
568
  request(message: Req, options?: RequestOptions): Promise<In>;
520
- /** Number of requests waiting for a reply. */
569
+ /**
570
+ * Send a request and iterate over its replies, up to and including the one `done` matches.
571
+ * Holds the connection until the stream ends. Needs the `done` option.
572
+ *
573
+ * `timeout` is the time to wait for each reply. Replies that arrive before the loop reads
574
+ * them are buffered. The stream is one-shot: iterate it once.
575
+ *
576
+ * Fails where `request()` rejects, with the same errors, after yielding the replies already
577
+ * received. Aborting `signal` throws `signal.reason` right away and discards them. Leaving
578
+ * the loop early, aborting and timing out send `cancel`.
579
+ *
580
+ * @example
581
+ * ```ts
582
+ * for await (const reply of rpc.stream({ type: 'ask', prompt }, { signal })) {
583
+ * text += reply.delta;
584
+ * }
585
+ * ```
586
+ */
587
+ stream(message: Req, options?: RequestOptions): AsyncIterableIterator<In>;
588
+ /** Number of requests and streams waiting for a reply. */
521
589
  pending(): number;
522
590
  /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
523
591
  dispose(): void;
package/dist/core.js CHANGED
@@ -30,8 +30,26 @@ const RECONNECT_DEFAULTS = {
30
30
  minDelay: 1e3,
31
31
  maxDelay: 3e4,
32
32
  factor: 2,
33
- jitter: .5
33
+ jitter: 1,
34
+ minUptime: 5e3
34
35
  };
36
+ const QUEUE_LIMIT = 100;
37
+ /**
38
+ * The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
39
+ * `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
40
+ * retrying the same way can't fix. Call it from your own `shouldReconnect` to keep that rule.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * createWebSocket(url, {
45
+ * // Also stop on our server's "invalid token" code.
46
+ * reconnect: { shouldReconnect: close => close.code !== 4001 && retryableClose(close) }
47
+ * });
48
+ * ```
49
+ */
50
+ function retryableClose(close) {
51
+ return close.code !== 1002 && close.code !== 1003 && close.code !== 1008;
52
+ }
35
53
  const READY_GRACE = 5e3;
36
54
  function createDeferred() {
37
55
  let resolve;
@@ -83,7 +101,7 @@ function createClient(url, options = {}) {
83
101
  ...RECONNECT_DEFAULTS,
84
102
  ...typeof options.reconnect === "object" ? options.reconnect : {}
85
103
  };
86
- const queueLimit = options.queue === false ? 0 : typeof options.queue === "object" ? Math.max(0, options.queue.limit ?? Infinity) : Infinity;
104
+ const queueLimit = options.queue === false ? 0 : Math.max(0, (typeof options.queue === "object" ? options.queue.limit : void 0) ?? QUEUE_LIMIT);
87
105
  let snapshot = {
88
106
  status: "idle",
89
107
  lastMessage: void 0,
@@ -108,8 +126,10 @@ function createClient(url, options = {}) {
108
126
  let inbox = null;
109
127
  let generation = 0;
110
128
  let windowBound = false;
129
+ let failures = 0;
111
130
  let retryTimer;
112
131
  let teardownTimer;
132
+ let stableTimer;
113
133
  let heartbeatTimer;
114
134
  let pongTimer;
115
135
  function set(patch) {
@@ -176,10 +196,12 @@ function createClient(url, options = {}) {
176
196
  }, heartbeat.timeout);
177
197
  }, heartbeat.interval);
178
198
  }
179
- function stopHeartbeat() {
199
+ /** Stop the timers that belong to the current open socket. */
200
+ function stopSocketTimers() {
180
201
  clearInterval(heartbeatTimer);
181
202
  clearTimeout(pongTimer);
182
- heartbeatTimer = pongTimer = void 0;
203
+ clearTimeout(stableTimer);
204
+ heartbeatTimer = pongTimer = stableTimer = void 0;
183
205
  }
184
206
  /** Stop tracking `socket`. Its eventual close event is still reported unless `reportClose` is false. */
185
207
  function detach(socket, reportClose) {
@@ -250,6 +272,7 @@ function createClient(url, options = {}) {
250
272
  status: "open",
251
273
  attempt: 0
252
274
  });
275
+ if (retry) stableTimer = setTimeout(() => failures = 0, retry.minUptime);
253
276
  startHeartbeat(socket);
254
277
  ready?.resolve();
255
278
  if (refs === 0 && !pinned) {
@@ -274,14 +297,14 @@ function createClient(url, options = {}) {
274
297
  };
275
298
  }
276
299
  function handleClose(info) {
277
- stopHeartbeat();
300
+ stopSocketTimers();
278
301
  log("closed", info);
279
302
  retryOrFail(info);
280
303
  emit("close", info);
281
304
  }
282
305
  function retryOrFail(info, error) {
283
- if (wanted && retry && snapshot.attempt < retry.maxAttempts && (retry.shouldReconnect?.(info) ?? true)) {
284
- const attempt = snapshot.attempt + 1;
306
+ if (wanted && retry && failures < retry.maxAttempts && (retry.shouldReconnect ?? retryableClose)(info)) {
307
+ const attempt = ++failures;
285
308
  const delay = backoff(attempt);
286
309
  set({
287
310
  status: "reconnecting",
@@ -296,7 +319,10 @@ function createClient(url, options = {}) {
296
319
  opened = false;
297
320
  unbindWindow();
298
321
  pending = [];
299
- set({ status: "closed" });
322
+ set({
323
+ status: "closed",
324
+ attempt: failures
325
+ });
300
326
  rejectReady(reason);
301
327
  }
302
328
  function receive(event, socket) {
@@ -401,6 +427,7 @@ function createClient(url, options = {}) {
401
427
  opened = false;
402
428
  if (ready?.settled) ready = null;
403
429
  }
430
+ failures = 0;
404
431
  snapshot = {
405
432
  ...snapshot,
406
433
  attempt: 0
@@ -422,7 +449,8 @@ function createClient(url, options = {}) {
422
449
  generation++;
423
450
  clearTimeout(retryTimer);
424
451
  retryTimer = void 0;
425
- stopHeartbeat();
452
+ failures = 0;
453
+ stopSocketTimers();
426
454
  unbindWindow();
427
455
  pending = [];
428
456
  if (ws) {
@@ -445,7 +473,7 @@ function createClient(url, options = {}) {
445
473
  if (!getImpl()) return;
446
474
  clearTimeout(retryTimer);
447
475
  retryTimer = void 0;
448
- stopHeartbeat();
476
+ stopSocketTimers();
449
477
  if (ws) {
450
478
  const socket = ws;
451
479
  detach(socket, true);
@@ -659,6 +687,8 @@ function createWebSocketLatency(client, options) {
659
687
  //#endregion
660
688
  //#region src/requests.ts
661
689
  const lastIds = /* @__PURE__ */ new WeakMap();
690
+ const VOLATILE$2 = { volatile: true };
691
+ const noop = () => {};
662
692
  /**
663
693
  * Send requests and await their replies, matched by an id the library generates.
664
694
  *
@@ -679,8 +709,8 @@ const lastIds = /* @__PURE__ */ new WeakMap();
679
709
  */
680
710
  function createWebSocketRequests(client, options) {
681
711
  const waiting = /* @__PURE__ */ new Map();
682
- function rejectWhere(test, reason) {
683
- for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
712
+ function endWhere(test, reason, cancel) {
713
+ for (const entry of [...waiting.values()]) if (test(entry)) entry.end(true, reason(), cancel);
684
714
  }
685
715
  const offs = [
686
716
  client.on("message", (message) => {
@@ -688,78 +718,131 @@ function createWebSocketRequests(client, options) {
688
718
  const entry = id == null ? void 0 : waiting.get(String(id));
689
719
  if (!entry) return;
690
720
  const error = options.error?.(message);
691
- if (error === void 0) entry.resolve(message);
692
- else entry.reject(error);
721
+ if (error === void 0) entry.receive(message);
722
+ else entry.end(true, error);
693
723
  }),
694
724
  client.on("open", () => {
695
725
  for (const entry of waiting.values()) entry.socket ??= client.socket;
696
726
  }),
697
727
  client.subscribe(() => {
698
728
  const { status } = client.getSnapshot();
699
- if (status === "closed" || status === "idle") rejectWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
700
- else rejectWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
729
+ if (status === "closed" || status === "idle") endWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
730
+ else endWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
701
731
  })
702
732
  ];
733
+ /**
734
+ * Send `message` as a new request. `next` gets each reply that isn't an error and returns
735
+ * whether it was the last; `done` runs once, when the request ends. Returns a function that
736
+ * ends it from this side and tells the server.
737
+ */
738
+ function start(message, requestOptions, next, done) {
739
+ const { signal, priority } = requestOptions;
740
+ if (signal?.aborted) {
741
+ done(true, signal.reason);
742
+ return noop;
743
+ }
744
+ const id = (lastIds.get(client) ?? 0) + 1;
745
+ lastIds.set(client, id);
746
+ const key = String(id);
747
+ const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
748
+ const release = client.acquire();
749
+ let timer;
750
+ const onAbort = () => entry.end(true, signal.reason, true);
751
+ function arm() {
752
+ clearTimeout(timer);
753
+ if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.end(true, new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError"), true), timeout);
754
+ }
755
+ const entry = {
756
+ socket: null,
757
+ receive(reply) {
758
+ if (next(reply)) entry.end(false);
759
+ else arm();
760
+ },
761
+ end(failed, reason, cancel) {
762
+ if (!waiting.delete(key)) return;
763
+ clearTimeout(timer);
764
+ signal?.removeEventListener("abort", onAbort);
765
+ release();
766
+ done(failed, reason);
767
+ if (cancel && options.cancel) try {
768
+ client.send(options.cancel(id), entry.socket ? VOLATILE$2 : { priority });
769
+ } catch (error) {
770
+ queueMicrotask(() => {
771
+ throw error;
772
+ });
773
+ }
774
+ }
775
+ };
776
+ waiting.set(key, entry);
777
+ signal?.addEventListener("abort", onAbort, { once: true });
778
+ arm();
779
+ const { status } = client.getSnapshot();
780
+ if (status === "closed" || status === "idle") entry.end(true, /* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
781
+ else try {
782
+ if (client.send(options.send(message, id), {
783
+ ...requestOptions,
784
+ onDrop() {
785
+ entry.end(true, /* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
786
+ requestOptions.onDrop?.();
787
+ }
788
+ })) entry.socket = client.socket;
789
+ else if (client.socket?.readyState === 1) entry.end(true, /* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
790
+ else if (requestOptions.volatile) entry.end(true, /* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
791
+ } catch (error) {
792
+ entry.end(true, error);
793
+ }
794
+ return () => entry.end(false, void 0, true);
795
+ }
703
796
  return {
704
797
  client,
705
798
  request(message, requestOptions = {}) {
706
- const { signal } = requestOptions;
707
- if (signal?.aborted) return Promise.reject(signal.reason);
708
- const id = (lastIds.get(client) ?? 0) + 1;
709
- lastIds.set(client, id);
710
- const key = String(id);
711
- const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
712
799
  return new Promise((resolve, reject) => {
713
- let timer;
714
- const release = client.acquire();
715
- const onAbort = () => entry.reject(signal.reason);
716
- function settle() {
717
- waiting.delete(key);
718
- clearTimeout(timer);
719
- signal?.removeEventListener("abort", onAbort);
720
- release();
721
- }
722
- const entry = {
723
- socket: null,
724
- resolve(reply) {
725
- settle();
726
- resolve(reply);
727
- },
728
- reject(reason) {
729
- settle();
730
- reject(reason);
731
- }
732
- };
733
- waiting.set(key, entry);
734
- signal?.addEventListener("abort", onAbort, { once: true });
735
- if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.reject(new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")), timeout);
736
- const { status } = client.getSnapshot();
737
- if (status === "closed" || status === "idle") {
738
- entry.reject(/* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
739
- return;
740
- }
741
- let sent;
800
+ start(message, requestOptions, (reply) => {
801
+ resolve(reply);
802
+ return true;
803
+ }, (failed, reason) => {
804
+ if (failed) reject(reason);
805
+ });
806
+ });
807
+ },
808
+ stream(message, requestOptions = {}) {
809
+ const last = options.done;
810
+ if (!last) throw new TypeError("stream() needs the `done` option");
811
+ const replies = [];
812
+ let ended;
813
+ let wake = noop;
814
+ const stop = start(message, requestOptions, (reply) => {
815
+ replies.push(reply);
816
+ wake();
817
+ return last(reply);
818
+ }, (failed, reason) => {
819
+ if (requestOptions.signal?.aborted) replies.length = 0;
820
+ ended = [failed, reason];
821
+ wake();
822
+ });
823
+ async function* read() {
742
824
  try {
743
- sent = client.send(options.send(message, id), {
744
- ...requestOptions,
745
- onDrop() {
746
- entry.reject(/* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
747
- requestOptions.onDrop?.();
748
- }
749
- });
750
- } catch (error) {
751
- entry.reject(error);
752
- return;
825
+ for (;;) if (replies.length) yield replies.shift();
826
+ else if (ended) {
827
+ if (ended[0]) throw ended[1];
828
+ return;
829
+ } else await new Promise((resolve) => wake = resolve);
830
+ } finally {
831
+ stop();
753
832
  }
754
- if (sent) entry.socket = client.socket;
755
- else if (client.socket?.readyState === 1) entry.reject(/* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
756
- else if (requestOptions.volatile) entry.reject(/* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
757
- });
833
+ }
834
+ const iterator = read();
835
+ const close = iterator.return;
836
+ iterator.return = (value) => {
837
+ stop();
838
+ return close.call(iterator, value);
839
+ };
840
+ return iterator;
758
841
  },
759
842
  pending: () => waiting.size,
760
843
  dispose() {
761
844
  offs.forEach((off) => off());
762
- rejectWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"));
845
+ endWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"), true);
763
846
  }
764
847
  };
765
848
  }
@@ -1168,6 +1251,6 @@ function createWebSocketTopics(client, options) {
1168
1251
  };
1169
1252
  }
1170
1253
  //#endregion
1171
- export { MessageError, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketResponder, createWebSocketSendRate, createWebSocketState, createWebSocketTopics };
1254
+ export { MessageError, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketResponder, createWebSocketSendRate, createWebSocketState, createWebSocketTopics, retryableClose };
1172
1255
 
1173
1256
  //# sourceMappingURL=core.js.map