react-websocket 3.2.0 → 3.3.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/index.js CHANGED
@@ -1,142 +1,225 @@
1
1
  'use client';
2
-
3
- // src/index.ts
4
- export * from "./core.js";
5
-
6
- // src/react.ts
7
- import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from "react";
8
2
  import { createWebSocket } from "./core.js";
9
- var IDLE = Object.freeze({ status: "idle", lastMessage: void 0, attempt: 0 });
10
- var idleSnapshot = () => IDLE;
11
- var idleStatus = () => "idle";
12
- var NO_LATENCY = Object.freeze({ rtt: void 0, offset: void 0 });
13
- var noLatency = () => NO_LATENCY;
14
- var noopSubscribe = () => () => {
15
- };
16
- var noopSend = () => false;
17
- var noop = () => {
18
- };
19
- var ON_TEARDOWN = /* @__PURE__ */ Symbol.for("react-websocket.onTeardown");
20
- var registry = /* @__PURE__ */ new Map();
3
+ import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from "react";
4
+ export * from "./core.js";
5
+ //#region src/react.ts
6
+ const IDLE = Object.freeze({
7
+ status: "idle",
8
+ lastMessage: void 0,
9
+ attempt: 0
10
+ });
11
+ const idleSnapshot = () => IDLE;
12
+ const idleStatus = () => "idle";
13
+ const NO_LATENCY = Object.freeze({
14
+ rtt: void 0,
15
+ offset: void 0
16
+ });
17
+ const noLatency = () => NO_LATENCY;
18
+ const NO_REQUESTS = [];
19
+ const noRequests = () => NO_REQUESTS;
20
+ const noopSubscribe = () => () => {};
21
+ const noopSend = () => false;
22
+ const noop = () => {};
23
+ const ON_TEARDOWN = Symbol.for("react-websocket.onTeardown");
24
+ const registry = /* @__PURE__ */ new Map();
21
25
  function sharedClient(url, options) {
22
- if (typeof window === "undefined") return createWebSocket(url, options);
23
- const key = `${url}\0${JSON.stringify(options.protocols ?? null)}`;
24
- let client = registry.get(key);
25
- if (!client) {
26
- const created = createWebSocket(url, {
27
- ...options,
28
- [ON_TEARDOWN]: () => {
29
- if (registry.get(key) === created) registry.delete(key);
30
- }
31
- });
32
- registry.set(key, created);
33
- client = created;
34
- }
35
- return client;
26
+ if (typeof window === "undefined") return createWebSocket(url, options);
27
+ const key = `${url}\u0000${JSON.stringify(options.protocols ?? null)}`;
28
+ let client = registry.get(key);
29
+ if (!client) {
30
+ const created = createWebSocket(url, {
31
+ ...options,
32
+ [ON_TEARDOWN]: () => {
33
+ if (registry.get(key) === created) registry.delete(key);
34
+ }
35
+ });
36
+ registry.set(key, created);
37
+ client = created;
38
+ }
39
+ return client;
36
40
  }
41
+ /** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */
37
42
  function useLatest(value) {
38
- const ref = useRef(value);
39
- useInsertionEffect(() => {
40
- ref.current = value;
41
- });
42
- return ref;
43
+ const ref = useRef(value);
44
+ useInsertionEffect(() => {
45
+ ref.current = value;
46
+ });
47
+ return ref;
43
48
  }
44
49
  function useHold(client, enabled) {
45
- useEffect(() => {
46
- if (client && enabled) return client.acquire();
47
- }, [client, enabled]);
50
+ useEffect(() => {
51
+ if (client && enabled) return client.acquire();
52
+ }, [client, enabled]);
48
53
  }
49
54
  function useWebSocket(target, options = {}) {
50
- const client = target == null ? null : typeof target === "string" ? sharedClient(target, options) : target;
51
- const enabled = options.enabled ?? true;
52
- const snapshot = useSyncExternalStore(
53
- client ? client.subscribe : noopSubscribe,
54
- client ? client.getSnapshot : idleSnapshot,
55
- idleSnapshot
56
- );
57
- const handlers = useLatest(options);
58
- useEffect(() => {
59
- if (!client || !enabled) return;
60
- const offs = [
61
- client.on("open", (event) => handlers.current.onOpen?.(event)),
62
- client.on("message", (message, event) => handlers.current.onMessage?.(message, event)),
63
- client.on("close", (info) => handlers.current.onClose?.(info)),
64
- client.on("error", (error) => handlers.current.onError?.(error))
65
- ];
66
- return () => offs.forEach((off) => off());
67
- }, [client, enabled, handlers]);
68
- useHold(client, enabled);
69
- return {
70
- ...snapshot,
71
- send: client ? client.send : noopSend,
72
- close: client ? client.close : noop,
73
- reconnect: client ? client.reconnect : noop,
74
- client
75
- };
55
+ const client = target == null ? null : typeof target === "string" ? sharedClient(target, options) : target;
56
+ const enabled = options.enabled ?? true;
57
+ const snapshot = useSyncExternalStore(client ? client.subscribe : noopSubscribe, client ? client.getSnapshot : idleSnapshot, idleSnapshot);
58
+ const handlers = useLatest(options);
59
+ useEffect(() => {
60
+ if (!client || !enabled) return;
61
+ const offs = [
62
+ client.on("open", (event) => handlers.current.onOpen?.(event)),
63
+ client.on("message", (message, event) => handlers.current.onMessage?.(message, event)),
64
+ client.on("close", (info) => handlers.current.onClose?.(info)),
65
+ client.on("error", (error) => handlers.current.onError?.(error))
66
+ ];
67
+ return () => offs.forEach((off) => off());
68
+ }, [
69
+ client,
70
+ enabled,
71
+ handlers
72
+ ]);
73
+ useHold(client, enabled);
74
+ return {
75
+ ...snapshot,
76
+ send: client ? client.send : noopSend,
77
+ close: client ? client.close : noop,
78
+ reconnect: client ? client.reconnect : noop,
79
+ client
80
+ };
76
81
  }
