react-websocket 3.5.0 → 3.6.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.ts CHANGED
@@ -487,20 +487,36 @@ interface WebSocketRequestsOptions<Req, In, Out> {
487
487
  reply: (message: In) => number | string | null | undefined;
488
488
  /**
489
489
  * 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.
490
+ * it instead of resolving, e.g. for a server-side error reply. A stream throws it.
491
491
  */
492
492
  error?: (message: In) => unknown;
493
493
  /**
494
- * Default time to wait for a reply, in ms. `Infinity` waits forever.
494
+ * Whether a reply is the last one of a stream. `stream()` needs it. That reply is yielded
495
+ * too, then the stream ends.
496
+ */
497
+ done?: (message: In) => boolean;
498
+ /**
499
+ * Build the message that tells the server to stop working on request `id`. Sent when this
500
+ * side gives up: the request's `signal` aborts, it times out, a loop over a stream exits
501
+ * early, or the manager is disposed. Not sent after the last reply, an error reply, a lost
502
+ * connection or `close()`.
503
+ */
504
+ cancel?: (id: number) => Out;
505
+ /**
506
+ * Default time to wait for a reply, in ms. For a stream, the time to wait for each reply,
507
+ * including the first. `Infinity` waits forever.
495
508
  * @default 10000
496
509
  */
497
510
  timeout?: number;
498
511
  }
499
- /** Per-call options for {@link WebSocketRequests.request}. */
512
+ /** Per-call options for {@link WebSocketRequests.request} and {@link WebSocketRequests.stream}. */
500
513
  interface RequestOptions extends SendOptions {
501
- /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
514
+ /**
515
+ * Time to wait for a reply (for a stream, for each reply), in ms. Overrides the manager's
516
+ * `timeout`.
517
+ */
502
518
  timeout?: number;
503
- /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
519
+ /** Abort the request. It rejects (a stream throws) with `signal.reason`. A late reply is ignored. */
504
520
  signal?: AbortSignal;
505
521
  }
506
522
  /** Request/response over one client. Create with {@link createWebSocketRequests}. */
