react-websocket 3.3.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/chaos.cjs +284 -0
- package/dist/chaos.cjs.map +1 -0
- package/dist/chaos.d.cts +131 -0
- package/dist/chaos.d.ts +131 -0
- package/dist/chaos.js +283 -0
- package/dist/chaos.js.map +1 -0
- package/dist/core.cjs +271 -72
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +166 -11
- package/dist/core.d.ts +166 -11
- package/dist/core.js +271 -73
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +221 -11
- package/dist/index.d.ts +140 -5
- package/dist/index.js +65 -2
- package/dist/index.js.map +1 -1
- package/dist/timeline.cjs +162 -0
- package/dist/timeline.cjs.map +1 -0
- package/dist/timeline.d.cts +379 -0
- package/dist/timeline.d.ts +140 -0
- package/dist/timeline.js +161 -0
- package/dist/timeline.js.map +1 -0
- package/package.json +67 -16
package/dist/index.cjs.map
CHANGED
|
@@ -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","useSyncExternalStore","useMemo","useState"],"sources":["../src/react.ts"],"sourcesContent":["import {\n useEffect,\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\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\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
|
@@ -112,14 +112,42 @@ interface SendOptions {
|
|
|
112
112
|
volatile?: boolean;
|
|
113
113
|
/**
|
|
114
114
|
* Keep only the newest queued message with this key. Sending again with the same key
|
|
115
|
-
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
115
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
116
|
+
* (or of the `priority` messages, for a priority one).
|
|
116
117
|
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
117
118
|
*/
|
|
118
119
|
key?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Queue this message ahead of every message queued without `priority`, after earlier
|
|
122
|
+
* priority messages. For control messages that must not wait behind a backlog, such as
|
|
123
|
+
* cancel, interrupt or a token refresh. Messages sent from `open` listeners still go first.
|
|
124
|
+
*
|
|
125
|
+
* It only matters while the socket isn't open: once a message is handed to the browser,
|
|
126
|
+
* nothing can overtake it. To keep that buffer short, send bulky streams with `maxBuffered`.
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
priority?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Drop the message instead of sending it while more than this many bytes are already
|
|
132
|
+
* waiting in the browser's send buffer (`socket.bufferedAmount`). For high-rate streams on
|
|
133
|
+
* a slow link, such as audio frames or per-tick input, where a late message is worse than
|
|
134
|
+
* none. `send()` returns `false`, and `onDrop` isn't called.
|
|
135
|
+
*
|
|
136
|
+
* Only checked while the socket is open: a message queued while disconnected is sent on
|
|
137
|
+
* open regardless. `bufferedAmount` includes what you sent earlier in the same task, so set
|
|
138
|
+
* it well above one burst of messages.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* voice.send(frame, { volatile: true, maxBuffered: 64 * 1024 });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
maxBuffered?: number;
|
|
119
146
|
/**
|
|
120
147
|
* Called if the message is dropped from the queue without being sent: replaced by a
|
|
121
148
|
* newer message with the same `key`, or evicted by `queue.limit`. Not called when the
|
|
122
|
-
* queue is cleared because the client closed or gave up
|
|
149
|
+
* queue is cleared because the client closed or gave up, or when `send()` drops the
|
|
150
|
+
* message right away (`volatile` while not open, or `maxBuffered`) and returns `false`.
|
|
123
151
|
*/
|
|
124
152
|
onDrop?: () => void;
|
|
125
153
|
}
|
|
@@ -127,7 +155,7 @@ interface SendOptions {
|
|
|
127
155
|
interface QueueOptions {
|
|
128
156
|
/**
|
|
129
157
|
* Maximum number of messages held while not connected. When full, the oldest
|
|
130
|
-
* message is dropped.
|
|
158
|
+
* message is dropped, skipping `priority` messages unless nothing else is queued.
|
|
131
159
|
* @default Infinity
|
|
132
160
|
*/
|
|
133
161
|
limit?: number;
|
|
@@ -310,12 +338,14 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
310
338
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
311
339
|
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
312
340
|
* `options.volatile` is set.
|
|
313
|
-
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
341
|
+
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
342
|
+
* (`volatile` while not open, or `maxBuffered` while open).
|
|
314
343
|
*
|
|
315
344
|
* @example
|
|
316
345
|
* ```ts
|
|
317
346
|
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
318
347
|
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
348
|
+
* game.send({ type: 'leave' }, { priority: true }); // ahead of everything already queued
|
|
319
349
|
* ```
|
|
320
350
|
*/
|
|
321
351
|
send(message: Out, options?: SendOptions): boolean;
|
|
@@ -458,20 +488,36 @@ interface WebSocketRequestsOptions<Req, In, Out> {
|
|
|
458
488
|
reply: (message: In) => number | string | null | undefined;
|
|
459
489
|
/**
|
|
460
490
|
* Called on a reply. Return something other than `undefined` to reject the request with
|
|
461
|
-
* it instead of resolving, e.g. for a server-side error reply.
|
|
491
|
+
* it instead of resolving, e.g. for a server-side error reply. A stream throws it.
|
|
462
492
|
*/
|
|
463
493
|
error?: (message: In) => unknown;
|
|
464
494
|
/**
|
|
465
|
-
*
|
|
495
|
+
* Whether a reply is the last one of a stream. `stream()` needs it. That reply is yielded
|
|
496
|
+
* too, then the stream ends.
|
|
497
|
+
*/
|
|
498
|
+
done?: (message: In) => boolean;
|
|
499
|
+
/**
|
|
500
|
+
* Build the message that tells the server to stop working on request `id`. Sent when this
|
|
501
|
+
* side gives up: the request's `signal` aborts, it times out, a loop over a stream exits
|
|
502
|
+
* early, or the manager is disposed. Not sent after the last reply, an error reply, a lost
|
|
503
|
+
* connection or `close()`.
|
|
504
|
+
*/
|
|
505
|
+
cancel?: (id: number) => Out;
|
|
506
|
+
/**
|
|
507
|
+
* Default time to wait for a reply, in ms. For a stream, the time to wait for each reply,
|
|
508
|
+
* including the first. `Infinity` waits forever.
|
|
466
509
|
* @default 10000
|
|
467
510
|
*/
|
|
468
511
|
timeout?: number;
|
|
469
512
|
}
|
|
470
|
-
/** Per-call options for {@link WebSocketRequests.request}. */
|
|
513
|
+
/** Per-call options for {@link WebSocketRequests.request} and {@link WebSocketRequests.stream}. */
|
|
471
514
|
interface RequestOptions extends SendOptions {
|
|
472
|
-
/**
|
|
515
|
+
/**
|
|
516
|
+
* Time to wait for a reply (for a stream, for each reply), in ms. Overrides the manager's
|
|
517
|
+
* `timeout`.
|
|
518
|
+
*/
|
|
473
519
|
timeout?: number;
|
|
474
|
-
/** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
|
|
520
|
+
/** Abort the request. It rejects (a stream throws) with `signal.reason`. A late reply is ignored. */
|
|
475
521
|
signal?: AbortSignal;
|
|
476
522
|
}
|
|
477
523
|
/** Request/response over one client. Create with {@link createWebSocketRequests}. */
|
|
@@ -488,7 +534,26 @@ interface WebSocketRequests<Req, In> {
|
|
|
488
534
|
* queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
|
|
489
535
|
*/
|
|
490
536
|
request(message: Req, options?: RequestOptions): Promise<In>;
|
|
491
|
-
/**
|
|
537
|
+
/**
|
|
538
|
+
* Send a request and iterate over its replies, up to and including the one `done` matches.
|
|
539
|
+
* Holds the connection until the stream ends. Needs the `done` option.
|
|
540
|
+
*
|
|
541
|
+
* `timeout` is the time to wait for each reply. Replies that arrive before the loop reads
|
|
542
|
+
* them are buffered. The stream is one-shot: iterate it once.
|
|
543
|
+
*
|
|
544
|
+
* Fails where `request()` rejects, with the same errors, after yielding the replies already
|
|
545
|
+
* received. Aborting `signal` throws `signal.reason` right away and discards them. Leaving
|
|
546
|
+
* the loop early, aborting and timing out send `cancel`.
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```ts
|
|
550
|
+
* for await (const reply of rpc.stream({ type: 'ask', prompt }, { signal })) {
|
|
551
|
+
* text += reply.delta;
|
|
552
|
+
* }
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
stream(message: Req, options?: RequestOptions): AsyncIterableIterator<In>;
|
|
556
|
+
/** Number of requests and streams waiting for a reply. */
|
|
492
557
|
pending(): number;
|
|
493
558
|
/** Reject every pending request and stop listening. Only needed for managers created dynamically. */
|
|
494
559
|
dispose(): void;
|
|
@@ -650,6 +715,85 @@ interface WebSocketResponder<P = unknown, R = unknown> {
|
|
|
650
715
|
*/
|
|
651
716
|
export declare function createWebSocketResponder<Q extends RequestFields, R, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketResponderOptions<Q, R, NoInfer<In>, NoInfer<Out>>): WebSocketResponder<ParamsOf<Q>, R>;
|
|
652
717
|
//#endregion
|
|
718
|
+
//#region src/sendrate.d.ts
|
|
719
|
+
/** Options for {@link createWebSocketSendRate}. */
|
|
720
|
+
interface WebSocketSendRateOptions {
|
|
721
|
+
/**
|
|
722
|
+
* Ticks per second while the connection keeps up.
|
|
723
|
+
* @default 30
|
|
724
|
+
*/
|
|
725
|
+
maxRate?: number;
|
|
726
|
+
/**
|
|
727
|
+
* The rate never drops below this, in ticks per second.
|
|
728
|
+
* @default 5
|
|
729
|
+
*/
|
|
730
|
+
minRate?: number;
|
|
731
|
+
/**
|
|
732
|
+
* Bytes waiting in the browser's send buffer (`socket.bufferedAmount`) above which a tick
|
|
733
|
+
* sends nothing and the rate halves.
|
|
734
|
+
* @default 1024
|
|
735
|
+
*/
|
|
736
|
+
maxBuffered?: number;
|
|
737
|
+
/**
|
|
738
|
+
* Measurements from `createWebSocketLatency` on the same client. With them, a round-trip time
|
|
739
|
+
* that rises also lowers the rate. Without them, only the send buffer is watched.
|
|
740
|
+
*/
|
|
741
|
+
latency?: Pick<WebSocketLatency, 'getSnapshot'>;
|
|
742
|
+
/**
|
|
743
|
+
* How far `rtt` may rise above the lowest one measured on this connection before the rate
|
|
744
|
+
* halves, in ms. A steady high ping keeps the full rate; only a rising one counts.
|
|
745
|
+
* @default 100
|
|
746
|
+
*/
|
|
747
|
+
maxRttIncrease?: number;
|
|
748
|
+
}
|
|
749
|
+
/** The current send rate. A new object is created on every change. */
|
|
750
|
+
interface SendRate {
|
|
751
|
+
/** Ticks per second, rounded. */
|
|
752
|
+
readonly rate: number;
|
|
753
|
+
/**
|
|
754
|
+
* Whether the last tick found the link congested: the send buffer above `maxBuffered`, or
|
|
755
|
+
* `rtt` risen more than `maxRttIncrease`.
|
|
756
|
+
*/
|
|
757
|
+
readonly congested: boolean;
|
|
758
|
+
}
|
|
759
|
+
/** Keyed sends coalesced at a rate that adapts to the link. Create with {@link createWebSocketSendRate}. */
|
|
760
|
+
interface WebSocketSendRate<Out> {
|
|
761
|
+
/** The client the messages are sent on. */
|
|
762
|
+
readonly client: WebSocketClient<any, Out>;
|
|
763
|
+
/**
|
|
764
|
+
* Send `message` at the next tick. A message with the same `key` still waiting for it is
|
|
765
|
+
* replaced, so only the newest one goes out. Sent with `volatile`.
|
|
766
|
+
*/
|
|
767
|
+
send(message: Out, key: string): void;
|
|
768
|
+
/** Send everything waiting now, e.g. right before a one-shot action that must carry the latest aim. */
|
|
769
|
+
flush(): void;
|
|
770
|
+
/** The current {@link SendRate}. Returns the same object until it changes. */
|
|
771
|
+
getSnapshot(): SendRate;
|
|
772
|
+
/** Called whenever the rate or `congested` changes. Compatible with `useSyncExternalStore`. */
|
|
773
|
+
subscribe(listener: () => void): () => void;
|
|
774
|
+
/** Stop sending and listening. Only needed for instances created dynamically. */
|
|
775
|
+
dispose(): void;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
|
|
779
|
+
* connection. Messages are coalesced per key between ticks. The rate drops by half when the
|
|
780
|
+
* send buffer fills up or the round trip grows, and climbs back one tick per second at a time
|
|
781
|
+
* when the link keeps up, so players on a poor connection degrade smoothly instead of building
|
|
782
|
+
* up lag.
|
|
783
|
+
*
|
|
784
|
+
* Ticks run only while the socket is open and something is waiting. Messages waiting during a
|
|
785
|
+
* reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
|
|
786
|
+
*
|
|
787
|
+
* @example
|
|
788
|
+
* ```ts
|
|
789
|
+
* export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
|
|
790
|
+
*
|
|
791
|
+
* // On every pointer move: at most 30 a second, fewer on a congested link.
|
|
792
|
+
* stream.send({ type: 'aim', angle }, 'aim');
|
|
793
|
+
* ```
|
|
794
|
+
*/
|
|
795
|
+
export declare function createWebSocketSendRate<Out>(client: WebSocketClient<any, Out>, options?: WebSocketSendRateOptions): WebSocketSendRate<Out>;
|
|
796
|
+
//#endregion
|
|
653
797
|
//#region src/state.d.ts
|
|
654
798
|
/** Options for {@link createWebSocketState}. */
|
|
655
799
|
interface WebSocketStateOptions<S, In> {
|
|
@@ -660,6 +804,17 @@ interface WebSocketStateOptions<S, In> {
|
|
|
660
804
|
* change, or the same `state` to ignore the message (subscribers aren't notified).
|
|
661
805
|
*/
|
|
662
806
|
reduce: (state: S, message: In) => S;
|
|
807
|
+
/**
|
|
808
|
+
* `'frame'`: notify subscribers at most once per animation frame instead of once per
|
|
809
|
+
* message, so a fast stream (tokens, tickers, game state) renders once per frame.
|
|
810
|
+
* `getState()` is always current; only notifications wait. `setState` and `reset` still
|
|
811
|
+
* notify right away.
|
|
812
|
+
*
|
|
813
|
+
* Browsers pause animation frames in background tabs: the state keeps up, and subscribers
|
|
814
|
+
* hear about it when the tab is visible again. Without `requestAnimationFrame` (Node, some
|
|
815
|
+
* workers), it waits about 16 ms instead.
|
|
816
|
+
*/
|
|
817
|
+
batch?: 'frame';
|
|
663
818
|
}
|
|
664
819
|
/**
|
|
665
820
|
* State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
|
|
@@ -892,6 +1047,61 @@ export declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
|
|
|
892
1047
|
* ```
|
|
893
1048
|
*/
|
|
894
1049
|
export declare function useAgentApproval<P, R>(responder: WebSocketResponder<P, R> | null | undefined, method: string | null | undefined): readonly IncomingRequest<P, R>[];
|
|
1050
|
+
/** Options for `useWebSocketStream` that fold replies into one value, e.g. tokens into text. */
|
|
1051
|
+
interface UseWebSocketStreamOptions<In, T> {
|
|
1052
|
+
/** The value before the first reply. `start()` resets to it. */
|
|
1053
|
+
initial: T;
|
|
1054
|
+
/** Fold a reply into the value. Return a new value instead of changing the old one. */
|
|
1055
|
+
reduce: (value: T, reply: In) => T;
|
|
1056
|
+
/**
|
|
1057
|
+
* `'frame'`: re-render at most once per animation frame instead of once per reply. Browsers
|
|
1058
|
+
* pause frames in background tabs; the value catches up when the tab is visible again.
|
|
1059
|
+
*/
|
|
1060
|
+
batch?: 'frame';
|
|
1061
|
+
}
|
|
1062
|
+
/** What `useWebSocketStream` returns: the stream's progress plus stable actions. */
|
|
1063
|
+
interface UseWebSocketStreamResult<Req, T> {
|
|
1064
|
+
/** The replies so far, folded with `reduce`, or as an array without it. */
|
|
1065
|
+
readonly value: T;
|
|
1066
|
+
/**
|
|
1067
|
+
* `idle` until `start()`. `done` after the last reply, `error` when the stream failed (see
|
|
1068
|
+
* `error`), `aborted` after `abort()`. The value stays as it was when the stream ended.
|
|
1069
|
+
*/
|
|
1070
|
+
readonly status: 'idle' | 'streaming' | 'done' | 'error' | 'aborted';
|
|
1071
|
+
/** Why the stream failed, while `status` is `error`. */
|
|
1072
|
+
readonly error: unknown;
|
|
1073
|
+
/** Reset the value and stream the replies to `message`, aborting the stream before. Stable identity. */
|
|
1074
|
+
start: (message: Req, options?: Omit<RequestOptions, 'signal'>) => void;
|
|
1075
|
+
/** Stop the stream and send the manager's `cancel`. Stable identity. */
|
|
1076
|
+
abort: () => void;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Stream the replies to a request into a value, e.g. a model's tokens into its answer.
|
|
1080
|
+
* `start(message)` sends it with `rpc.stream()` and folds each reply in with `reduce`, or
|
|
1081
|
+
* collects the replies without it. Starting again aborts the stream before; so do `abort()`,
|
|
1082
|
+
* unmounting and a new `rpc`, and each sends the manager's `cancel`. Re-renders on every
|
|
1083
|
+
* reply, or once per frame with `batch: 'frame'`. Keeps the connection open while mounted.
|
|
1084
|
+
*
|
|
1085
|
+
* @example
|
|
1086
|
+
* ```tsx
|
|
1087
|
+
* const answer = useWebSocketStream(ask, {
|
|
1088
|
+
* initial: '',
|
|
1089
|
+
* reduce: (text, reply) => text + reply.delta,
|
|
1090
|
+
* batch: 'frame'
|
|
1091
|
+
* });
|
|
1092
|
+
* return (
|
|
1093
|
+
* <>
|
|
1094
|
+
* <button onClick={() => answer.start({ prompt })}>Ask</button>
|
|
1095
|
+
* <button onClick={answer.abort} disabled={answer.status !== 'streaming'}>Stop</button>
|
|
1096
|
+
* <p>{answer.value}</p>
|
|
1097
|
+
* </>
|
|
1098
|
+
* );
|
|
1099
|
+
* ```
|
|
1100
|
+
*/
|
|
1101
|
+
export declare function useWebSocketStream<Req, In, T>(rpc: WebSocketRequests<Req, In> | null | undefined, options: UseWebSocketStreamOptions<NoInfer<In>, T>): UseWebSocketStreamResult<Req, T>;
|
|
1102
|
+
export declare function useWebSocketStream<Req, In>(rpc: WebSocketRequests<Req, In> | null | undefined, options?: {
|
|
1103
|
+
batch?: 'frame';
|
|
1104
|
+
}): UseWebSocketStreamResult<Req, readonly In[]>;
|
|
895
1105
|
//#endregion
|
|
896
|
-
export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, Snapshot, StandardSchemaV1, Status, UrlSource, UseWebSocketOptions, UseWebSocketResult, WebSocketClient, WebSocketHandlers, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
|
|
1106
|
+
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 };
|
|
897
1107
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -113,14 +113,42 @@ interface SendOptions {
|
|
|
113
113
|
volatile?: boolean;
|
|
114
114
|
/**
|
|
115
115
|
* Keep only the newest queued message with this key. Sending again with the same key
|
|
116
|
-
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
116
|
+
* while disconnected replaces the earlier one, which moves to the back of the queue
|
|
117
|
+
* (or of the `priority` messages, for a priority one).
|
|
117
118
|
* For state where only the latest value matters, such as "my position is now (x, y)".
|
|
118
119
|
*/
|
|
119
120
|
key?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Queue this message ahead of every message queued without `priority`, after earlier
|
|
123
|
+
* priority messages. For control messages that must not wait behind a backlog, such as
|
|
124
|
+
* cancel, interrupt or a token refresh. Messages sent from `open` listeners still go first.
|
|
125
|
+
*
|
|
126
|
+
* It only matters while the socket isn't open: once a message is handed to the browser,
|
|
127
|
+
* nothing can overtake it. To keep that buffer short, send bulky streams with `maxBuffered`.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
priority?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Drop the message instead of sending it while more than this many bytes are already
|
|
133
|
+
* waiting in the browser's send buffer (`socket.bufferedAmount`). For high-rate streams on
|
|
134
|
+
* a slow link, such as audio frames or per-tick input, where a late message is worse than
|
|
135
|
+
* none. `send()` returns `false`, and `onDrop` isn't called.
|
|
136
|
+
*
|
|
137
|
+
* Only checked while the socket is open: a message queued while disconnected is sent on
|
|
138
|
+
* open regardless. `bufferedAmount` includes what you sent earlier in the same task, so set
|
|
139
|
+
* it well above one burst of messages.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* voice.send(frame, { volatile: true, maxBuffered: 64 * 1024 });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
maxBuffered?: number;
|
|
120
147
|
/**
|
|
121
148
|
* Called if the message is dropped from the queue without being sent: replaced by a
|
|
122
149
|
* newer message with the same `key`, or evicted by `queue.limit`. Not called when the
|
|
123
|
-
* queue is cleared because the client closed or gave up
|
|
150
|
+
* queue is cleared because the client closed or gave up, or when `send()` drops the
|
|
151
|
+
* message right away (`volatile` while not open, or `maxBuffered`) and returns `false`.
|
|
124
152
|
*/
|
|
125
153
|
onDrop?: () => void;
|
|
126
154
|
}
|
|
@@ -128,7 +156,7 @@ interface SendOptions {
|
|
|
128
156
|
interface QueueOptions {
|
|
129
157
|
/**
|
|
130
158
|
* Maximum number of messages held while not connected. When full, the oldest
|
|
131
|
-
* message is dropped.
|
|
159
|
+
* message is dropped, skipping `priority` messages unless nothing else is queued.
|
|
132
160
|
* @default Infinity
|
|
133
161
|
*/
|
|
134
162
|
limit?: number;
|
|
@@ -311,12 +339,14 @@ interface WebSocketClient<In = unknown, Out = unknown> {
|
|
|
311
339
|
* Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
|
|
312
340
|
* open, the message is queued and sent on open (see the `queue` option), unless
|
|
313
341
|
* `options.volatile` is set.
|
|
314
|
-
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
342
|
+
* @returns `true` if it was sent immediately, `false` if it was queued or dropped
|
|
343
|
+
* (`volatile` while not open, or `maxBuffered` while open).
|
|
315
344
|
*
|
|
316
345
|
* @example
|
|
317
346
|
* ```ts
|
|
318
347
|
* game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
|
|
319
348
|
* game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
|
|
349
|
+
* game.send({ type: 'leave' }, { priority: true }); // ahead of everything already queued
|
|
320
350
|
* ```
|
|
321
351
|
*/
|
|
322
352
|
send(message: Out, options?: SendOptions): boolean;
|
|
@@ -372,6 +402,56 @@ interface WebSocketLatency {
|
|
|
372
402
|
dispose(): void;
|
|
373
403
|
}
|
|
374
404
|
//#endregion
|
|
405
|
+
//#region src/requests.d.ts
|
|
406
|
+
/** Per-call options for {@link WebSocketRequests.request} and {@link WebSocketRequests.stream}. */
|
|
407
|
+
interface RequestOptions extends SendOptions {
|
|
408
|
+
/**
|
|
409
|
+
* Time to wait for a reply (for a stream, for each reply), in ms. Overrides the manager's
|
|
410
|
+
* `timeout`.
|
|
411
|
+
*/
|
|
412
|
+
timeout?: number;
|
|
413
|
+
/** Abort the request. It rejects (a stream throws) with `signal.reason`. A late reply is ignored. */
|
|
414
|
+
signal?: AbortSignal;
|
|
415
|
+
}
|
|
416
|
+
/** Request/response over one client. Create with {@link createWebSocketRequests}. */
|
|
417
|
+
interface WebSocketRequests<Req, In> {
|
|
418
|
+
/** The client requests are sent on. */
|
|
419
|
+
readonly client: WebSocketClient<any, any>;
|
|
420
|
+
/**
|
|
421
|
+
* Send a request and resolve with its reply. Holds the connection until it settles.
|
|
422
|
+
*
|
|
423
|
+
* Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
|
|
424
|
+
* aborted, and with an `Error` when the connection it was sent on closes (the reply can
|
|
425
|
+
* no longer arrive) or the client stops. A request made while disconnected is queued like
|
|
426
|
+
* any other message and waits for the connection; it rejects if it is dropped from the
|
|
427
|
+
* queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
|
|
428
|
+
*/
|
|
429
|
+
request(message: Req, options?: RequestOptions): Promise<In>;
|
|
430
|
+
/**
|
|
431
|
+
* Send a request and iterate over its replies, up to and including the one `done` matches.
|
|
432
|
+
* Holds the connection until the stream ends. Needs the `done` option.
|
|
433
|
+
*
|
|
434
|
+
* `timeout` is the time to wait for each reply. Replies that arrive before the loop reads
|
|
435
|
+
* them are buffered. The stream is one-shot: iterate it once.
|
|
436
|
+
*
|
|
437
|
+
* Fails where `request()` rejects, with the same errors, after yielding the replies already
|
|
438
|
+
* received. Aborting `signal` throws `signal.reason` right away and discards them. Leaving
|
|
439
|
+
* the loop early, aborting and timing out send `cancel`.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* for await (const reply of rpc.stream({ type: 'ask', prompt }, { signal })) {
|
|
444
|
+
* text += reply.delta;
|
|
445
|
+
* }
|
|
446
|
+
* ```
|
|
447
|
+
*/
|
|
448
|
+
stream(message: Req, options?: RequestOptions): AsyncIterableIterator<In>;
|
|
449
|
+
/** Number of requests and streams waiting for a reply. */
|
|
450
|
+
pending(): number;
|
|
451
|
+
/** Reject every pending request and stop listening. Only needed for managers created dynamically. */
|
|
452
|
+
dispose(): void;
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
375
455
|
//#region src/responder.d.ts
|
|
376
456
|
/** A request from the server that hasn't been answered yet. */
|
|
377
457
|
interface IncomingRequest<P = unknown, R = unknown> {
|
|
@@ -616,6 +696,61 @@ export declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
|
|
|
616
696
|
* ```
|
|
617
697
|
*/
|
|
618
698
|
export declare function useAgentApproval<P, R>(responder: WebSocketResponder<P, R> | null | undefined, method: string | null | undefined): readonly IncomingRequest<P, R>[];
|
|
699
|
+
/** Options for `useWebSocketStream` that fold replies into one value, e.g. tokens into text. */
|
|
700
|
+
interface UseWebSocketStreamOptions<In, T> {
|
|
701
|
+
/** The value before the first reply. `start()` resets to it. */
|
|
702
|
+
initial: T;
|
|
703
|
+
/** Fold a reply into the value. Return a new value instead of changing the old one. */
|
|
704
|
+
reduce: (value: T, reply: In) => T;
|
|
705
|
+
/**
|
|
706
|
+
* `'frame'`: re-render at most once per animation frame instead of once per reply. Browsers
|
|
707
|
+
* pause frames in background tabs; the value catches up when the tab is visible again.
|
|
708
|
+
*/
|
|
709
|
+
batch?: 'frame';
|
|
710
|
+
}
|
|
711
|
+
/** What `useWebSocketStream` returns: the stream's progress plus stable actions. */
|
|
712
|
+
interface UseWebSocketStreamResult<Req, T> {
|
|
713
|
+
/** The replies so far, folded with `reduce`, or as an array without it. */
|
|
714
|
+
readonly value: T;
|
|
715
|
+
/**
|
|
716
|
+
* `idle` until `start()`. `done` after the last reply, `error` when the stream failed (see
|
|
717
|
+
* `error`), `aborted` after `abort()`. The value stays as it was when the stream ended.
|
|
718
|
+
*/
|
|
719
|
+
readonly status: 'idle' | 'streaming' | 'done' | 'error' | 'aborted';
|
|
720
|
+
/** Why the stream failed, while `status` is `error`. */
|
|
721
|
+
readonly error: unknown;
|
|
722
|
+
/** Reset the value and stream the replies to `message`, aborting the stream before. Stable identity. */
|
|
723
|
+
start: (message: Req, options?: Omit<RequestOptions, 'signal'>) => void;
|
|
724
|
+
/** Stop the stream and send the manager's `cancel`. Stable identity. */
|
|
725
|
+
abort: () => void;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Stream the replies to a request into a value, e.g. a model's tokens into its answer.
|
|
729
|
+
* `start(message)` sends it with `rpc.stream()` and folds each reply in with `reduce`, or
|
|
730
|
+
* collects the replies without it. Starting again aborts the stream before; so do `abort()`,
|
|
731
|
+
* unmounting and a new `rpc`, and each sends the manager's `cancel`. Re-renders on every
|
|
732
|
+
* reply, or once per frame with `batch: 'frame'`. Keeps the connection open while mounted.
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```tsx
|
|
736
|
+
* const answer = useWebSocketStream(ask, {
|
|
737
|
+
* initial: '',
|
|
738
|
+
* reduce: (text, reply) => text + reply.delta,
|
|
739
|
+
* batch: 'frame'
|
|
740
|
+
* });
|
|
741
|
+
* return (
|
|
742
|
+
* <>
|
|
743
|
+
* <button onClick={() => answer.start({ prompt })}>Ask</button>
|
|
744
|
+
* <button onClick={answer.abort} disabled={answer.status !== 'streaming'}>Stop</button>
|
|
745
|
+
* <p>{answer.value}</p>
|
|
746
|
+
* </>
|
|
747
|
+
* );
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
export declare function useWebSocketStream<Req, In, T>(rpc: WebSocketRequests<Req, In> | null | undefined, options: UseWebSocketStreamOptions<NoInfer<In>, T>): UseWebSocketStreamResult<Req, T>;
|
|
751
|
+
export declare function useWebSocketStream<Req, In>(rpc: WebSocketRequests<Req, In> | null | undefined, options?: {
|
|
752
|
+
batch?: 'frame';
|
|
753
|
+
}): UseWebSocketStreamResult<Req, readonly In[]>;
|
|
619
754
|
//#endregion
|
|
620
|
-
export type { UseWebSocketOptions, UseWebSocketResult, WebSocketHandlers };
|
|
755
|
+
export type { UseWebSocketOptions, UseWebSocketResult, UseWebSocketStreamOptions, UseWebSocketStreamResult, WebSocketHandlers };
|
|
621
756
|
//# sourceMappingURL=index.d.ts.map
|