82
+ /**
83
+ * Listen to a client's events without re-rendering the component. The listener always
84
+ * sees the latest props and state. Keeps the connection open while mounted.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));
89
+ * ```
90
+ */
77
91
  function useWebSocketEvent(client, type, listener) {
78
- const latest = useLatest(listener);
79
- useEffect(() => {
80
- if (!client) return;
81
- const off = client.on(type, ((...args) => latest.current(...args)));
82
- const release = client.acquire();
83
- return () => {
84
- off();
85
- release();
86
- };
87
- }, [client, type, latest]);
92
+ const latest = useLatest(listener);
93
+ useEffect(() => {
94
+ if (!client) return;
95
+ const off = client.on(type, ((...args) => latest.current(...args)));
96
+ const release = client.acquire();
97
+ return () => {
98
+ off();
99
+ release();
100
+ };
101
+ }, [
102
+ client,
103
+ type,
104
+ latest
105
+ ]);
88
106
  }
107
+ /**
108
+ * The connection status of a client. Re-renders only when the status changes, not on
109
+ * messages. Keeps the connection open while mounted.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'
114
+ * ```
115
+ */
89
116
  function useWebSocketStatus(client) {
90
- const status = useSyncExternalStore(
91
- client ? client.subscribe : noopSubscribe,
92
- client ? () => client.getSnapshot().status : idleStatus,
93
- idleStatus
94
- );
95
- useHold(client ?? null, true);
96
- return status;
117
+ const status = useSyncExternalStore(client ? client.subscribe : noopSubscribe, client ? () => client.getSnapshot().status : idleStatus, idleStatus);
118
+ useHold(client ?? null, true);
119
+ return status;
97
120
  }
98
- var identity = (value) => value;
121
+ const identity = (value) => value;
99
122
  function useWebSocketState(store, selector = identity, isEqual = Object.is) {
100
- const [getSelection, getServerSelection] = useMemo(() => {
101
- const memoized = (read) => {
102
- let memo;
103
- return () => {
104
- const state = read();
105
- if (memo && Object.is(memo.state, state)) return memo.selection;
106
- const selection2 = selector(state);
107
- if (memo && isEqual(memo.selection, selection2)) {
108
- memo = { state, selection: memo.selection };
109
- return memo.selection;
110
- }
111
- memo = { state, selection: selection2 };
112
- return selection2;
113
- };
114
- };
115
- return [memoized(store.getState), memoized(() => store.initialState)];
116
- }, [store, selector, isEqual]);
117
- const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);
118
- useHold(store.client, true);
119
- return selection;
123
+ const [getSelection, getServerSelection] = useMemo(() => {
124
+ const memoized = (read) => {
125
+ let memo;
126
+ return () => {
127
+ const state = read();
128
+ if (memo && Object.is(memo.state, state)) return memo.selection;
129
+ const selection = selector(state);
130
+ if (memo && isEqual(memo.selection, selection)) {
131
+ memo = {
132
+ state,
133
+ selection: memo.selection
134
+ };
135
+ return memo.selection;
136
+ }
137
+ memo = {
138
+ state,
139
+ selection
140
+ };
141
+ return selection;
142
+ };
143
+ };
144
+ return [memoized(store.getState), memoized(() => store.initialState)];
145
+ }, [
146
+ store,
147
+ selector,
148
+ isEqual
149
+ ]);
150
+ const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);
151
+ useHold(store.client, true);
152
+ return selection;
120
153
  }
