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 +3 -2
- package/dist/core.cjs +111 -56
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +41 -6
- package/dist/core.d.ts +41 -6
- package/dist/core.js +111 -56
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -7
- package/dist/index.d.ts +106 -1
- package/dist/index.js +65 -2
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
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
|
|
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
|
|
684
|
-
for (const entry of [...waiting.values()]) if (test(entry)) entry.
|
|
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.
|
|
693
|
-
else entry.
|
|
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")
|
|
701
|
-
else
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
818
|
+
endWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"), true);
|
|
764
819
|
}
|
|
765
820
|
};
|
|
766
821
|
}
|