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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  - **Suspense.** `use(client.ready)` waits for the first connection, and reconnects never flash a fallback.
19
19
  - **Topic subscriptions.** Reference-counted `subscribe`/`unsubscribe` messages that are sent again after every reconnect, so feeds don't silently stop.
20
20
  - **Message state.** Reduce messages into a store, and let each component re-render only when its slice changes, at most once per frame if you like.
21
- - **Requests and latency.** Await replies with timeouts and cancellation, and measure round-trip time and the server's clock for ping displays and synchronized countdowns.
21
+ - **Requests, streams and latency.** Await a reply, or iterate over streamed ones such as an LLM's tokens, with timeouts and a cancel that tells the server to stop. Measure round-trip time and the server's clock for ping displays and synchronized countdowns.
22
22
  - **Games.** Snapshot interpolation and client-side prediction with server reconciliation, from `react-websocket/timeline`, so other players move smoothly and your own controls respond at once. A send rate that adapts to the connection keeps players on a poor link from building up lag.
23
23
  - **Server requests.** Answer requests from the server, such as an AI agent's, in code for browser-only tools, or from a dialog for human approval. Every request gets exactly one reply, and none is sent to the wrong connection.
24
24
  - **SSR-safe.** Never connects during a server render, hydrates cleanly, and ships a `'use client'` entry for Next.js and other Server Components frameworks.
