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/index.cjs CHANGED
@@ -22,7 +22,38 @@ const noopSend = () => false;
22
22
  const noop = () => {};
23
23
  const ON_TEARDOWN = Symbol.for("react-websocket.onTeardown");
24
24
  const registry = /* @__PURE__ */ new Map();
25
- function sharedClient(url, options) {
25
+ const origins = /* @__PURE__ */ new WeakMap();
26
+ const warned = /* @__PURE__ */ new Set();
27
+ const CONNECTION_OPTIONS = {
28
+ protocols: void 0,
29
+ binaryType: "blob",
30
+ json: false,
31
+ parse: void 0,
32
+ serialize: void 0,
33
+ schema: void 0,
34
+ reconnect: true,
35
+ heartbeat: void 0,
36
+ queue: true,
37
+ logger: void 0,
38
+ WebSocket: void 0
39
+ };
40
+ const IGNORED_OPTIONS = /* @__PURE__ */ new Set(["protocols", "logger"]);
41
+ const sameValue = (a, b) => Object.is(a, b) || typeof a === "function" && typeof b === "function" && String(a) === String(b);
42
+ const isPlainObject = (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
43
+ function sameOption(a, b) {
44
+ if (sameValue(a, b)) return true;
45
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
46
+ return [...Object.keys(a), ...Object.keys(b)].every((key) => sameValue(a[key], b[key]));
47
+ }
48
+ function warnOnConflict(url, key, client, options, id) {
49
+ const origin = origins.get(client);
50
+ if (!origin || origin.id === id || warned.has(`${key}\u0000${id}`)) return;
51
+ const differing = Object.keys(CONNECTION_OPTIONS).filter((name) => !IGNORED_OPTIONS.has(name) && !sameOption(origin.options[name] ?? CONNECTION_OPTIONS[name], options[name] ?? CONNECTION_OPTIONS[name]));
52
+ if (!differing.length) return;
53
+ warned.add(`${key}\u0000${id}`);
54
+ console.warn(`react-websocket: useWebSocket("${url}") was given different ${differing.join(", ")} than the component that opened this shared connection. They are ignored: every component using the URL gets the first one's options. Pass the same options, or define the connection once with createWebSocket().`);
55
+ }
56
+ function sharedClient(url, options, id) {
26
57
  if (typeof window === "undefined") return (0, __core_cjs.createWebSocket)(url, options);
27
58
  const key = `${url}\u0000${JSON.stringify(options.protocols ?? null)}`;
28
59
  let client = registry.get(key);
@@ -35,7 +66,11 @@ function sharedClient(url, options) {
35
66
  });
36
67
  registry.set(key, created);
37
68
  client = created;
38
- }
69
+ if (process.env.NODE_ENV !== "production") origins.set(created, {
70
+ options,
71
+ id
72
+ });
73
+ } else if (process.env.NODE_ENV !== "production") warnOnConflict(url, key, client, options, id);
39
74
  return client;
40
75
  }
41
76
  /** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */
@@ -52,7 +87,8 @@ function useHold(client, enabled) {
52
87
  }, [client, enabled]);
53
88
  }
54
89
  function useWebSocket(target, options = {}) {
55
- const client = target == null ? null : typeof target === "string" ? sharedClient(target, options) : target;
90
+ const id = (0, react.useId)();
91
+ const client = target == null ? null : typeof target === "string" ? sharedClient(target, options, id) : target;
56
92
  const enabled = options.enabled ?? true;
57
93
  const snapshot = (0, react.useSyncExternalStore)(client ? client.subscribe : noopSubscribe, client ? client.getSnapshot : idleSnapshot, idleSnapshot);
58
94
  const handlers = useLatest(options);
@@ -219,6 +255,69 @@ function useAgentApproval(responder, method) {
219
255
  return mine.length ? mine : NO_REQUESTS;
220
256
  }, [waiting, method]);
221
257
  }