154
+ /**
155
+ * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`
156
+ * manager. Components holding the same topic share one subscription, and it is sent again
157
+ * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.
158
+ *
159
+ * @example
160
+ * ```tsx
161
+ * useWebSocketTopic(topics, `prices:${symbol}`);
162
+ * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));
163
+ * ```
164
+ */
121
165
  function useWebSocketTopic(topics, topic) {
122
- const key = topic == null ? null : JSON.stringify(typeof topic === "string" ? [topic] : topic);
123
- useEffect(() => {
124
- if (!topics || key === null) return;
125
- const releases = JSON.parse(key).map((t) => topics.acquire(t));
126
- return () => releases.forEach((release) => release());
127
- }, [topics, key]);
166
+ const key = topic == null ? null : JSON.stringify(typeof topic === "string" ? [topic] : topic);
167
+ useEffect(() => {
168
+ if (!topics || key === null) return;
169
+ const releases = JSON.parse(key).map((t) => topics.acquire(t));
170
+ return () => releases.forEach((release) => release());
171
+ }, [topics, key]);
128
172
  }
173
+ /**
174
+ * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.
175
+ * Re-renders when a measurement changes, about once per ping interval. Keeps the connection
176
+ * open while mounted. On the server, and during hydration, both values are `undefined`.
177
+ *
178
+ * @example
179
+ * ```tsx
180
+ * const { rtt } = useWebSocketLatency(latency);
181
+ * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;
182
+ * ```
183
+ */
129
184
  function useWebSocketLatency(latency) {
130
- const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);
131
- useHold(latency.client, true);
132
- return snapshot;
185
+ const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);
186
+ useHold(latency.client, true);
187
+ return snapshot;
133
188
  }