@@ -117,10 +117,11 @@ function Room() {
117
117
  | `useWebSocketState(store, selector?)` | Read state reduced from messages; re-renders only when the selection changes |
118
118
  | `useWebSocketLatency(latency)` | Round-trip time and server clock offset |
119
119
  | `useAgentApproval(responder, method)` | Requests from the server waiting for a person to answer |
120
+ | `useWebSocketStream(rpc, options?)` | Stream the replies to a request into a value, e.g. tokens into an answer; returns `{ value, status, error, start, abort }` |
120
121
  | `createWebSocket(url, options?)` | A shareable client: `send`, `on`, `connect`, `close`, `reconnect`, `ready`, `getSnapshot`, `subscribe` |
121
122
  | `createWebSocketTopics(client, { subscribe, unsubscribe })` | Topic subscription manager for `useWebSocketTopic` |
122
123
  | `createWebSocketState(client, { initialState, reduce })` | Message-state store for `useWebSocketState` |
123
- | `createWebSocketRequests(client, { send, reply })` | `request(message)` returns a promise of the reply, with timeouts |
124
+ | `createWebSocketRequests(client, { send, reply })` | `request(message)` returns a promise of the reply, and `stream(message)` an async iterator of replies, with timeouts and `cancel` |
124
125
  | `createWebSocketLatency(client, { ping, pong })` | Ping tracker for `useWebSocketLatency`, plus `serverNow()` |
125
126
  | `createWebSocketSendRate(client, options?)` | `send(message, key)` coalesced per key at a rate that drops on a congested link |
126
127
  | `createWebSocketResponder(client, { request, respond, error })` | Answer the server's requests: `onRequest(method, handler)`, or a person through `useAgentApproval` |
package/dist/core.cjs CHANGED
@@ -660,6 +660,8 @@ function createWebSocketLatency(client, options) {
660
660
  //#endregion
661
661
  //#region src/requests.ts
662
662
  const lastIds = /* @__PURE__ */ new WeakMap();
663
+ const VOLATILE$2 = { volatile: true };
664
+ const noop = () => {};
663
665
  /**
664
666
  * Send requests and await their replies, matched by an id the library generates.
665
667
  *
@@ -680,8 +682,8 @@ const lastIds = /* @__PURE__ */ new WeakMap();
680
682
  */
681
683
  function createWebSocketRequests(client, options) {
682
684
  const waiting = /* @__PURE__ */ new Map();
683
- function rejectWhere(test, reason) {
684
- for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
685
+ function endWhere(test, reason, cancel) {
686
+ for (const entry of [...waiting.values()]) if (test(entry)) entry.end(true, reason(), cancel);
685
687
  }
686
688
  const offs = [
687
689
  client.on("message", (message) => {
@@ -689,78 +691,131 @@ function createWebSocketRequests(client, options) {
689
691
  const entry = id == null ? void 0 : waiting.get(String(id));
690
692
  if (!entry) return;
691
693
  const error = options.error?.(message);
692
- if (error === void 0) entry.resolve(message);
693
- else entry.reject(error);
694
+ if (error === void 0) entry.receive(message);
695
+ else entry.end(true, error);
694
696
  }),
695
697
  client.on("open", () => {
696
698
  for (const entry of waiting.values()) entry.socket ??= client.socket;
697
699
  }),
698
700
  client.subscribe(() => {
699
701
  const { status } = client.getSnapshot();
700
- if (status === "closed" || status === "idle") rejectWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
701
- else rejectWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
702
+ if (status === "closed" || status === "idle") endWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
703
+ else endWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
702
704
  })
703
705
  ];
706
+ /**
707
+ * Send `message` as a new request. `next` gets each reply that isn't an error and returns
708
+ * whether it was the last; `done` runs once, when the request ends. Returns a function that
709
+ * ends it from this side and tells the server.
710
+ */
711
+ function start(message, requestOptions, next, done) {
712
+ const { signal, priority } = requestOptions;
713
+ if (signal?.aborted) {
714
+ done(true, signal.reason);
715
+ return noop;
716
+ }
717
+ const id = (lastIds.get(client) ?? 0) + 1;
718
+ lastIds.set(client, id);
719
+ const key = String(id);
720
+ const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
721
+ const release = client.acquire();
722
+ let timer;
723
+ const onAbort = () => entry.end(true, signal.reason, true);
724
+ function arm() {
725
+ clearTimeout(timer);
726
+ if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.end(true, new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError"), true), timeout);
727
+ }
728
+ const entry = {
729
+ socket: null,
730
+ receive(reply) {
731
+ if (next(reply)) entry.end(false);
732
+ else arm();
733
+ },
734
+ end(failed, reason, cancel) {
735
+ if (!waiting.delete(key)) return;
736
+ clearTimeout(timer);
737
+ signal?.removeEventListener("abort", onAbort);
738
+ release();
739
+ done(failed, reason);
740
+ if (cancel && options.cancel) try {
741
+ client.send(options.cancel(id), entry.socket ? VOLATILE$2 : { priority });
742
+ } catch (error) {
743
+ queueMicrotask(() => {
744
+ throw error;
745
+ });
746
+ }
747
+ }
748
+ };
749
+ waiting.set(key, entry);
750
+ signal?.addEventListener("abort", onAbort, { once: true });
751
+ arm();
752
+ const { status } = client.getSnapshot();
753
+ if (status === "closed" || status === "idle") entry.end(true, /* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
754
+ else try {
755
+ if (client.send(options.send(message, id), {
756
+ ...requestOptions,
757
+ onDrop() {
758
+ entry.end(true, /* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
759
+ requestOptions.onDrop?.();
760
+ }
761
+ })) entry.socket = client.socket;
762
+ else if (client.socket?.readyState === 1) entry.end(true, /* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
763
+ else if (requestOptions.volatile) entry.end(true, /* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
764
+ } catch (error) {
765
+ entry.end(true, error);
766
+ }
767
+ return () => entry.end(false, void 0, true);
768
+ }
704
769
  return {
705
770
  client,
706
771
  request(message, requestOptions = {}) {
707
- const { signal } = requestOptions;
708
- if (signal?.aborted) return Promise.reject(signal.reason);
709
- const id = (lastIds.get(client) ?? 0) + 1;
710
- lastIds.set(client, id);
711
- const key = String(id);
712
- const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
713
772
  return new Promise((resolve, reject) => {
714
- let timer;
715
- const release = client.acquire();
716
- const onAbort = () => entry.reject(signal.reason);
717
- function settle() {
718
- waiting.delete(key);
719
- clearTimeout(timer);
720
- signal?.removeEventListener("abort", onAbort);
721
- release();
722
- }
723
- const entry = {
724
- socket: null,
725
- resolve(reply) {
726
- settle();
727
- resolve(reply);
728
- },
729
- reject(reason) {
730
- settle();
731
- reject(reason);
732
- }
733
- };
734
- waiting.set(key, entry);
735
- signal?.addEventListener("abort", onAbort, { once: true });
736
- if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.reject(new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")), timeout);
737
- const { status } = client.getSnapshot();
738
- if (status === "closed" || status === "idle") {
739
- entry.reject(/* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
740
- return;
741
- }
742
- let sent;
773
+ start(message, requestOptions, (reply) => {
774
+ resolve(reply);
775
+ return true;
776
+ }, (failed, reason) => {
777
+ if (failed) reject(reason);
778
+ });
779
+ });
780
+ },
781
+ stream(message, requestOptions = {}) {
782
+ const last = options.done;
783
+ if (!last) throw new TypeError("stream() needs the `done` option");
784
+ const replies = [];
785
+ let ended;
786
+ let wake = noop;
787
+ const stop = start(message, requestOptions, (reply) => {
788
+ replies.push(reply);
789
+ wake();
790
+ return last(reply);
791
+ }, (failed, reason) => {
792
+ if (requestOptions.signal?.aborted) replies.length = 0;
793
+ ended = [failed, reason];
794
+ wake();
795
+ });
796
+ async function* read() {
743
797
  try {
744
- sent = client.send(options.send(message, id), {
745
- ...requestOptions,
746
- onDrop() {
747
- entry.reject(/* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
748
- requestOptions.onDrop?.();
749
- }
750
- });
751
- } catch (error) {
752
- entry.reject(error);
753
- return;
798
+ for (;;) if (replies.length) yield replies.shift();
799
+ else if (ended) {
800
+ if (ended[0]) throw ended[1];
801
+ return;
802
+ } else await new Promise((resolve) => wake = resolve);
803
+ } finally {
804
+ stop();
754
805
  }
755
- if (sent) entry.socket = client.socket;
756
- else if (client.socket?.readyState === 1) entry.reject(/* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
757
- else if (requestOptions.volatile) entry.reject(/* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
758
- });
806
+ }
807
+ const iterator = read();
808
+ const close = iterator.return;
809
+ iterator.return = (value) => {
810
+ stop();
811
+ return close.call(iterator, value);
812
+ };
813
+ return iterator;
759
814
  },
760
815
  pending: () => waiting.size,
761
816
  dispose() {
762
817
  offs.forEach((off) => off());
763
- rejectWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"));
818
+ endWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"), true);
764
819
  }
765
820
  };
766
821
  }