258
+ const append = (list, reply) => [...list, reply];
259
+ function nextFrame(callback) {
260
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(callback);
261
+ else setTimeout(callback, 16);
262
+ }
263
+ function useWebSocketStream(rpc, options = {}) {
264
+ const [state, setState] = (0, react.useState)(() => ({
265
+ value: options.reduce ? options.initial : NO_REQUESTS,
266
+ status: "idle",
267
+ error: void 0
268
+ }));
269
+ const latest = useLatest({
270
+ rpc,
271
+ options
272
+ });
273
+ const running = (0, react.useRef)(null);
274
+ const actions = (0, react.useMemo)(() => ({
275
+ abort: () => running.current?.abort(),
276
+ start(message, requestOptions) {
277
+ running.current?.abort();
278
+ const controller = running.current = new AbortController();
279
+ const { reduce, initial, batch } = latest.current.options;
280
+ const fold = reduce ?? append;
281
+ let value = reduce ? initial : NO_REQUESTS;
282
+ let frame = false;
283
+ const show = (status, error) => {
284
+ frame = false;
285
+ if (running.current === controller) setState({
286
+ value,
287
+ status,
288
+ error
289
+ });
290
+ };
291
+ show("streaming");
292
+ (async () => {
293
+ try {
294
+ const replies = latest.current.rpc.stream(message, {
295
+ ...requestOptions,
296
+ signal: controller.signal
297
+ });
298
+ for await (const reply of replies) {
299
+ value = fold(value, reply);
300
+ if (batch !== "frame") show("streaming");
301
+ else if (!frame) {
302
+ frame = true;
303
+ nextFrame(() => frame && show("streaming"));
304
+ }
305
+ }
306
+ show("done");
307
+ } catch (error) {
308
+ if (controller.signal.aborted) show("aborted");
309
+ else show("error", error);
310
+ }
311
+ })();
312
+ }
313
+ }), [latest]);
314
+ (0, react.useEffect)(() => actions.abort, [actions, rpc]);
315
+ useHold(rpc?.client ?? null, true);
316
+ return {
317
+ ...state,
318
+ ...actions
319
+ };
320
+ }
222
321
  //#endregion
223
322
  exports.useAgentApproval = useAgentApproval;
224
323
  exports.useWebSocket = useWebSocket;
@@ -226,6 +325,7 @@ exports.useWebSocketEvent = useWebSocketEvent;
226
325
  exports.useWebSocketLatency = useWebSocketLatency;
227
326
  exports.useWebSocketState = useWebSocketState;
228
327
  exports.useWebSocketStatus = useWebSocketStatus;
328
+ exports.useWebSocketStream = useWebSocketStream;
229
329
  exports.useWebSocketTopic = useWebSocketTopic;