134
- export {
135
- useWebSocket,
136
- useWebSocketEvent,
137
- useWebSocketLatency,
138
- useWebSocketState,
139
- useWebSocketStatus,
140
- useWebSocketTopic
141
- };
189
+ /**
190
+ * Requests for `method` that a person answers, e.g. an agent asking to run a tool. While mounted,
191
+ * it registers `method` on the responder without a handler, so new requests wait here instead of
192
+ * getting a `NotFoundError` reply. Call `respond` or `reject` on one to answer it. Re-renders
193
+ * when the list changes. Keeps the connection open while mounted. `null` holds nothing.
194
+ *
195
+ * Requests stay listed after unmount until they are answered, time out, are cancelled or lose
196
+ * their connection, so a remounted dialog shows them again. On the server the list is empty.
197
+ *
198
+ * @example
199
+ * ```tsx
200
+ * const [request] = useAgentApproval(tools, 'approve');
201
+ * if (!request) return null;
202
+ * return (
203
+ * <dialog open>
204
+ * {request.params.summary}
205
+ * <button onClick={() => request.respond({ approved: true })}>Approve</button>
206
+ * <button onClick={() => request.respond({ approved: false })}>Deny</button>
207
+ * </dialog>
208
+ * );
209
+ * ```
210
+ */
211
+ function useAgentApproval(responder, method) {
212
+ const waiting = useSyncExternalStore(responder ? responder.subscribe : noopSubscribe, responder ? responder.getSnapshot : noRequests, noRequests);
213
+ useEffect(() => {
214
+ if (responder && method != null) return responder.onRequest(method);
215
+ }, [responder, method]);
216
+ useHold(responder?.client ?? null, method != null);
217
+ return useMemo(() => {
218
+ const mine = waiting.filter((request) => request.method === method);
219
+ return mine.length ? mine : NO_REQUESTS;
220
+ }, [waiting, method]);
221
+ }
222
+ //#endregion
223
+ export { useAgentApproval, useWebSocket, useWebSocketEvent, useWebSocketLatency, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
224
+
142
225
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/react.ts"],"sourcesContent":["export * from './core';\nexport {\n useWebSocket,\n useWebSocketEvent,\n useWebSocketLatency,\n useWebSocketState,\n useWebSocketStatus,\n useWebSocketTopic\n} from './react';\nexport type { UseWebSocketOptions, UseWebSocketResult, WebSocketHandlers } from './react';\n","import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n// Import the client through the `core` entry: the build keeps it external, so both entries\n// share one copy of it (and of `MessageError`).\nimport { createWebSocket } from './core';\nimport type { Latency, WebSocketLatency } from './latency';\nimport type { WebSocketState } from './state';\nimport type { WebSocketTopics } from './topics';\nimport type {\n CloseInfo,\n EventMap,\n SendOptions,\n Snapshot,\n Status,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n\n/**\n * Per-component event handlers. They always see the component's latest props and\n * state, and changing them never reconnects.\n */\nexport interface WebSocketHandlers<In> {\n /** The socket opened (initially or after a reconnect). */\n onOpen?: (event: Event) => void;\n /** A message arrived and passed `parse`/`json` and `schema`. */\n onMessage?: (message: In, event: MessageEvent) => void;\n /** A socket closed. Called whether or not it will be retried. */\n onClose?: (close: CloseInfo) => void;\n /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */\n onError?: (error: Event | Error) => void;\n /**\n * `false` releases this component's hold on the connection and stops its handlers.\n * The connection closes once nothing else holds it.\n * @default true\n */\n enabled?: boolean;\n}\n\n/** Options for `useWebSocket(url, options)`: connection options plus handlers. */\nexport interface UseWebSocketOptions<In, Out>\n extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}\n\n/** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */\nexport interface UseWebSocketResult<In, Out> extends Snapshot<In> {\n /**\n * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).\n * Returns `true` if sent immediately. Stable identity.\n */\n send: (message: Out, options?: SendOptions) => boolean;\n /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */\n close: (code?: number, reason?: string) => void;\n /** Drop the socket and connect again right away. Stable identity. */\n reconnect: () => void;\n /** The underlying client, or `null` when the URL is `null`/`undefined`. */\n client: WebSocketClient<In, Out> | null;\n}\n\nconst IDLE: Snapshot<never> = Object.freeze({ status: 'idle', lastMessage: undefined, attempt: 0 });\nconst idleSnapshot = () => IDLE;\nconst idleStatus = (): Status => 'idle';\nconst NO_LATENCY: Latency = Object.freeze({ rtt: undefined, offset: undefined });\nconst noLatency = () => NO_LATENCY;\nconst noopSubscribe = () => () => {};\nconst noopSend = () => false;\nconst noop = () => {};\n\n// Same key as `ON_TEARDOWN` in ./client, which this entry must not import directly.\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\n// Hooks given the same URL (and protocols) share one connection.\nconst registry = new Map<string, WebSocketClient<any, any>>();\n\nfunction sharedClient<In, Out>(url: string, options: WebSocketOptions<In, Out>) {\n // Don't let server renders accumulate clients that can never be torn down.\n if (typeof window === 'undefined') return createWebSocket(url, options);\n const key = `${url}\\u0000${JSON.stringify(options.protocols ?? null)}`;\n let client = registry.get(key) as WebSocketClient<In, Out> | undefined;\n if (!client) {\n const created: WebSocketClient<In, Out> = createWebSocket(url, {\n ...options,\n [ON_TEARDOWN]: () => {\n if (registry.get(key) === created) registry.delete(key);\n }\n } as WebSocketOptions<In, Out>);\n registry.set(key, created);\n client = created;\n }\n return client;\n}\n\n/** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */\nfunction useLatest<T>(value: T) {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\nfunction useHold(client: WebSocketClient<any, any> | null, enabled: boolean) {\n useEffect(() => {\n if (client && enabled) return client.acquire();\n }, [client, enabled]);\n}\n\n/**\n * Connect to a WebSocket and re-render with its status and latest message.\n *\n * - With a URL, every component using the same URL (and `protocols`) shares one\n * connection. Its options come from whichever component created it first.\n * - With a client from `createWebSocket()`, options are the client's; only handlers\n * and `enabled` apply here.\n * - With `null` or `undefined`, nothing connects and the status stays `idle`.\n *\n * The connection opens on mount and closes one tick after the last component using it\n * unmounts, so StrictMode and `<Activity>` don't open duplicate sockets.\n *\n * Re-renders on status changes and on every message. For high-frequency streams use\n * `useWebSocketEvent` and `useWebSocketStatus` instead.\n *\n * @example\n * ```tsx\n * const { status, lastMessage, send } = useWebSocket<Price, Order>('wss://api.example.com/prices', {\n * json: true,\n * onMessage: price => console.log(price)\n * });\n * ```\n */\nexport function useWebSocket<In = unknown, Out = unknown>(\n url: string | null | undefined,\n options?: UseWebSocketOptions<In, Out>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n client: WebSocketClient<In, Out> | null | undefined,\n options?: WebSocketHandlers<In>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n target: string | WebSocketClient<In, Out> | null | undefined,\n options: UseWebSocketOptions<In, Out> = {}\n): UseWebSocketResult<In, Out> {\n const client =\n target == null ? null : typeof target === 'string' ? sharedClient(target, options) : target;\n const enabled = options.enabled ?? true;\n\n const snapshot = useSyncExternalStore<Snapshot<In>>(\n client ? client.subscribe : noopSubscribe,\n client ? client.getSnapshot : idleSnapshot,\n idleSnapshot\n );\n\n const handlers = useLatest(options);\n useEffect(() => {\n if (!client || !enabled) return;\n const offs = [\n client.on('open', event => handlers.current.onOpen?.(event)),\n client.on('message', (message, event) => handlers.current.onMessage?.(message, event)),\n client.on('close', info => handlers.current.onClose?.(info)),\n client.on('error', error => handlers.current.onError?.(error))\n ];\n return () => offs.forEach(off => off());\n }, [client, enabled, handlers]);\n useHold(client, enabled);\n\n return {\n ...snapshot,\n send: client ? client.send : noopSend,\n close: client ? client.close : noop,\n reconnect: client ? client.reconnect : noop,\n client\n };\n}\n\n/**\n * Listen to a client's events without re-rendering the component. The listener always\n * sees the latest props and state. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));\n * ```\n */\nexport function useWebSocketEvent<In, K extends keyof EventMap<In>>(\n client: WebSocketClient<In, any> | null | undefined,\n type: K,\n listener: EventMap<In>[K]\n): void {\n const latest = useLatest(listener);\n useEffect(() => {\n if (!client) return;\n const off = client.on(type, ((...args: unknown[]) =>\n (latest.current as (...a: unknown[]) => void)(...args)) as EventMap<In>[K]);\n const release = client.acquire();\n return () => {\n off();\n release();\n };\n }, [client, type, latest]);\n}\n\n/**\n * The connection status of a client. Re-renders only when the status changes, not on\n * messages. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'\n * ```\n */\nexport function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status {\n const status = useSyncExternalStore(\n client ? client.subscribe : noopSubscribe,\n client ? () => client.getSnapshot().status : idleStatus,\n idleStatus\n );\n useHold(client ?? null, true);\n return status;\n}\n\nconst identity = <T>(value: T) => value;\n\n/**\n * Read state built from messages by `createWebSocketState`. Re-renders only when the\n * selected value changes (by `isEqual`, default `Object.is`), however often messages\n * arrive. Keeps the connection open while mounted.\n *\n * On the server, and during hydration, it reads from `initialState`.\n *\n * @example\n * ```tsx\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function useWebSocketState<S>(store: WebSocketState<S>): S;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T,\n isEqual?: (a: T, b: T) => boolean\n): T;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T = identity as (state: S) => T,\n isEqual: (a: T, b: T) => boolean = Object.is\n): T {\n const [getSelection, getServerSelection] = useMemo(() => {\n // Return the previous selection while the state is unchanged or the result is equal,\n // so inline selectors that build objects don't re-render (or loop) on every message.\n const memoized = (read: () => S) => {\n let memo: { state: S; selection: T } | undefined;\n return () => {\n const state = read();\n if (memo && Object.is(memo.state, state)) return memo.selection;\n const selection = selector(state);\n if (memo && isEqual(memo.selection, selection)) {\n memo = { state, selection: memo.selection };\n return memo.selection;\n }\n memo = { state, selection };\n return selection;\n };\n };\n return [memoized(store.getState), memoized(() => store.initialState)] as const;\n }, [store, selector, isEqual]);\n\n const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);\n useHold(store.client, true);\n return selection;\n}\n\n/**\n * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`\n * manager. Components holding the same topic share one subscription, and it is sent again\n * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.\n *\n * @example\n * ```tsx\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));\n * ```\n */\nexport function useWebSocketTopic(\n topics: WebSocketTopics | null | undefined,\n topic: string | readonly string[] | null | undefined\n): void {\n // A stable key, so a new array with the same topics doesn't resubscribe.\n const key = topic == null ? null : JSON.stringify(typeof topic === 'string' ? [topic] : topic);\n useEffect(() => {\n if (!topics || key === null) return;\n const releases = (JSON.parse(key) as string[]).map(t => topics.acquire(t));\n return () => releases.forEach(release => release());\n }, [topics, key]);\n}\n\n/**\n * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.\n * Re-renders when a measurement changes, about once per ping interval. Keeps the connection\n * open while mounted. On the server, and during hydration, both values are `undefined`.\n *\n * @example\n * ```tsx\n * const { rtt } = useWebSocketLatency(latency);\n * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;\n * ```\n */\nexport function useWebSocketLatency(latency: WebSocketLatency): Latency {\n const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);\n useHold(latency.client, true);\n return snapshot;\n}\n"],"mappings":"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","names":["selection"]}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n// Import the client through the `core` entry: the build keeps it external, so both entries\n// share one copy of it (and of `MessageError`).\nimport { createWebSocket } from './core';\nimport type { Latency, WebSocketLatency } from './latency';\n// Types only: a value import from anything but `./core` would bundle a second copy into this entry.\nimport type { IncomingRequest, WebSocketResponder } from './responder';\nimport type { WebSocketState } from './state';\nimport type { WebSocketTopics } from './topics';\nimport type {\n CloseInfo,\n EventMap,\n SendOptions,\n Snapshot,\n Status,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n\n/**\n * Per-component event handlers. They always see the component's latest props and\n * state, and changing them never reconnects.\n */\nexport interface WebSocketHandlers<In> {\n /** The socket opened (initially or after a reconnect). */\n onOpen?: (event: Event) => void;\n /** A message arrived and passed `parse`/`json` and `schema`. */\n onMessage?: (message: In, event: MessageEvent) => void;\n /** A socket closed. Called whether or not it will be retried. */\n onClose?: (close: CloseInfo) => void;\n /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */\n onError?: (error: Event | Error) => void;\n /**\n * `false` releases this component's hold on the connection and stops its handlers.\n * The connection closes once nothing else holds it.\n * @default true\n */\n enabled?: boolean;\n}\n\n/** Options for `useWebSocket(url, options)`: connection options plus handlers. */\nexport interface UseWebSocketOptions<In, Out>\n extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}\n\n/** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */\nexport interface UseWebSocketResult<In, Out> extends Snapshot<In> {\n /**\n * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).\n * Returns `true` if sent immediately. Stable identity.\n */\n send: (message: Out, options?: SendOptions) => boolean;\n /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */\n close: (code?: number, reason?: string) => void;\n /** Drop the socket and connect again right away. Stable identity. */\n reconnect: () => void;\n /** The underlying client, or `null` when the URL is `null`/`undefined`. */\n client: WebSocketClient<In, Out> | null;\n}\n\nconst IDLE: Snapshot<never> = Object.freeze({ status: 'idle', lastMessage: undefined, attempt: 0 });\nconst idleSnapshot = () => IDLE;\nconst idleStatus = (): Status => 'idle';\nconst NO_LATENCY: Latency = Object.freeze({ rtt: undefined, offset: undefined });\nconst noLatency = () => NO_LATENCY;\n// Not frozen: a call would keep it in bundles that never use the hook.\nconst NO_REQUESTS: readonly never[] = [];\nconst noRequests = () => NO_REQUESTS;\nconst noopSubscribe = () => () => {};\nconst noopSend = () => false;\nconst noop = () => {};\n\n// Same key as `ON_TEARDOWN` in ./client, which this entry must not import directly.\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\n// Hooks given the same URL (and protocols) share one connection.\nconst registry = new Map<string, WebSocketClient<any, any>>();\n\nfunction sharedClient<In, Out>(url: string, options: WebSocketOptions<In, Out>) {\n // Don't let server renders accumulate clients that can never be torn down.\n if (typeof window === 'undefined') return createWebSocket(url, options);\n const key = `${url}\\u0000${JSON.stringify(options.protocols ?? null)}`;\n let client = registry.get(key) as WebSocketClient<In, Out> | undefined;\n if (!client) {\n const created: WebSocketClient<In, Out> = createWebSocket(url, {\n ...options,\n [ON_TEARDOWN]: () => {\n if (registry.get(key) === created) registry.delete(key);\n }\n } as WebSocketOptions<In, Out>);\n registry.set(key, created);\n client = created;\n }\n return client;\n}\n\n/** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */\nfunction useLatest<T>(value: T) {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\nfunction useHold(client: WebSocketClient<any, any> | null, enabled: boolean) {\n useEffect(() => {\n if (client && enabled) return client.acquire();\n }, [client, enabled]);\n}\n\n/**\n * Connect to a WebSocket and re-render with its status and latest message.\n *\n * - With a URL, every component using the same URL (and `protocols`) shares one\n * connection. Its options come from whichever component created it first.\n * - With a client from `createWebSocket()`, options are the client's; only handlers\n * and `enabled` apply here.\n * - With `null` or `undefined`, nothing connects and the status stays `idle`.\n *\n * The connection opens on mount and closes one tick after the last component using it\n * unmounts, so StrictMode and `<Activity>` don't open duplicate sockets.\n *\n * Re-renders on status changes and on every message. For high-frequency streams use\n * `useWebSocketEvent` and `useWebSocketStatus` instead.\n *\n * @example\n * ```tsx\n * const { status, lastMessage, send } = useWebSocket<Price, Order>('wss://api.example.com/prices', {\n * json: true,\n * onMessage: price => console.log(price)\n * });\n * ```\n */\nexport function useWebSocket<In = unknown, Out = unknown>(\n url: string | null | undefined,\n options?: UseWebSocketOptions<In, Out>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n client: WebSocketClient<In, Out> | null | undefined,\n options?: WebSocketHandlers<In>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n target: string | WebSocketClient<In, Out> | null | undefined,\n options: UseWebSocketOptions<In, Out> = {}\n): UseWebSocketResult<In, Out> {\n const client =\n target == null ? null : typeof target === 'string' ? sharedClient(target, options) : target;\n const enabled = options.enabled ?? true;\n\n const snapshot = useSyncExternalStore<Snapshot<In>>(\n client ? client.subscribe : noopSubscribe,\n client ? client.getSnapshot : idleSnapshot,\n idleSnapshot\n );\n\n const handlers = useLatest(options);\n useEffect(() => {\n if (!client || !enabled) return;\n const offs = [\n client.on('open', event => handlers.current.onOpen?.(event)),\n client.on('message', (message, event) => handlers.current.onMessage?.(message, event)),\n client.on('close', info => handlers.current.onClose?.(info)),\n client.on('error', error => handlers.current.onError?.(error))\n ];\n return () => offs.forEach(off => off());\n }, [client, enabled, handlers]);\n useHold(client, enabled);\n\n return {\n ...snapshot,\n send: client ? client.send : noopSend,\n close: client ? client.close : noop,\n reconnect: client ? client.reconnect : noop,\n client\n };\n}\n\n/**\n * Listen to a client's events without re-rendering the component. The listener always\n * sees the latest props and state. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));\n * ```\n */\nexport function useWebSocketEvent<In, K extends keyof EventMap<In>>(\n client: WebSocketClient<In, any> | null | undefined,\n type: K,\n listener: EventMap<In>[K]\n): void {\n const latest = useLatest(listener);\n useEffect(() => {\n if (!client) return;\n const off = client.on(type, ((...args: unknown[]) =>\n (latest.current as (...a: unknown[]) => void)(...args)) as EventMap<In>[K]);\n const release = client.acquire();\n return () => {\n off();\n release();\n };\n }, [client, type, latest]);\n}\n\n/**\n * The connection status of a client. Re-renders only when the status changes, not on\n * messages. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'\n * ```\n */\nexport function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status {\n const status = useSyncExternalStore(\n client ? client.subscribe : noopSubscribe,\n client ? () => client.getSnapshot().status : idleStatus,\n idleStatus\n );\n useHold(client ?? null, true);\n return status;\n}\n\nconst identity = <T>(value: T) => value;\n\n/**\n * Read state built from messages by `createWebSocketState`. Re-renders only when the\n * selected value changes (by `isEqual`, default `Object.is`), however often messages\n * arrive. Keeps the connection open while mounted.\n *\n * On the server, and during hydration, it reads from `initialState`.\n *\n * @example\n * ```tsx\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function useWebSocketState<S>(store: WebSocketState<S>): S;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T,\n isEqual?: (a: T, b: T) => boolean\n): T;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T = identity as (state: S) => T,\n isEqual: (a: T, b: T) => boolean = Object.is\n): T {\n const [getSelection, getServerSelection] = useMemo(() => {\n // Return the previous selection while the state is unchanged or the result is equal,\n // so inline selectors that build objects don't re-render (or loop) on every message.\n const memoized = (read: () => S) => {\n let memo: { state: S; selection: T } | undefined;\n return () => {\n const state = read();\n if (memo && Object.is(memo.state, state)) return memo.selection;\n const selection = selector(state);\n if (memo && isEqual(memo.selection, selection)) {\n memo = { state, selection: memo.selection };\n return memo.selection;\n }\n memo = { state, selection };\n return selection;\n };\n };\n return [memoized(store.getState), memoized(() => store.initialState)] as const;\n }, [store, selector, isEqual]);\n\n const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);\n useHold(store.client, true);\n return selection;\n}\n\n/**\n * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`\n * manager. Components holding the same topic share one subscription, and it is sent again\n * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.\n *\n * @example\n * ```tsx\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));\n * ```\n */\nexport function useWebSocketTopic(\n topics: WebSocketTopics | null | undefined,\n topic: string | readonly string[] | null | undefined\n): void {\n // A stable key, so a new array with the same topics doesn't resubscribe.\n const key = topic == null ? null : JSON.stringify(typeof topic === 'string' ? [topic] : topic);\n useEffect(() => {\n if (!topics || key === null) return;\n const releases = (JSON.parse(key) as string[]).map(t => topics.acquire(t));\n return () => releases.forEach(release => release());\n }, [topics, key]);\n}\n\n/**\n * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.\n * Re-renders when a measurement changes, about once per ping interval. Keeps the connection\n * open while mounted. On the server, and during hydration, both values are `undefined`.\n *\n * @example\n * ```tsx\n * const { rtt } = useWebSocketLatency(latency);\n * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;\n * ```\n */\nexport function useWebSocketLatency(latency: WebSocketLatency): Latency {\n const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);\n useHold(latency.client, true);\n return snapshot;\n}\n\n/**\n * Requests for `method` that a person answers, e.g. an agent asking to run a tool. While mounted,\n * it registers `method` on the responder without a handler, so new requests wait here instead of\n * getting a `NotFoundError` reply. Call `respond` or `reject` on one to answer it. Re-renders\n * when the list changes. Keeps the connection open while mounted. `null` holds nothing.\n *\n * Requests stay listed after unmount until they are answered, time out, are cancelled or lose\n * their connection, so a remounted dialog shows them again. On the server the list is empty.\n *\n * @example\n * ```tsx\n * const [request] = useAgentApproval(tools, 'approve');\n * if (!request) return null;\n * return (\n * <dialog open>\n * {request.params.summary}\n * <button onClick={() => request.respond({ approved: true })}>Approve</button>\n * <button onClick={() => request.respond({ approved: false })}>Deny</button>\n * </dialog>\n * );\n * ```\n */\nexport function useAgentApproval<P, R>(\n responder: WebSocketResponder<P, R> | null | undefined,\n method: string | null | undefined\n): readonly IncomingRequest<P, R>[] {\n const waiting = useSyncExternalStore(\n responder ? responder.subscribe : noopSubscribe,\n responder ? responder.getSnapshot : noRequests,\n noRequests\n );\n useEffect(() => {\n if (responder && method != null) return responder.onRequest(method);\n }, [responder, method]);\n useHold(responder?.client ?? null, method != null);\n return useMemo(() => {\n const mine = waiting.filter(request => request.method === method);\n return mine.length ? mine : NO_REQUESTS;\n }, [waiting, method]);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-websocket",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Hooks-first WebSocket client for React: shared connections, auto-reconnect, typed messages, Suspense-ready",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -37,23 +37,25 @@
37
37
  "access": "public"
38
38
  },