@@ -517,7 +533,26 @@ interface WebSocketRequests<Req, In> {
517
533
  * queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
518
534
  */
519
535
  request(message: Req, options?: RequestOptions): Promise<In>;
520
- /** Number of requests waiting for a reply. */
536
+ /**
537
+ * Send a request and iterate over its replies, up to and including the one `done` matches.
538
+ * Holds the connection until the stream ends. Needs the `done` option.
539
+ *
540
+ * `timeout` is the time to wait for each reply. Replies that arrive before the loop reads
541
+ * them are buffered. The stream is one-shot: iterate it once.
542
+ *
543
+ * Fails where `request()` rejects, with the same errors, after yielding the replies already
544
+ * received. Aborting `signal` throws `signal.reason` right away and discards them. Leaving
545
+ * the loop early, aborting and timing out send `cancel`.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * for await (const reply of rpc.stream({ type: 'ask', prompt }, { signal })) {
550
+ * text += reply.delta;
551
+ * }
552
+ * ```
553
+ */
554
+ stream(message: Req, options?: RequestOptions): AsyncIterableIterator<In>;
555
+ /** Number of requests and streams waiting for a reply. */
521
556
  pending(): number;
522
557
  /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
523
558
  dispose(): void;
package/dist/core.js CHANGED
@@ -659,6 +659,8 @@ function createWebSocketLatency(client, options) {
659
659
  //#endregion
660
660
  //#region src/requests.ts
661
661
  const lastIds = /* @__PURE__ */ new WeakMap();
662
+ const VOLATILE$2 = { volatile: true };
663
+ const noop = () => {};
662
664
  /**
663
665
  * Send requests and await their replies, matched by an id the library generates.
664
666
  *
@@ -679,8 +681,8 @@ const lastIds = /* @__PURE__ */ new WeakMap();
679
681
  */
680
682
  function createWebSocketRequests(client, options) {
681
683
  const waiting = /* @__PURE__ */ new Map();
682
- function rejectWhere(test, reason) {
683
- for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
684
+ function endWhere(test, reason, cancel) {
685
+ for (const entry of [...waiting.values()]) if (test(entry)) entry.end(true, reason(), cancel);
684
686
  }
685
687
  const offs = [
686
688
  client.on("message", (message) => {
@@ -688,78 +690,131 @@ function createWebSocketRequests(client, options) {
688
690
  const entry = id == null ? void 0 : waiting.get(String(id));
689
691
  if (!entry) return;
690
692
  const error = options.error?.(message);
691
- if (error === void 0) entry.resolve(message);
692
- else entry.reject(error);
693
+ if (error === void 0) entry.receive(message);
694
+ else entry.end(true, error);
693
695
  }),
694
696
  client.on("open", () => {
695
697
  for (const entry of waiting.values()) entry.socket ??= client.socket;
696
698
  }),
697
699
  client.subscribe(() => {
698
700
  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"));
701
+ if (status === "closed" || status === "idle") endWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
702
+ else endWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
701
703
  })
702
704
  ];
705
+ /**
706
+ * Send `message` as a new request. `next` gets each reply that isn't an error and returns
707
+ * whether it was the last; `done` runs once, when the request ends. Returns a function that
708
+ * ends it from this side and tells the server.
709
+ */
710
+ function start(message, requestOptions, next, done) {
711
+ const { signal, priority } = requestOptions;
712
+ if (signal?.aborted) {
713
+ done(true, signal.reason);
714
+ return noop;
715
+ }
716
+ const id = (lastIds.get(client) ?? 0) + 1;
717
+ lastIds.set(client, id);
718
+ const key = String(id);
719
+ const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
720
+ const release = client.acquire();
721
+ let timer;
722
+ const onAbort = () => entry.end(true, signal.reason, true);
723
+ function arm() {
724
+ clearTimeout(timer);
725
+ if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.end(true, new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError"), true), timeout);
726
+ }
727
+ const entry = {
728
+ socket: null,
729
+ receive(reply) {
730
+ if (next(reply)) entry.end(false);
731
+ else arm();
732
+ },
733
+ end(failed, reason, cancel) {
734
+ if (!waiting.delete(key)) return;
735
+ clearTimeout(timer);
736
+ signal?.removeEventListener("abort", onAbort);
737
+ release();
738
+ done(failed, reason);
739
+ if (cancel && options.cancel) try {
740
+ client.send(options.cancel(id), entry.socket ? VOLATILE$2 : { priority });
741
+ } catch (error) {
742
+ queueMicrotask(() => {
743
+ throw error;
744
+ });
745
+ }
746
+ }
747
+ };
748
+ waiting.set(key, entry);
749
+ signal?.addEventListener("abort", onAbort, { once: true });
750
+ arm();
751
+ const { status } = client.getSnapshot();
752
+ if (status === "closed" || status === "idle") entry.end(true, /* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
753
+ else try {
754
+ if (client.send(options.send(message, id), {
755
+ ...requestOptions,
756
+ onDrop() {
757
+ entry.end(true, /* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
758
+ requestOptions.onDrop?.();
759
+ }
760
+ })) entry.socket = client.socket;
761
+ else if (client.socket?.readyState === 1) entry.end(true, /* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
762
+ else if (requestOptions.volatile) entry.end(true, /* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
763
+ } catch (error) {
764
+ entry.end(true, error);
765
+ }
766
+ return () => entry.end(false, void 0, true);
767
+ }
703
768
  return {
704
769
  client,
705
770
  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
771
  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;
772
+ start(message, requestOptions, (reply) => {
773
+ resolve(reply);
774
+ return true;
775
+ }, (failed, reason) => {
776
+ if (failed) reject(reason);
777
+ });
778
+ });
779
+ },
780
+ stream(message, requestOptions = {}) {
781
+ const last = options.done;
782
+ if (!last) throw new TypeError("stream() needs the `done` option");
783
+ const replies = [];
784
+ let ended;
785
+ let wake = noop;
786
+ const stop = start(message, requestOptions, (reply) => {
787
+ replies.push(reply);
788
+ wake();
789
+ return last(reply);
790
+ }, (failed, reason) => {
791
+ if (requestOptions.signal?.aborted) replies.length = 0;
792
+ ended = [failed, reason];
793
+ wake();
794
+ });
795
+ async function* read() {
742
796
  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;
797
+ for (;;) if (replies.length) yield replies.shift();
798
+ else if (ended) {
799
+ if (ended[0]) throw ended[1];
800
+ return;
801
+ } else await new Promise((resolve) => wake = resolve);
802
+ } finally {
803
+ stop();
753
804
  }
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
- });
805
+ }
806
+ const iterator = read();
807
+ const close = iterator.return;
808
+ iterator.return = (value) => {
809
+ stop();
810
+ return close.call(iterator, value);
811
+ };
812
+ return iterator;
758
813
  },
759
814
  pending: () => waiting.size,
760
815
  dispose() {
761
816
  offs.forEach((off) => off());
762
- rejectWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"));
817
+ endWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"), true);
763
818
  }
764
819
  };
765
820
  }