230
330
  Object.keys(__core_cjs).forEach(function(k) {
231
331
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["createWebSocket","useRef","useSyncExternalStore","useMemo"],"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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"}
1
+ {"version":3,"file":"index.cjs","names":["createWebSocket","useRef","useId","useSyncExternalStore","useMemo","useState"],"sources":["../src/react.ts"],"sourcesContent":["import {\n useEffect,\n useId,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore\n} 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 { RequestOptions, WebSocketRequests } from './requests';\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\n// Development only: the options (and `useId` of the component) each URL client was created with,\n// to warn when another component passes different ones for the same URL.\nconst origins = new WeakMap<\n WebSocketClient<any, any>,\n { options: WebSocketOptions<any, any>; id: string }\n>();\nconst warned = new Set<string>();\n\n// The options that shape a shared connection, with what leaving one out means.\nconst CONNECTION_OPTIONS: { [K in keyof WebSocketOptions]-?: unknown } = {\n protocols: undefined, // part of the registry key\n binaryType: 'blob',\n json: false,\n parse: undefined,\n serialize: undefined,\n schema: undefined,\n reconnect: true,\n heartbeat: undefined,\n queue: true,\n logger: undefined, // debugging only\n WebSocket: undefined\n};\nconst IGNORED_OPTIONS = new Set(['protocols', 'logger']);\n\n// Inline functions and objects are new on every render: compare them by source and by field.\nconst sameValue = (a: unknown, b: unknown) =>\n Object.is(a, b) ||\n (typeof a === 'function' && typeof b === 'function' && String(a) === String(b));\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype;\n\nfunction sameOption(a: unknown, b: unknown) {\n if (sameValue(a, b)) return true;\n if (!isPlainObject(a) || !isPlainObject(b)) return false;\n return [...Object.keys(a), ...Object.keys(b)].every(key => sameValue(a[key], b[key]));\n}\n\nfunction warnOnConflict(\n url: string,\n key: string,\n client: WebSocketClient<any, any>,\n options: WebSocketOptions<any, any>,\n id: string\n) {\n const origin = origins.get(client);\n if (!origin || origin.id === id || warned.has(`${key}\\u0000${id}`)) return;\n const differing = (Object.keys(CONNECTION_OPTIONS) as (keyof WebSocketOptions)[]).filter(\n name =>\n !IGNORED_OPTIONS.has(name) &&\n !sameOption(\n origin.options[name] ?? CONNECTION_OPTIONS[name],\n options[name] ?? CONNECTION_OPTIONS[name]\n )\n );\n if (!differing.length) return;\n warned.add(`${key}\\u0000${id}`);\n console.warn(\n `react-websocket: useWebSocket(\"${url}\") was given different ${differing.join(', ')} than ` +\n `the component that opened this shared connection. They are ignored: every component ` +\n `using the URL gets the first one's options. Pass the same options, or define the ` +\n `connection once with createWebSocket().`\n );\n}\n\nfunction sharedClient<In, Out>(url: string, options: WebSocketOptions<In, Out>, id: string) {\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 if (process.env.NODE_ENV !== 'production') origins.set(created, { options, id });\n } else if (process.env.NODE_ENV !== 'production') {\n warnOnConflict(url, key, client, options, id);\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; in\n * development, a component passing different ones gets a console warning.\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 id = useId();\n const client =\n target == null ? null : typeof target === 'string' ? sharedClient(target, options, id) : 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\n/** Options for `useWebSocketStream` that fold replies into one value, e.g. tokens into text. */\nexport interface UseWebSocketStreamOptions<In, T> {\n /** The value before the first reply. `start()` resets to it. */\n initial: T;\n /** Fold a reply into the value. Return a new value instead of changing the old one. */\n reduce: (value: T, reply: In) => T;\n /**\n * `'frame'`: re-render at most once per animation frame instead of once per reply. Browsers\n * pause frames in background tabs; the value catches up when the tab is visible again.\n */\n batch?: 'frame';\n}\n\n/** What `useWebSocketStream` returns: the stream's progress plus stable actions. */\nexport interface UseWebSocketStreamResult<Req, T> {\n /** The replies so far, folded with `reduce`, or as an array without it. */\n readonly value: T;\n /**\n * `idle` until `start()`. `done` after the last reply, `error` when the stream failed (see\n * `error`), `aborted` after `abort()`. The value stays as it was when the stream ended.\n */\n readonly status: 'idle' | 'streaming' | 'done' | 'error' | 'aborted';\n /** Why the stream failed, while `status` is `error`. */\n readonly error: unknown;\n /** Reset the value and stream the replies to `message`, aborting the stream before. Stable identity. */\n start: (message: Req, options?: Omit<RequestOptions, 'signal'>) => void;\n /** Stop the stream and send the manager's `cancel`. Stable identity. */\n abort: () => void;\n}\n\nconst append = <T>(list: readonly T[], reply: T): readonly T[] => [...list, reply];\n\n// Node and some workers have no animation frames: wait about one instead.\nfunction nextFrame(callback: () => void) {\n if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback);\n else setTimeout(callback, 16);\n}\n\n/**\n * Stream the replies to a request into a value, e.g. a model's tokens into its answer.\n * `start(message)` sends it with `rpc.stream()` and folds each reply in with `reduce`, or\n * collects the replies without it. Starting again aborts the stream before; so do `abort()`,\n * unmounting and a new `rpc`, and each sends the manager's `cancel`. Re-renders on every\n * reply, or once per frame with `batch: 'frame'`. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * const answer = useWebSocketStream(ask, {\n * initial: '',\n * reduce: (text, reply) => text + reply.delta,\n * batch: 'frame'\n * });\n * return (\n * <>\n * <button onClick={() => answer.start({ prompt })}>Ask</button>\n * <button onClick={answer.abort} disabled={answer.status !== 'streaming'}>Stop</button>\n * <p>{answer.value}</p>\n * </>\n * );\n * ```\n */\nexport function useWebSocketStream<Req, In, T>(\n rpc: WebSocketRequests<Req, In> | null | undefined,\n options: UseWebSocketStreamOptions<NoInfer<In>, T>\n): UseWebSocketStreamResult<Req, T>;\nexport function useWebSocketStream<Req, In>(\n rpc: WebSocketRequests<Req, In> | null | undefined,\n options?: { batch?: 'frame' }\n): UseWebSocketStreamResult<Req, readonly In[]>;\nexport function useWebSocketStream<Req, In>(\n rpc: WebSocketRequests<Req, In> | null | undefined,\n options: Partial<UseWebSocketStreamOptions<In, any>> = {}\n): UseWebSocketStreamResult<Req, unknown> {\n type Status = UseWebSocketStreamResult<Req, unknown>['status'];\n const [state, setState] = useState<{ value: unknown; status: Status; error: unknown }>(() => ({\n value: options.reduce ? options.initial : NO_REQUESTS,\n status: 'idle',\n error: undefined\n }));\n const latest = useLatest({ rpc, options });\n const running = useRef<AbortController | null>(null);\n\n const actions = useMemo(\n () => ({\n abort: () => running.current?.abort(),\n start(message: Req, requestOptions?: Omit<RequestOptions, 'signal'>) {\n running.current?.abort();\n const controller = (running.current = new AbortController());\n const { reduce, initial, batch } = latest.current.options;\n const fold = reduce ?? append;\n let value: unknown = reduce ? initial : NO_REQUESTS;\n let frame = false;\n // A stream that was started over no longer shows; ending also skips a pending frame.\n const show = (status: Status, error?: unknown) => {\n frame = false;\n if (running.current === controller) setState({ value, status, error });\n };\n show('streaming');\n void (async () => {\n try {\n const replies = latest.current.rpc!.stream(message, {\n ...requestOptions,\n signal: controller.signal\n });\n for await (const reply of replies) {\n value = fold(value, reply);\n if (batch !== 'frame') show('streaming');\n else if (!frame) {\n frame = true;\n nextFrame(() => frame && show('streaming'));\n }\n }\n show('done');\n } catch (error) {\n if (controller.signal.aborted) show('aborted');\n else show('error', error);\n }\n })();\n }\n }),\n [latest]\n );\n // Unmounting, or replies from another manager: stop this one's stream.\n useEffect(() => actions.abort, [actions, rpc]);\n useHold(rpc?.client ?? null, true);\n\n return { ...state, ...actions };\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -30,7 +30,10 @@ interface Snapshot<In> {
30
30
  readonly status: Status;
31
31
  /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
32
32
  readonly lastMessage: In | undefined;
33
- /** Consecutive failed attempts since the last successful open. `0` while open. */
33
+ /**
34
+ * Consecutive failed attempts since the last stable connection. A connection that closes
35
+ * within `reconnect.minUptime` of opening counts as failed. `0` while open.
36
+ */
34
37
  readonly attempt: number;
35
38
  }
36
39
  /** Details of a closed connection, from the browser's `CloseEvent`. */
@@ -52,6 +55,14 @@ interface ReconnectOptions {
52
55
  * @default Infinity
53
56
  */
54
57
  maxAttempts?: number;
58
+ /**
59
+ * How long a connection must stay open, in ms, before the backoff resets. One that closes
60
+ * sooner counts as a failed attempt, so a server that accepts and then closes right away
61
+ * (e.g. rejecting the credentials sent on open) is retried with a growing delay, and
62
+ * `maxAttempts` still applies. `0` resets on every open.
63
+ * @default 5000
64
+ */
65
+ minUptime?: number;
55
66
  /**
56
67
  * Delay before the first retry, in ms.
57
68
  * @default 1000
@@ -69,19 +80,26 @@ interface ReconnectOptions {
69
80
  factor?: number;
70
81
  /**
71
82
  * Randomly shave up to this fraction (0–1) off each delay, so that many clients
72
- * dropped at once don't all reconnect at the same moment.
73
- * @default 0.5
83
+ * dropped at once don't all reconnect at the same moment. The default `1` picks each
84
+ * delay at random between 0 and the backoff ("full jitter").
85
+ * @default 1
74
86
  */
75
87
  jitter?: number;
76
88
  /**
77
89
  * Called on every unexpected close. Return `false` to stop reconnecting.
78
90
  *
91
+ * The default, `retryableClose`, retries everything except `1002` (protocol error), `1003`
92
+ * (unsupported data) and `1008` (policy violation). Your own function replaces it; call
93
+ * `retryableClose` from it to keep that rule, or return `true` to retry every close.
94
+ *
79
95
  * @example
80
96
  * ```ts
81
- * // Don't retry when the server rejects our credentials.
82
- * shouldReconnect: close => close.code !== 4001
97
+ * import { retryableClose } from 'react-websocket';
98
+ *
99
+ * // Also stop when our server rejects the credentials.
100
+ * shouldReconnect: close => close.code !== 4001 && retryableClose(close)
83
101
  * ```
84
- * @default () => true
102
+ * @default retryableClose
85
103
  */
86
104
  shouldReconnect?: (close: CloseInfo) => boolean;
87
105
  }
@@ -156,7 +174,8 @@ interface QueueOptions {
156
174
  /**
157
175
  * Maximum number of messages held while not connected. When full, the oldest
158
176
  * message is dropped, skipping `priority` messages unless nothing else is queued.
159
- * @default Infinity
177
+ * `Infinity` keeps every message.
178
+ * @default 100
160
179
  */
161
180
  limit?: number;
162
181
  }
@@ -232,8 +251,8 @@ interface WebSocketOptions<In = unknown, Out = unknown> {
232
251
  heartbeat?: HeartbeatOptions;
233
252
  /**
234
253
  * Buffer `send()` calls made while not connected and send them, in order, on open
235
- * (right after `open` listeners run).
236
- * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
254
+ * (right after `open` listeners run). It holds up to 100 messages, dropping the oldest;
255
+ * `{ limit }` changes that. `false` drops messages instead of queueing them.
237
256
  * The queue is cleared by `close()` or when the client gives up.
238
257
  * @default true
239
258
  */
@@ -375,6 +394,20 @@ interface WebSocketClient<In = unknown, Out = unknown> {
375
394
  }
376
395
  //#endregion
377
396
  //#region src/client.d.ts
397
+ /**
398
+ * The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
399
+ * `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
400
+ * retrying the same way can't fix. Call it from your own `shouldReconnect` to keep that rule.
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * createWebSocket(url, {
405
+ * // Also stop on our server's "invalid token" code.
406
+ * reconnect: { shouldReconnect: close => close.code !== 4001 && retryableClose(close) }
407
+ * });
408
+ * ```
409
+ */
410
+ export declare function retryableClose(close: CloseInfo): boolean;
378
411
  /**
379
412
  * Create a WebSocket client, usually at module level, to share across components.
380
413
  *
@@ -488,20 +521,36 @@ interface WebSocketRequestsOptions<Req, In, Out> {
488
521
  reply: (message: In) => number | string | null | undefined;
489
522
  /**
490
523
  * Called on a reply. Return something other than `undefined` to reject the request with
491
- * it instead of resolving, e.g. for a server-side error reply.
524
+ * it instead of resolving, e.g. for a server-side error reply. A stream throws it.
492
525
  */
493
526
  error?: (message: In) => unknown;
494
527
  /**
495
- * Default time to wait for a reply, in ms. `Infinity` waits forever.
528
+ * Whether a reply is the last one of a stream. `stream()` needs it. That reply is yielded
529
+ * too, then the stream ends.
530
+ */
531
+ done?: (message: In) => boolean;
532
+ /**
533
+ * Build the message that tells the server to stop working on request `id`. Sent when this
534
+ * side gives up: the request's `signal` aborts, it times out, a loop over a stream exits
535
+ * early, or the manager is disposed. Not sent after the last reply, an error reply, a lost
536
+ * connection or `close()`.
537
+ */
538
+ cancel?: (id: number) => Out;
539
+ /**
540
+ * Default time to wait for a reply, in ms. For a stream, the time to wait for each reply,
541
+ * including the first. `Infinity` waits forever.
496
542
  * @default 10000
497
543
  */
498
544
  timeout?: number;
499
545
  }
500
- /** Per-call options for {@link WebSocketRequests.request}. */
546
+ /** Per-call options for {@link WebSocketRequests.request} and {@link WebSocketRequests.stream}. */
501
547
  interface RequestOptions extends SendOptions {
502
- /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
548
+ /**
549
+ * Time to wait for a reply (for a stream, for each reply), in ms. Overrides the manager's
550
+ * `timeout`.
551
+ */
503
552
  timeout?: number;
504
- /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
553
+ /** Abort the request. It rejects (a stream throws) with `signal.reason`. A late reply is ignored. */
505
554
  signal?: AbortSignal;
506
555
  }
507
556
  /** Request/response over one client. Create with {@link createWebSocketRequests}. */
@@ -518,7 +567,26 @@ interface WebSocketRequests<Req, In> {
518
567
  * queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
519
568
  */
520
569
  request(message: Req, options?: RequestOptions): Promise<In>;
521
- /** Number of requests waiting for a reply. */
570
+ /**
571
+ * Send a request and iterate over its replies, up to and including the one `done` matches.
572
+ * Holds the connection until the stream ends. Needs the `done` option.
573
+ *
574
+ * `timeout` is the time to wait for each reply. Replies that arrive before the loop reads
575
+ * them are buffered. The stream is one-shot: iterate it once.
576
+ *
577
+ * Fails where `request()` rejects, with the same errors, after yielding the replies already
578
+ * received. Aborting `signal` throws `signal.reason` right away and discards them. Leaving
579
+ * the loop early, aborting and timing out send `cancel`.
580
+ *
581
+ * @example
582
+ * ```ts
583
+ * for await (const reply of rpc.stream({ type: 'ask', prompt }, { signal })) {
584
+ * text += reply.delta;
585
+ * }
586
+ * ```
587
+ */
588
+ stream(message: Req, options?: RequestOptions): AsyncIterableIterator<In>;
589
+ /** Number of requests and streams waiting for a reply. */
522
590
  pending(): number;
523
591
  /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
524
592
  dispose(): void;
@@ -910,7 +978,8 @@ interface UseWebSocketResult<In, Out> extends Snapshot<In> {
910
978
  * Connect to a WebSocket and re-render with its status and latest message.
911
979
  *
912
980
  * - With a URL, every component using the same URL (and `protocols`) shares one
913
- * connection. Its options come from whichever component created it first.
981
+ * connection. Its options come from whichever component created it first; in
982
+ * development, a component passing different ones gets a console warning.
914
983
  * - With a client from `createWebSocket()`, options are the client's; only handlers
915
984
  * and `enabled` apply here.
916
985
  * - With `null` or `undefined`, nothing connects and the status stays `idle`.
@@ -1012,6 +1081,61 @@ export declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
1012
1081
  * ```
1013
1082
  */
1014
1083
  export declare function useAgentApproval<P, R>(responder: WebSocketResponder<P, R> | null | undefined, method: string | null | undefined): readonly IncomingRequest<P, R>[];
1084
+ /** Options for `useWebSocketStream` that fold replies into one value, e.g. tokens into text. */
1085
+ interface UseWebSocketStreamOptions<In, T> {
1086
+ /** The value before the first reply. `start()` resets to it. */
1087
+ initial: T;
1088
+ /** Fold a reply into the value. Return a new value instead of changing the old one. */
1089
+ reduce: (value: T, reply: In) => T;
1090
+ /**
1091
+ * `'frame'`: re-render at most once per animation frame instead of once per reply. Browsers
1092
+ * pause frames in background tabs; the value catches up when the tab is visible again.
1093
+ */
1094
+ batch?: 'frame';
1095
+ }
1096
+ /** What `useWebSocketStream` returns: the stream's progress plus stable actions. */
1097
+ interface UseWebSocketStreamResult<Req, T> {
1098
+ /** The replies so far, folded with `reduce`, or as an array without it. */
1099
+ readonly value: T;
1100
+ /**
1101
+ * `idle` until `start()`. `done` after the last reply, `error` when the stream failed (see
1102
+ * `error`), `aborted` after `abort()`. The value stays as it was when the stream ended.
1103
+ */
1104
+ readonly status: 'idle' | 'streaming' | 'done' | 'error' | 'aborted';
1105
+ /** Why the stream failed, while `status` is `error`. */
1106
+ readonly error: unknown;
1107
+ /** Reset the value and stream the replies to `message`, aborting the stream before. Stable identity. */
1108
+ start: (message: Req, options?: Omit<RequestOptions, 'signal'>) => void;
1109
+ /** Stop the stream and send the manager's `cancel`. Stable identity. */
1110
+ abort: () => void;
1111
+ }
1112
+ /**
1113
+ * Stream the replies to a request into a value, e.g. a model's tokens into its answer.
1114
+ * `start(message)` sends it with `rpc.stream()` and folds each reply in with `reduce`, or
1115
+ * collects the replies without it. Starting again aborts the stream before; so do `abort()`,
1116
+ * unmounting and a new `rpc`, and each sends the manager's `cancel`. Re-renders on every
1117
+ * reply, or once per frame with `batch: 'frame'`. Keeps the connection open while mounted.
1118
+ *
1119
+ * @example
1120
+ * ```tsx
1121
+ * const answer = useWebSocketStream(ask, {
1122
+ * initial: '',
1123
+ * reduce: (text, reply) => text + reply.delta,
1124
+ * batch: 'frame'
1125
+ * });
1126
+ * return (
1127
+ * <>
1128
+ * <button onClick={() => answer.start({ prompt })}>Ask</button>
1129
+ * <button onClick={answer.abort} disabled={answer.status !== 'streaming'}>Stop</button>
1130
+ * <p>{answer.value}</p>
1131
+ * </>
1132
+ * );
1133
+ * ```
1134
+ */
1135
+ export declare function useWebSocketStream<Req, In, T>(rpc: WebSocketRequests<Req, In> | null | undefined, options: UseWebSocketStreamOptions<NoInfer<In>, T>): UseWebSocketStreamResult<Req, T>;
1136
+ export declare function useWebSocketStream<Req, In>(rpc: WebSocketRequests<Req, In> | null | undefined, options?: {
1137
+ batch?: 'frame';
1138
+ }): UseWebSocketStreamResult<Req, readonly In[]>;
1015
1139
  //#endregion
1016
- export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, SendRate, Snapshot, StandardSchemaV1, Status, UrlSource, UseWebSocketOptions, UseWebSocketResult, WebSocketClient, WebSocketHandlers, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketSendRate, WebSocketSendRateOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
1140
+ export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, SendRate, Snapshot, StandardSchemaV1, Status, UrlSource, UseWebSocketOptions, UseWebSocketResult, UseWebSocketStreamOptions, UseWebSocketStreamResult, WebSocketClient, WebSocketHandlers, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketSendRate, WebSocketSendRateOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
1017
1141
  //# sourceMappingURL=index.d.cts.map