39
39
  "scripts": {
40
- "build": "rm -rf dist && tsup",
41
- "dev": "tsup --watch",
42
- "test": "vitest run",
43
- "test:watch": "vitest",
44
- "test:coverage": "vitest run --coverage",
45
- "typecheck": "tsc --noEmit",
46
- "lint": "eslint .",
47
- "format": "prettier --write .",
48
- "format:check": "prettier --check .",
40
+ "build": "rm -rf dist && vp pack",
41
+ "dev": "vp pack --watch",
42
+ "test": "vp test run",
43
+ "test:watch": "vp test",
44
+ "test:coverage": "vp test run --coverage",
45
+ "check": "vp check",
46
+ "typecheck": "vp check --no-fmt --no-lint",
47
+ "lint": "vp lint",
48
+ "format": "vp fmt",
49
+ "format:check": "vp fmt --check",
49
50
  "check:package": "npm run build && publint --strict && attw --pack . --profile node16",
50
51
  "size": "npm run build && size-limit",
51
52
  "docs:dev": "vitepress dev docs",
52
53
  "docs:build": "vitepress build docs",
53
54
  "docs:preview": "vitepress preview docs",
54
- "example": "vite example",
55
- "example:server": "python3 example/server.py",
56
- "prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run check:package"
55
+ "example:tornado": "vp dev examples/tornado",
56
+ "example:tornado:server": "python3 examples/tornado/server.py",
57
+ "example:nextjs": "npm --prefix examples/nextjs run update-library && npm --prefix examples/nextjs run dev",
58
+ "prepublishOnly": "npm run check && npm test && npm run check:package"
57
59
  },
