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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { createWebSocket } from "./core.js";
3
- import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
+ import { useEffect, useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  export * from "./core.js";
5
5
  //#region src/react.ts
6
6
  const IDLE = Object.freeze({
@@ -219,7 +219,70 @@ function useAgentApproval(responder, method) {
219
219
  return mine.length ? mine : NO_REQUESTS;
220
220
  }, [waiting, method]);
221
221
  }
222
+ const append = (list, reply) => [...list, reply];
223
+ function nextFrame(callback) {
224
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(callback);
225
+ else setTimeout(callback, 16);
226
+ }
227
+ function useWebSocketStream(rpc, options = {}) {
228
+ const [state, setState] = useState(() => ({
229
+ value: options.reduce ? options.initial : NO_REQUESTS,
230
+ status: "idle",
231
+ error: void 0
232
+ }));
233
+ const latest = useLatest({
234
+ rpc,
235
+ options
236
+ });
237
+ const running = useRef(null);
238
+ const actions = useMemo(() => ({
239
+ abort: () => running.current?.abort(),
240
+ start(message, requestOptions) {
241
+ running.current?.abort();
242
+ const controller = running.current = new AbortController();
243
+ const { reduce, initial, batch } = latest.current.options;
244
+ const fold = reduce ?? append;
245
+ let value = reduce ? initial : NO_REQUESTS;
246
+ let frame = false;
247
+ const show = (status, error) => {
248
+ frame = false;
249
+ if (running.current === controller) setState({
250
+ value,
251
+ status,
252
+ error
253
+ });
254
+ };
255
+ show("streaming");
256
+ (async () => {
257
+ try {
258
+ const replies = latest.current.rpc.stream(message, {
259
+ ...requestOptions,
260
+ signal: controller.signal
261
+ });
262
+ for await (const reply of replies) {
263
+ value = fold(value, reply);
264
+ if (batch !== "frame") show("streaming");
265
+ else if (!frame) {
266
+ frame = true;
267
+ nextFrame(() => frame && show("streaming"));
268
+ }
269
+ }
270
+ show("done");
271
+ } catch (error) {
272
+ if (controller.signal.aborted) show("aborted");
273
+ else show("error", error);
274
+ }
275
+ })();
276
+ }
277
+ }), [latest]);
278
+ useEffect(() => actions.abort, [actions, rpc]);
279
+ useHold(rpc?.client ?? null, true);
280
+ return {
281
+ ...state,
282
+ ...actions
283
+ };
284
+ }
222
285
  //#endregion
223
- export { useAgentApproval, useWebSocket, useWebSocketEvent, useWebSocketLatency, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
286
+ export { useAgentApproval, useWebSocket, useWebSocketEvent, useWebSocketLatency, useWebSocketState, useWebSocketStatus, useWebSocketStream, useWebSocketTopic };
224
287
 
225
288
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n// Import the client through the `core` entry: the build keeps it external, so both entries\n// share one copy of it (and of `MessageError`).\nimport { createWebSocket } from './core';\nimport type { Latency, WebSocketLatency } from './latency';\n// Types only: a value import from anything but `./core` would bundle a second copy into this entry.\nimport type { IncomingRequest, WebSocketResponder } from './responder';\nimport type { WebSocketState } from './state';\nimport type { WebSocketTopics } from './topics';\nimport type {\n CloseInfo,\n EventMap,\n SendOptions,\n Snapshot,\n Status,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n\n/**\n * Per-component event handlers. They always see the component's latest props and\n * state, and changing them never reconnects.\n */\nexport interface WebSocketHandlers<In> {\n /** The socket opened (initially or after a reconnect). */\n onOpen?: (event: Event) => void;\n /** A message arrived and passed `parse`/`json` and `schema`. */\n onMessage?: (message: In, event: MessageEvent) => void;\n /** A socket closed. Called whether or not it will be retried. */\n onClose?: (close: CloseInfo) => void;\n /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */\n onError?: (error: Event | Error) => void;\n /**\n * `false` releases this component's hold on the connection and stops its handlers.\n * The connection closes once nothing else holds it.\n * @default true\n */\n enabled?: boolean;\n}\n\n/** Options for `useWebSocket(url, options)`: connection options plus handlers. */\nexport interface UseWebSocketOptions<In, Out>\n extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}\n\n/** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */\nexport interface UseWebSocketResult<In, Out> extends Snapshot<In> {\n /**\n * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).\n * Returns `true` if sent immediately. Stable identity.\n */\n send: (message: Out, options?: SendOptions) => boolean;\n /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */\n close: (code?: number, reason?: string) => void;\n /** Drop the socket and connect again right away. Stable identity. */\n reconnect: () => void;\n /** The underlying client, or `null` when the URL is `null`/`undefined`. */\n client: WebSocketClient<In, Out> | null;\n}\n\nconst IDLE: Snapshot<never> = Object.freeze({ status: 'idle', lastMessage: undefined, attempt: 0 });\nconst idleSnapshot = () => IDLE;\nconst idleStatus = (): Status => 'idle';\nconst NO_LATENCY: Latency = Object.freeze({ rtt: undefined, offset: undefined });\nconst noLatency = () => NO_LATENCY;\n// Not frozen: a call would keep it in bundles that never use the hook.\nconst NO_REQUESTS: readonly never[] = [];\nconst noRequests = () => NO_REQUESTS;\nconst noopSubscribe = () => () => {};\nconst noopSend = () => false;\nconst noop = () => {};\n\n// Same key as `ON_TEARDOWN` in ./client, which this entry must not import directly.\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\n// Hooks given the same URL (and protocols) share one connection.\nconst registry = new Map<string, WebSocketClient<any, any>>();\n\nfunction sharedClient<In, Out>(url: string, options: WebSocketOptions<In, Out>) {\n // Don't let server renders accumulate clients that can never be torn down.\n if (typeof window === 'undefined') return createWebSocket(url, options);\n const key = `${url}\\u0000${JSON.stringify(options.protocols ?? null)}`;\n let client = registry.get(key) as WebSocketClient<In, Out> | undefined;\n if (!client) {\n const created: WebSocketClient<In, Out> = createWebSocket(url, {\n ...options,\n [ON_TEARDOWN]: () => {\n if (registry.get(key) === created) registry.delete(key);\n }\n } as WebSocketOptions<In, Out>);\n registry.set(key, created);\n client = created;\n }\n return client;\n}\n\n/** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */\nfunction useLatest<T>(value: T) {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\nfunction useHold(client: WebSocketClient<any, any> | null, enabled: boolean) {\n useEffect(() => {\n if (client && enabled) return client.acquire();\n }, [client, enabled]);\n}\n\n/**\n * Connect to a WebSocket and re-render with its status and latest message.\n *\n * - With a URL, every component using the same URL (and `protocols`) shares one\n * connection. Its options come from whichever component created it first.\n * - With a client from `createWebSocket()`, options are the client's; only handlers\n * and `enabled` apply here.\n * - With `null` or `undefined`, nothing connects and the status stays `idle`.\n *\n * The connection opens on mount and closes one tick after the last component using it\n * unmounts, so StrictMode and `<Activity>` don't open duplicate sockets.\n *\n * Re-renders on status changes and on every message. For high-frequency streams use\n * `useWebSocketEvent` and `useWebSocketStatus` instead.\n *\n * @example\n * ```tsx\n * const { status, lastMessage, send } = useWebSocket<Price, Order>('wss://api.example.com/prices', {\n * json: true,\n * onMessage: price => console.log(price)\n * });\n * ```\n */\nexport function useWebSocket<In = unknown, Out = unknown>(\n url: string | null | undefined,\n options?: UseWebSocketOptions<In, Out>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n client: WebSocketClient<In, Out> | null | undefined,\n options?: WebSocketHandlers<In>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n target: string | WebSocketClient<In, Out> | null | undefined,\n options: UseWebSocketOptions<In, Out> = {}\n): UseWebSocketResult<In, Out> {\n const client =\n target == null ? null : typeof target === 'string' ? sharedClient(target, options) : target;\n const enabled = options.enabled ?? true;\n\n const snapshot = useSyncExternalStore<Snapshot<In>>(\n client ? client.subscribe : noopSubscribe,\n client ? client.getSnapshot : idleSnapshot,\n idleSnapshot\n );\n\n const handlers = useLatest(options);\n useEffect(() => {\n if (!client || !enabled) return;\n const offs = [\n client.on('open', event => handlers.current.onOpen?.(event)),\n client.on('message', (message, event) => handlers.current.onMessage?.(message, event)),\n client.on('close', info => handlers.current.onClose?.(info)),\n client.on('error', error => handlers.current.onError?.(error))\n ];\n return () => offs.forEach(off => off());\n }, [client, enabled, handlers]);\n useHold(client, enabled);\n\n return {\n ...snapshot,\n send: client ? client.send : noopSend,\n close: client ? client.close : noop,\n reconnect: client ? client.reconnect : noop,\n client\n };\n}\n\n/**\n * Listen to a client's events without re-rendering the component. The listener always\n * sees the latest props and state. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));\n * ```\n */\nexport function useWebSocketEvent<In, K extends keyof EventMap<In>>(\n client: WebSocketClient<In, any> | null | undefined,\n type: K,\n listener: EventMap<In>[K]\n): void {\n const latest = useLatest(listener);\n useEffect(() => {\n if (!client) return;\n const off = client.on(type, ((...args: unknown[]) =>\n (latest.current as (...a: unknown[]) => void)(...args)) as EventMap<In>[K]);\n const release = client.acquire();\n return () => {\n off();\n release();\n };\n }, [client, type, latest]);\n}\n\n/**\n * The connection status of a client. Re-renders only when the status changes, not on\n * messages. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'\n * ```\n */\nexport function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status {\n const status = useSyncExternalStore(\n client ? client.subscribe : noopSubscribe,\n client ? () => client.getSnapshot().status : idleStatus,\n idleStatus\n );\n useHold(client ?? null, true);\n return status;\n}\n\nconst identity = <T>(value: T) => value;\n\n/**\n * Read state built from messages by `createWebSocketState`. Re-renders only when the\n * selected value changes (by `isEqual`, default `Object.is`), however often messages\n * arrive. Keeps the connection open while mounted.\n *\n * On the server, and during hydration, it reads from `initialState`.\n *\n * @example\n * ```tsx\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function useWebSocketState<S>(store: WebSocketState<S>): S;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T,\n isEqual?: (a: T, b: T) => boolean\n): T;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T = identity as (state: S) => T,\n isEqual: (a: T, b: T) => boolean = Object.is\n): T {\n const [getSelection, getServerSelection] = useMemo(() => {\n // Return the previous selection while the state is unchanged or the result is equal,\n // so inline selectors that build objects don't re-render (or loop) on every message.\n const memoized = (read: () => S) => {\n let memo: { state: S; selection: T } | undefined;\n return () => {\n const state = read();\n if (memo && Object.is(memo.state, state)) return memo.selection;\n const selection = selector(state);\n if (memo && isEqual(memo.selection, selection)) {\n memo = { state, selection: memo.selection };\n return memo.selection;\n }\n memo = { state, selection };\n return selection;\n };\n };\n return [memoized(store.getState), memoized(() => store.initialState)] as const;\n }, [store, selector, isEqual]);\n\n const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);\n useHold(store.client, true);\n return selection;\n}\n\n/**\n * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`\n * manager. Components holding the same topic share one subscription, and it is sent again\n * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.\n *\n * @example\n * ```tsx\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));\n * ```\n */\nexport function useWebSocketTopic(\n topics: WebSocketTopics | null | undefined,\n topic: string | readonly string[] | null | undefined\n): void {\n // A stable key, so a new array with the same topics doesn't resubscribe.\n const key = topic == null ? null : JSON.stringify(typeof topic === 'string' ? [topic] : topic);\n useEffect(() => {\n if (!topics || key === null) return;\n const releases = (JSON.parse(key) as string[]).map(t => topics.acquire(t));\n return () => releases.forEach(release => release());\n }, [topics, key]);\n}\n\n/**\n * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.\n * Re-renders when a measurement changes, about once per ping interval. Keeps the connection\n * open while mounted. On the server, and during hydration, both values are `undefined`.\n *\n * @example\n * ```tsx\n * const { rtt } = useWebSocketLatency(latency);\n * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;\n * ```\n */\nexport function useWebSocketLatency(latency: WebSocketLatency): Latency {\n const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);\n useHold(latency.client, true);\n return snapshot;\n}\n\n/**\n * Requests for `method` that a person answers, e.g. an agent asking to run a tool. While mounted,\n * it registers `method` on the responder without a handler, so new requests wait here instead of\n * getting a `NotFoundError` reply. Call `respond` or `reject` on one to answer it. Re-renders\n * when the list changes. Keeps the connection open while mounted. `null` holds nothing.\n *\n * Requests stay listed after unmount until they are answered, time out, are cancelled or lose\n * their connection, so a remounted dialog shows them again. On the server the list is empty.\n *\n * @example\n * ```tsx\n * const [request] = useAgentApproval(tools, 'approve');\n * if (!request) return null;\n * return (\n * <dialog open>\n * {request.params.summary}\n * <button onClick={() => request.respond({ approved: true })}>Approve</button>\n * <button onClick={() => request.respond({ approved: false })}>Deny</button>\n * </dialog>\n * );\n * ```\n */\nexport function useAgentApproval<P, R>(\n responder: WebSocketResponder<P, R> | null | undefined,\n method: string | null | undefined\n): readonly IncomingRequest<P, R>[] {\n const waiting = useSyncExternalStore(\n responder ? responder.subscribe : noopSubscribe,\n responder ? responder.getSnapshot : noRequests,\n noRequests\n );\n useEffect(() => {\n if (responder && method != null) return responder.onRequest(method);\n }, [responder, method]);\n useHold(responder?.client ?? null, method != null);\n return useMemo(() => {\n const mine = waiting.filter(request => request.method === method);\n return mine.length ? mine : NO_REQUESTS;\n }, [waiting, method]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"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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"}
@@ -0,0 +1,162 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/timeline.ts
3
+ const MAX_PENDING = 1e3;
4
+ /**
5
+ * Keep server snapshots on a timeline for multiplayer games: render what the server moves
6
+ * slightly in the past, interpolated between snapshots, and what your inputs move right away,
7
+ * predicted and corrected when the server confirms them.
8
+ *
9
+ * Read it once per frame (in `requestAnimationFrame` or React Three Fiber's `useFrame`): `at()`
10
+ * for everyone else, `predicted()` for you. It listens from creation, starts over on reconnect,
11
+ * and doesn't open the connection by itself.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { createWebSocketTimeline } from 'react-websocket/timeline';
16
+ *
17
+ * export const timeline = createWebSocketTimeline(game, {
18
+ * snapshot: m => (m.type === 'snapshot' ? { time: m.time, ack: m.ack, state: m.world } : undefined),
19
+ * interpolate: (from, to, t) => lerpWorld(from, to, t),
20
+ * latency,
21
+ * input: {
22
+ * send: (input: Move, seq) => ({ type: 'input', seq, ...input }),
23
+ * predict: (world, input) => move(world, me, input)
24
+ * }
25
+ * });
26
+ *
27
+ * // Every frame:
28
+ * draw(timeline.at(), timeline.predicted());
29
+ * // On a key press:
30
+ * timeline.input({ dx: 1, dy: 0 });
31
+ * ```
32
+ */
33
+ function createWebSocketTimeline(client, options) {
34
+ const { interpolate, latency, input: inputs } = options;
35
+ const delay = options.delay ?? 100;
36
+ const extrapolate = options.extrapolate ?? 0;
37
+ const keep = Math.max(2, options.buffer ?? 30);
38
+ const listeners = /* @__PURE__ */ new Set();
39
+ let buffer = [];
40
+ let arrival = -Infinity;
41
+ let rendered = -Infinity;
42
+ let stale = false;
43
+ let seq = 0;
44
+ let pending = [];
45
+ let prediction;
46
+ let folded = -1;
47
+ function notify() {
48
+ for (const listener of [...listeners]) listener();
49
+ }
50
+ function clear() {
51
+ buffer = [];
52
+ arrival = rendered = -Infinity;
53
+ stale = false;
54
+ }
55
+ function receive(snapshot) {
56
+ if (stale) clear();
57
+ const newest = buffer.at(-1);
58
+ if (newest && snapshot.time <= newest.snapshot.time) return;
59
+ buffer.push({
60
+ snapshot,
61
+ arrived: performance.now()
62
+ });
63
+ if (buffer.length > keep) buffer.shift();
64
+ arrival = -Infinity;
65
+ for (const entry of buffer) arrival = Math.max(arrival, entry.snapshot.time - entry.arrived);
66
+ const { ack } = snapshot;
67
+ if (ack != null) pending = pending.filter((entry) => entry.seq > ack);
68
+ folded = -1;
69
+ notify();
70
+ }
71
+ function renderTime() {
72
+ if (!buffer.length) return;
73
+ const measured = latency?.getSnapshot();
74
+ const edge = measured?.offset !== void 0 && measured.rtt !== void 0 ? latency.serverNow() - measured.rtt / 2 : performance.now() + arrival;
75
+ rendered = Math.max(rendered, edge - (typeof delay === "function" ? delay() : delay));
76
+ return rendered;
77
+ }
78
+ function at(time) {
79
+ const n = buffer.length;
80
+ if (!n) return;
81
+ const t = time ?? renderTime();
82
+ const last = buffer[n - 1].snapshot;
83
+ if (t >= last.time) {
84
+ const previous = buffer[n - 2]?.snapshot;
85
+ if (!previous || !interpolate || !extrapolate || t === last.time) return last.state;
86
+ const capped = Math.min(t, last.time + extrapolate);
87
+ return interpolate(previous.state, last.state, (capped - previous.time) / (last.time - previous.time));
88
+ }
89
+ let i = n - 2;
90
+ while (i >= 0 && buffer[i].snapshot.time > t) i--;
91
+ if (i < 0) return buffer[0].snapshot.state;
92
+ const from = buffer[i].snapshot;
93
+ const to = buffer[i + 1].snapshot;
94
+ return !interpolate || t === from.time ? from.state : interpolate(from.state, to.state, (t - from.time) / (to.time - from.time));
95
+ }
96
+ function predicted() {
97
+ const newest = buffer.at(-1);
98
+ if (!newest) return;
99
+ if (folded < 0) {
100
+ prediction = newest.snapshot.state;
101
+ folded = 0;
102
+ }
103
+ while (folded < pending.length) {
104
+ prediction = inputs.predict(prediction, pending[folded].input);
105
+ folded++;
106
+ }
107
+ return prediction;
108
+ }
109
+ const offs = [client.on("open", () => {
110
+ pending = [];
111
+ folded = -1;
112
+ stale = true;
113
+ }), client.on("message", (message) => {
114
+ const snapshot = options.snapshot(message);
115
+ if (snapshot != null) receive(snapshot);
116
+ })];
117
+ return {
118
+ client,
119
+ at,
120
+ predicted,
121
+ input(value) {
122
+ const next = seq + 1;
123
+ const message = inputs.send(value, next);
124
+ if (!client.send(message, {
125
+ volatile: true,
126
+ maxBuffered: inputs.maxBuffered
127
+ })) return;
128
+ seq = next;
129
+ pending.push({
130
+ seq,
131
+ input: value
132
+ });
133
+ if (pending.length > MAX_PENDING) {
134
+ pending.shift();
135
+ folded = -1;
136
+ }
137
+ return seq;
138
+ },
139
+ latest: () => buffer.at(-1)?.snapshot,
140
+ renderTime,
141
+ pending: () => pending.length,
142
+ subscribe(listener) {
143
+ listeners.add(listener);
144
+ return () => {
145
+ listeners.delete(listener);
146
+ };
147
+ },
148
+ reset() {
149
+ clear();
150
+ pending = [];
151
+ folded = -1;
152
+ notify();
153
+ },
154
+ dispose() {
155
+ offs.forEach((off) => off());
156
+ }
157
+ };
158
+ }
159
+ //#endregion
160
+ exports.createWebSocketTimeline = createWebSocketTimeline;
161
+
162
+ //# sourceMappingURL=timeline.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.cjs","names":[],"sources":["../src/timeline.ts"],"sourcesContent":["// Types only: the built entry has no imports, and its declarations point at `core`'s.\nimport type { WebSocketClient, WebSocketLatency } from './core';\n\n/** A server snapshot, as returned by the `snapshot` option. */\nexport interface TimelineSnapshot<S> {\n /** Server time the state is for, in ms. Must increase from one snapshot to the next. */\n readonly time: number;\n /** The authoritative state at `time`. */\n readonly state: S;\n /**\n * `seq` of the last input from this client that the server applied before taking the\n * snapshot. Inputs up to it are confirmed and no longer predicted. Omit it to keep the\n * previous one.\n */\n readonly ack?: number;\n}\n\n/** Client-side prediction for your own inputs. See {@link WebSocketTimelineOptions.input}. */\nexport interface TimelineInputOptions<S, I, Out> {\n /**\n * Build the message for an input, tagged with its `seq`. Annotate `input`: that's what\n * `timeline.input()` accepts. Sent with `volatile`, so inputs are never replayed late.\n */\n send: (input: I, seq: number) => Out;\n /**\n * Apply one input to a state, exactly as the server does. Must be pure: return a new state\n * instead of changing `state`, which is a snapshot from the server.\n */\n predict: (state: S, input: I) => S;\n /**\n * Drop an input instead of sending it while more than this many bytes are already waiting in\n * the browser's send buffer (see `SendOptions.maxBuffered`). For input sent every tick on a\n * slow link. A dropped input isn't predicted.\n */\n maxBuffered?: number;\n}\n\n/** Options for {@link createWebSocketTimeline}. */\nexport interface WebSocketTimelineOptions<S, I, In, Out> {\n /** Recognize a snapshot. Return `undefined` (or `null`) for any other message. */\n snapshot: (message: In) => TimelineSnapshot<S> | null | undefined;\n /**\n * Blend two states. `t` runs from 0 (`from`) to 1 (`to`), and above 1 only when\n * extrapolating. Without it, `at()` steps from one snapshot to the next.\n */\n interpolate?: (from: S, to: S, t: number) => S;\n /**\n * How far behind the newest snapshot to render, in ms: enough to cover the snapshot\n * interval plus jitter, so there are usually two snapshots to blend between. A function is\n * read on every call, for a delay that adapts.\n * @default 100\n */\n delay?: number | (() => number);\n /**\n * How far past the newest snapshot `at()` may extrapolate when the next one is late, in ms.\n * Needs `interpolate`. `0` holds the newest state instead.\n * @default 0\n */\n extrapolate?: number;\n /**\n * How many snapshots to keep (at least 2).\n * @default 30\n */\n buffer?: number;\n /**\n * Take the server clock from `createWebSocketLatency` on the same client. Its pongs must use\n * the same clock as snapshot `time`. Without it, or before the first pong with a time, the\n * server clock is estimated from when snapshots arrive.\n */\n latency?: Pick<WebSocketLatency, 'getSnapshot' | 'serverNow'>;\n /** Send inputs tagged with a sequence number, and predict their effect until the server confirms them. */\n input?: TimelineInputOptions<S, I, Out>;\n}\n\n/** Server snapshots on a timeline, with your own inputs predicted on top. Create with {@link createWebSocketTimeline}. */\nexport interface WebSocketTimeline<S, I> {\n /** The client whose snapshots this timeline keeps. */\n readonly client: WebSocketClient<any, any>;\n /**\n * The state at `time` (by default `renderTime()`), interpolated between the snapshots around\n * it. For what the server moves, such as other players. `undefined` before any snapshot.\n */\n at(time?: number): S | undefined;\n /**\n * The newest snapshot with your unconfirmed inputs replayed on top. For what your inputs\n * move, such as your own player. Cached, so it's cheap to call every frame.\n */\n predicted(): S | undefined;\n /**\n * Send an input and predict it. Returns its `seq`, or `undefined` if it couldn't be sent\n * (the socket isn't open, or `maxBuffered` dropped it), in which case it isn't predicted.\n */\n input(input: I): number | undefined;\n /** The newest snapshot, as `snapshot` returned it. */\n latest(): TimelineSnapshot<S> | undefined;\n /**\n * The server time `at()` renders by default: the newest snapshot's time, give or take, minus\n * `delay`. It never goes backward until the timeline starts over. `undefined` before any snapshot.\n */\n renderTime(): number | undefined;\n /** How many inputs were sent but not confirmed yet. */\n pending(): number;\n /**\n * Called after each new snapshot and on `reset()`. Compatible with `useSyncExternalStore` for\n * `latest` and `pending`, but not for `at`, `predicted` or `renderTime`, which change with time.\n */\n subscribe(listener: () => void): () => void;\n /** Forget all snapshots and pending inputs now. `seq` keeps counting. */\n reset(): void;\n /** Stop listening. Only needed for timelines created dynamically. */\n dispose(): void;\n}\n\n// Unconfirmed inputs kept at most. A server that stops confirming would otherwise make every\n// snapshot replay a growing list.\nconst MAX_PENDING = 1000;\n\n/**\n * Keep server snapshots on a timeline for multiplayer games: render what the server moves\n * slightly in the past, interpolated between snapshots, and what your inputs move right away,\n * predicted and corrected when the server confirms them.\n *\n * Read it once per frame (in `requestAnimationFrame` or React Three Fiber's `useFrame`): `at()`\n * for everyone else, `predicted()` for you. It listens from creation, starts over on reconnect,\n * and doesn't open the connection by itself.\n *\n * @example\n * ```ts\n * import { createWebSocketTimeline } from 'react-websocket/timeline';\n *\n * export const timeline = createWebSocketTimeline(game, {\n * snapshot: m => (m.type === 'snapshot' ? { time: m.time, ack: m.ack, state: m.world } : undefined),\n * interpolate: (from, to, t) => lerpWorld(from, to, t),\n * latency,\n * input: {\n * send: (input: Move, seq) => ({ type: 'input', seq, ...input }),\n * predict: (world, input) => move(world, me, input)\n * }\n * });\n *\n * // Every frame:\n * draw(timeline.at(), timeline.predicted());\n * // On a key press:\n * timeline.input({ dx: 1, dy: 0 });\n * ```\n */\n// `const` + `NoInfer`: take `In` and `Out` from the client only. `S` comes from `snapshot`, and\n// `I` from how `input.send` annotates its parameter (`never` without `input`).\nexport function createWebSocketTimeline<In, const Out, S, I = never>(\n client: WebSocketClient<In, Out>,\n options: WebSocketTimelineOptions<S, I, NoInfer<In>, NoInfer<Out>>\n): WebSocketTimeline<S, I> {\n const { interpolate, latency, input: inputs } = options;\n const delay = options.delay ?? 100;\n const extrapolate = options.extrapolate ?? 0;\n const keep = Math.max(2, options.buffer ?? 30);\n const listeners = new Set<() => void>();\n\n // Oldest first, `time` strictly increasing. `arrived` is `performance.now()` on receipt.\n let buffer: { snapshot: TimelineSnapshot<S>; arrived: number }[] = [];\n // Server time minus `performance.now()`, from the least delayed snapshot in the buffer.\n let arrival = -Infinity;\n // The last render time handed out, so that it never goes backward.\n let rendered = -Infinity;\n // A new connection opened: its first snapshot replaces the buffer.\n let stale = false;\n let seq = 0;\n let pending: { seq: number; input: I }[] = [];\n let prediction: S | undefined;\n // How many pending inputs `prediction` includes. `-1`: start again from the newest snapshot.\n let folded = -1;\n\n function notify() {\n for (const listener of [...listeners]) listener();\n }\n\n function clear() {\n buffer = [];\n arrival = rendered = -Infinity;\n stale = false;\n }\n\n function receive(snapshot: TimelineSnapshot<S>) {\n if (stale) clear();\n const newest = buffer.at(-1);\n // A WebSocket keeps order, so this is a duplicate or a server clock that stepped back.\n if (newest && snapshot.time <= newest.snapshot.time) return;\n buffer.push({ snapshot, arrived: performance.now() });\n if (buffer.length > keep) buffer.shift();\n arrival = -Infinity;\n for (const entry of buffer) arrival = Math.max(arrival, entry.snapshot.time - entry.arrived);\n const { ack } = snapshot;\n if (ack != null) pending = pending.filter(entry => entry.seq > ack);\n folded = -1;\n notify();\n }\n\n function renderTime() {\n if (!buffer.length) return;\n const measured = latency?.getSnapshot();\n // The server time of a snapshot arriving now: half a round trip behind the server's clock.\n const edge =\n measured?.offset !== undefined && measured.rtt !== undefined\n ? latency!.serverNow() - measured.rtt / 2\n : performance.now() + arrival;\n rendered = Math.max(rendered, edge - (typeof delay === 'function' ? delay() : delay));\n return rendered;\n }\n\n function at(time?: number): S | undefined {\n const n = buffer.length;\n if (!n) return;\n const t = time ?? renderTime()!;\n const last = buffer[n - 1]!.snapshot;\n if (t >= last.time) {\n const previous = buffer[n - 2]?.snapshot;\n if (!previous || !interpolate || !extrapolate || t === last.time) return last.state;\n const capped = Math.min(t, last.time + extrapolate);\n return interpolate(\n previous.state,\n last.state,\n (capped - previous.time) / (last.time - previous.time)\n );\n }\n // Reads are usually near the newest snapshot, so search from the end.\n let i = n - 2;\n while (i >= 0 && buffer[i]!.snapshot.time > t) i--;\n if (i < 0) return buffer[0]!.snapshot.state;\n const from = buffer[i]!.snapshot;\n const to = buffer[i + 1]!.snapshot;\n return !interpolate || t === from.time\n ? from.state\n : interpolate(from.state, to.state, (t - from.time) / (to.time - from.time));\n }\n\n function predicted(): S | undefined {\n const newest = buffer.at(-1);\n if (!newest) return;\n if (folded < 0) {\n prediction = newest.snapshot.state;\n folded = 0;\n }\n // Usually zero or one new input per frame; all of them once after each snapshot.\n while (folded < pending.length) {\n prediction = inputs!.predict(prediction!, pending[folded]!.input);\n folded++; // after `predict`, so one that throws is retried rather than skipped\n }\n return prediction;\n }\n\n const offs = [\n // Inputs on the old connection will never be confirmed. Keep showing the old snapshots\n // until the new connection sends its first one.\n client.on('open', () => {\n pending = [];\n folded = -1;\n stale = true;\n }),\n // A `snapshot` that throws leaves the timeline unchanged; the client re-throws it asynchronously.\n client.on('message', message => {\n const snapshot = options.snapshot(message);\n if (snapshot != null) receive(snapshot);\n })\n ];\n\n return {\n client,\n at,\n predicted,\n input(value) {\n const next = seq + 1;\n // Recorded only once it's sent, so a dropped input never shows as a move the server didn't make.\n const message = inputs!.send(value, next);\n if (!client.send(message, { volatile: true, maxBuffered: inputs!.maxBuffered })) return;\n seq = next;\n pending.push({ seq, input: value });\n if (pending.length > MAX_PENDING) {\n pending.shift();\n folded = -1;\n }\n return seq;\n },\n latest: () => buffer.at(-1)?.snapshot,\n renderTime,\n pending: () => pending.length,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n reset() {\n clear();\n pending = [];\n folded = -1;\n notify();\n },\n dispose() {\n offs.forEach(off => off());\n }\n };\n}\n"],"mappings":"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"}