58
60
  "repository": {
59
61
  "type": "git",
@@ -81,29 +83,23 @@
81
83
  },
82
84
  "devDependencies": {
83
85
  "@arethetypeswrong/cli": "^0.18.5",
84
- "@eslint/js": "^10.0.1",
85
86
  "@fontsource-variable/geist-mono": "^5.3.0",
86
87
  "@size-limit/preset-small-lib": "^14.2.0",
87
88
  "@testing-library/react": "^16.3.3",
88
89
  "@types/react": "^19.3.0",
89
90
  "@types/react-dom": "^19.3.0",
90
91
  "@vitejs/plugin-react": "^6.1.2",
91
- "@vitest/coverage-v8": "^5.0.3",
92
- "eslint": "^10.12.0",
93
- "eslint-plugin-react-hooks": "^7.1.1",
94
- "globals": "^17.13.0",
92
+ "@vitest/coverage-v8": "5.0.3",
95
93
  "jsdom": "^30.1.2",
96
- "prettier": "^3.9.9",
97
94
  "publint": "^0.3.25",
98
95
  "react": "^19.3.0",
99
96
  "react-dom": "^19.3.0",
100
97
  "size-limit": "^14.2.0",
101
- "tsup": "^8.5.1",
102
- "typescript": "^6.0.3",
103
- "typescript-eslint": "^8.71.1",
104
- "vite": "^8.3.4",
98
+ "typescript": "^7.0.2",
99
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
100
+ "vite-plus": "1.1.0",
105
101
  "vitepress": "^1.6.4",
106
- "vitest": "^5.0.3"
102
+ "vitest": "5.0.3"
107
103
  },
108
104
  "size-limit": [
109
105
  {
@@ -113,7 +109,8 @@
113
109
  "ignore": [
114
110
  "react"
115
111
  ],
116
- "limit": "3 kB"
112
+ "limit": "3.2 kB",
113
+ "gzip": true
117
114
  },
118
115
  {
119
116
  "name": "react-websocket/core (createWebSocket)",
@@ -122,7 +119,8 @@
122
119
  "ignore": [
123
120
  "react"
124
121
  ],
125
- "limit": "2.6 kB"
122
+ "limit": "2.8 kB",
123
+ "gzip": true
126
124
  },
127
125
  {
128
126
  "name": "react-websocket (topics + state hooks)",
@@ -131,7 +129,8 @@
131
129
  "ignore": [
132
130
  "react"
133
131
  ],
134
- "limit": "1 kB"
132
+ "limit": "1 kB",
133
+ "gzip": true
135
134
  },
136
135
  {
137
136
  "name": "react-websocket (requests + latency)",
@@ -140,7 +139,18 @@
140
139
  "ignore": [
141
140
  "react"
142
141
  ],
143
- "limit": "1.3 kB"
142
+ "limit": "1.5 kB",
143
+ "gzip": true
144
+ },
145
+ {
146
+ "name": "react-websocket (responder + approval hook)",
147
+ "path": "dist/index.js",
148
+ "import": "{ createWebSocketResponder, useAgentApproval }",
149
+ "ignore": [
150
+ "react"
151
+ ],
152
+ "limit": "1.2 kB",
153
+ "gzip": true
144
154
  }
145
155
  ],
146
156
  "overrides": {
@@ -148,6 +158,11 @@
148
158
  "react": "$react",
149
159
  "react-dom": "$react-dom",
150
160
  "@types/react": "$@types/react"
161
+ },
162
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
163
+ "vitest": "5.0.3",
164
+ "vitepress": {
165
+ "vite": "^5.4.21"
151
166
  }
152
167
  }
153
168
  }