react-websocket 3.2.0 → 3.5.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.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+
2
+ //#region src/types.d.ts
1
3
  /**
2
4
  * Where to connect: a URL, or a function that returns one (sync or async).
3
5
  *
@@ -24,105 +26,139 @@ type UrlSource = string | (() => string | Promise<string>);
24
26
  type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
25
27
  /** The client's state as seen by React. A new object is created on every change. */
26
28
  interface Snapshot<In> {
27
- /** Connection state. */
28
- readonly status: Status;
29
- /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
30
- readonly lastMessage: In | undefined;
31
- /** Consecutive failed attempts since the last successful open. `0` while open. */
32
- readonly attempt: number;
29
+ /** Connection state. */
30
+ readonly status: Status;
31
+ /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
32
+ readonly lastMessage: In | undefined;
33
+ /** Consecutive failed attempts since the last successful open. `0` while open. */
34
+ readonly attempt: number;
33
35
  }
34
36
  /** Details of a closed connection, from the browser's `CloseEvent`. */
35
37
  interface CloseInfo {
36
- /** Close code: `1000` is a normal close, `1006` an abnormal one (e.g. network loss), `4000`–`4999` are yours. */
37
- readonly code: number;
38
- /** Close reason sent by the side that closed the connection. Often empty. */
39
- readonly reason: string;
40
- /** Whether the closing handshake completed. */
41
- readonly wasClean: boolean;
38
+ /** Close code: `1000` is a normal close, `1006` an abnormal one (e.g. network loss), `4000`–`4999` are yours. */
39
+ readonly code: number;
40
+ /** Close reason sent by the side that closed the connection. Often empty. */
41
+ readonly reason: string;
42
+ /** Whether the closing handshake completed. */
43
+ readonly wasClean: boolean;
42
44
  }
43
45
  /** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */
44
46
  type SendData = Parameters<WebSocket['send']>[0];
45
47
  /** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */
46
48
  interface ReconnectOptions {
47
- /**
48
- * Maximum consecutive reconnect attempts. When the last one fails too, the client
49
- * gives up: the status becomes `closed` and `ready` rejects. `0` never retries.
50
- * @default Infinity
51
- */
52
- maxAttempts?: number;
53
- /**
54
- * Delay before the first retry, in ms.
55
- * @default 1000
56
- */
57
- minDelay?: number;
58
- /**
59
- * Upper bound for the delay, in ms.
60
- * @default 30000
61
- */
62
- maxDelay?: number;
63
- /**
64
- * Backoff multiplier applied per attempt.
65
- * @default 2
66
- */
67
- factor?: number;
68
- /**
69
- * Randomly shave up to this fraction (0–1) off each delay, so that many clients
70
- * dropped at once don't all reconnect at the same moment.
71
- * @default 0.5
72
- */
73
- jitter?: number;
74
- /**
75
- * Called on every unexpected close. Return `false` to stop reconnecting.
76
- *
77
- * @example
78
- * ```ts
79
- * // Don't retry when the server rejects our credentials.
80
- * shouldReconnect: close => close.code !== 4001
81
- * ```
82
- * @default () => true
83
- */
84
- shouldReconnect?: (close: CloseInfo) => boolean;
49
+ /**
50
+ * Maximum consecutive reconnect attempts. When the last one fails too, the client
51
+ * gives up: the status becomes `closed` and `ready` rejects. `0` never retries.
52
+ * @default Infinity
53
+ */
54
+ maxAttempts?: number;
55
+ /**
56
+ * Delay before the first retry, in ms.
57
+ * @default 1000
58
+ */
59
+ minDelay?: number;
60
+ /**
61
+ * Upper bound for the delay, in ms.
62
+ * @default 30000
63
+ */
64
+ maxDelay?: number;
65
+ /**
66
+ * Backoff multiplier applied per attempt.
67
+ * @default 2
68
+ */
69
+ factor?: number;
70
+ /**
71
+ * Randomly shave up to this fraction (0–1) off each delay, so that many clients
72
+ * dropped at once don't all reconnect at the same moment.
73
+ * @default 0.5
74
+ */
75
+ jitter?: number;
76
+ /**
77
+ * Called on every unexpected close. Return `false` to stop reconnecting.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * // Don't retry when the server rejects our credentials.
82
+ * shouldReconnect: close => close.code !== 4001
83
+ * ```
84
+ * @default () => true
85
+ */
86
+ shouldReconnect?: (close: CloseInfo) => boolean;
85
87
  }
86
88
  /** Application-level keep-alive. Useful behind proxies that drop idle connections. */
87
89
  interface HeartbeatOptions {
88
- /** Send `message` every `interval` ms while open. */
89
- interval: number;
90
- /**
91
- * Sent as-is (it is not passed through `serialize` or `json`).
92
- * @default 'ping'
93
- */
94
- message?: string;
95
- /** Incoming data equal to this string is swallowed instead of being delivered as a message. */
96
- pong?: string;
97
- /**
98
- * If nothing at all arrives within this many ms after a ping, the connection is
99
- * treated as dead: it closes with code `4000` and reconnects.
100
- */
101
- timeout?: number;
90
+ /** Send `message` every `interval` ms while open. */
91
+ interval: number;
92
+ /**
93
+ * Sent as-is (it is not passed through `serialize` or `json`).
94
+ * @default 'ping'
95
+ */
96
+ message?: string;
97
+ /** Incoming data equal to this string is swallowed instead of being delivered as a message. */
98
+ pong?: string;
99
+ /**
100
+ * If nothing at all arrives within this many ms after a ping, the connection is
101
+ * treated as dead: it closes with code `4000` and reconnects.
102
+ */
103
+ timeout?: number;
102
104
  }
103
105
  /** Per-message options for `send()`. */
104
106
  interface SendOptions {
105
- /**
106
- * Drop the message instead of queueing it when the socket isn't open. For data that
107
- * is stale by the time a reconnect finishes, such as player input or cursor positions.
108
- * @default false
109
- */
110
- volatile?: boolean;
111
- /**
112
- * Keep only the newest queued message with this key. Sending again with the same key
113
- * while disconnected replaces the earlier one, which moves to the back of the queue.
114
- * For state where only the latest value matters, such as "my position is now (x, y)".
115
- */
116
- key?: string;
107
+ /**
108
+ * Drop the message instead of queueing it when the socket isn't open. For data that
109
+ * is stale by the time a reconnect finishes, such as player input or cursor positions.
110
+ * @default false
111
+ */
112
+ volatile?: boolean;
113
+ /**
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
116
+ * (or of the `priority` messages, for a priority one).
117
+ * For state where only the latest value matters, such as "my position is now (x, y)".
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;
146
+ /**
147
+ * Called if the message is dropped from the queue without being sent: replaced by a
148
+ * newer message with the same `key`, or evicted by `queue.limit`. Not called when the
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`.
151
+ */
152
+ onDrop?: () => void;
117
153
  }
118
154
  /** Bounds for the send queue. */
119
155
  interface QueueOptions {
120
- /**
121
- * Maximum number of messages held while not connected. When full, the oldest
122
- * message is dropped.
123
- * @default Infinity
124
- */
125
- limit?: number;
156
+ /**
157
+ * Maximum number of messages held while not connected. When full, the oldest
158
+ * message is dropped, skipping `priority` messages unless nothing else is queued.
159
+ * @default Infinity
160
+ */
161
+ limit?: number;
126
162
  }
127
163
  /**
128
164
  * The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented
@@ -130,24 +166,24 @@ interface QueueOptions {
130
166
  * import this; pass your schema object directly.
131
167
  */
132
168
  interface StandardSchemaV1<Input = unknown, Output = Input> {
133
- readonly '~standard': {
134
- readonly version: 1;
135
- readonly vendor: string;
136
- readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
137
- readonly types?: {
138
- readonly input: Input;
139
- readonly output: Output;
140
- } | undefined;
141
- };
169
+ readonly '~standard': {
170
+ readonly version: 1;
171
+ readonly vendor: string;
172
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
173
+ readonly types?: {
174
+ readonly input: Input;
175
+ readonly output: Output;
176
+ } | undefined;
177
+ };
142
178
  }
143
179
  /** The result of a Standard Schema `validate` call. */
144
180
  type StandardSchemaResult<Output> = {
145
- readonly value: Output;
146
- readonly issues?: undefined;
181
+ readonly value: Output;
182
+ readonly issues?: undefined;
147
183
  } | {
148
- readonly issues: ReadonlyArray<{
149
- readonly message: string;
150
- }>;
184
+ readonly issues: ReadonlyArray<{
185
+ readonly message: string;
186
+ }>;
151
187
  };
152
188
  /**
153
189
  * Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.
@@ -156,66 +192,66 @@ type StandardSchemaResult<Output> = {
156
192
  * @typeParam Out - Type accepted by `send()`.
157
193
  */
158
194
  interface WebSocketOptions<In = unknown, Out = unknown> {
159
- /** Sub-protocol(s) passed to the `WebSocket` constructor. */
160
- protocols?: string | string[];
161
- /**
162
- * How binary messages are exposed.
163
- * @default 'blob'
164
- */
165
- binaryType?: BinaryType;
166
- /**
167
- * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail
168
- * to parse are reported to `onError` as a {@link MessageError} and not delivered.
169
- * @default false
170
- */
171
- json?: boolean;
172
- /**
173
- * Custom decoder for incoming `event.data`. Takes precedence over `json`.
174
- * Throwing reports a {@link MessageError} and drops the message.
175
- */
176
- parse?: (data: unknown) => unknown;
177
- /** Custom encoder for outgoing messages. Takes precedence over `json`. */
178
- serialize?: (message: Out) => SendData;
179
- /**
180
- * Validate incoming messages (after parsing) with any Standard Schema library.
181
- * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.
182
- * The message type `In` is inferred from it.
183
- *
184
- * @example
185
- * ```ts
186
- * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });
187
- * ```
188
- */
189
- schema?: StandardSchemaV1<unknown, In>;
190
- /**
191
- * Reconnect with exponential backoff after an unexpected close. `false` disables it.
192
- * @default true
193
- */
194
- reconnect?: boolean | ReconnectOptions;
195
- /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */
196
- heartbeat?: HeartbeatOptions;
197
- /**
198
- * Buffer `send()` calls made while not connected and send them, in order, on open
199
- * (right after `open` listeners run).
200
- * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
201
- * The queue is cleared by `close()` or when the client gives up.
202
- * @default true
203
- */
204
- queue?: boolean | QueueOptions;
205
- /**
206
- * Receives debug messages about connection attempts, retries and dropped messages.
207
- * @example
208
- * ```ts
209
- * logger: console.debug
210
- * ```
211
- */
212
- logger?: (message: string, ...details: unknown[]) => void;
213
- /**
214
- * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.
215
- * When omitted, the global `WebSocket` is used in browsers. During a server render
216
- * (no `window`), nothing connects unless an implementation is passed.
217
- */
218
- WebSocket?: typeof WebSocket;
195
+ /** Sub-protocol(s) passed to the `WebSocket` constructor. */
196
+ protocols?: string | string[];
197
+ /**
198
+ * How binary messages are exposed.
199
+ * @default 'blob'
200
+ */
201
+ binaryType?: BinaryType;
202
+ /**
203
+ * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail
204
+ * to parse are reported to `onError` as a {@link MessageError} and not delivered.
205
+ * @default false
206
+ */
207
+ json?: boolean;
208
+ /**
209
+ * Custom decoder for incoming `event.data`. Takes precedence over `json`.
210
+ * Throwing reports a {@link MessageError} and drops the message.
211
+ */
212
+ parse?: (data: unknown) => unknown;
213
+ /** Custom encoder for outgoing messages. Takes precedence over `json`. */
214
+ serialize?: (message: Out) => SendData;
215
+ /**
216
+ * Validate incoming messages (after parsing) with any Standard Schema library.
217
+ * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.
218
+ * The message type `In` is inferred from it.
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });
223
+ * ```
224
+ */
225
+ schema?: StandardSchemaV1<unknown, In>;
226
+ /**
227
+ * Reconnect with exponential backoff after an unexpected close. `false` disables it.
228
+ * @default true
229
+ */
230
+ reconnect?: boolean | ReconnectOptions;
231
+ /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */
232
+ heartbeat?: HeartbeatOptions;
233
+ /**
234
+ * Buffer `send()` calls made while not connected and send them, in order, on open
235
+ * (right after `open` listeners run).
236
+ * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
237
+ * The queue is cleared by `close()` or when the client gives up.
238
+ * @default true
239
+ */
240
+ queue?: boolean | QueueOptions;
241
+ /**
242
+ * Receives debug messages about connection attempts, retries and dropped messages.
243
+ * @example
244
+ * ```ts
245
+ * logger: console.debug
246
+ * ```
247
+ */
248
+ logger?: (message: string, ...details: unknown[]) => void;
249
+ /**
250
+ * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.
251
+ * When omitted, the global `WebSocket` is used in browsers. During a server render
252
+ * (no `window`), nothing connects unless an implementation is passed.
253
+ */
254
+ WebSocket?: typeof WebSocket;
219
255
  }
220
256
  /**
221
257
  * Reported via the `error` event (and `onError`) when an incoming message can't be
@@ -228,36 +264,36 @@ interface WebSocketOptions<In = unknown, Out = unknown> {
228
264
  * }
229
265
  * ```
230
266
  */
231
- declare class MessageError extends Error {
232
- /** The raw `event.data` that failed. */
233
- readonly data: unknown;
234
- /** Schema validation issues, when the message failed `schema`. */
235
- readonly issues: ReadonlyArray<{
236
- readonly message: string;
237
- }> | undefined;
238
- constructor(message: string, data: unknown, options?: {
239
- cause?: unknown;
240
- issues?: ReadonlyArray<{
241
- readonly message: string;
242
- }>;
243
- });
267
+ export declare class MessageError extends Error {
268
+ /** The raw `event.data` that failed. */
269
+ readonly data: unknown;
270
+ /** Schema validation issues, when the message failed `schema`. */
271
+ readonly issues: ReadonlyArray<{
272
+ readonly message: string;
273
+ }> | undefined;
274
+ constructor(message: string, data: unknown, options?: {
275
+ cause?: unknown;
276
+ issues?: ReadonlyArray<{
277
+ readonly message: string;
278
+ }>;
279
+ });
244
280
  }
245
281
  /** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */
246
282
  interface EventMap<In> {
247
- /**
248
- * The socket opened (initially or after a reconnect). Listeners run before queued
249
- * messages are sent, so a handshake sent from here goes out first.
250
- */
251
- open: (event: Event) => void;
252
- /** A message arrived and passed `parse` and `schema`. */
253
- message: (message: In, event: MessageEvent) => void;
254
- /** A socket closed, whether it will be retried or not. */
255
- close: (close: CloseInfo) => void;
256
- /**
257
- * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,
258
- * or the `Error` thrown by the `WebSocket` constructor or a `url` function.
259
- */
260
- error: (error: Event | MessageError | Error) => void;
283
+ /**
284
+ * The socket opened (initially or after a reconnect). Listeners run before queued
285
+ * messages are sent, so a handshake sent from here goes out first.
286
+ */
287
+ open: (event: Event) => void;
288
+ /** A message arrived and passed `parse` and `schema`. */
289
+ message: (message: In, event: MessageEvent) => void;
290
+ /** A socket closed, whether it will be retried or not. */
291
+ close: (close: CloseInfo) => void;
292
+ /**
293
+ * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,
294
+ * or the `Error` thrown by the `WebSocket` constructor or a `url` function.
295
+ */
296
+ error: (error: Event | MessageError | Error) => void;
261
297
  }
262
298
  /**
263
299
  * A WebSocket connection that reconnects, queues, decodes and can be shared.
@@ -271,71 +307,74 @@ interface EventMap<In> {
271
307
  * @typeParam Out - Type accepted by `send()`.
272
308
  */
273
309
  interface WebSocketClient<In = unknown, Out = unknown> {
274
- /** The URL (or URL function) the client was created with. */
275
- readonly url: UrlSource;
276
- /**
277
- * A promise that resolves once the socket has opened. Reading it starts an idle client.
278
- *
279
- * It stays resolved through automatic reconnects, so `use(client.ready)` shows a
280
- * Suspense fallback only for the first connection, never on a network blip; use
281
- * `status` to show reconnects. It rejects when the client gives up reconnecting, when
282
- * `close()` is called, and during a server render. After that, `connect()` or
283
- * `reconnect()` make it a new pending promise.
284
- *
285
- * It is safe to pass to React 19's `use()`: the same promise is returned until its state
286
- * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is
287
- * already settled.
288
- */
289
- readonly ready: Promise<void>;
290
- /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */
291
- readonly socket: WebSocket | null;
292
- /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */
293
- subscribe(listener: () => void): () => void;
294
- /** The current {@link Snapshot}. Returns the same object until something changes. */
295
- getSnapshot(): Snapshot<In>;
296
- /**
297
- * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown
298
- * asynchronously, so one failing listener doesn't break the others.
299
- */
300
- on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
301
- /**
302
- * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
303
- * open, the message is queued and sent on open (see the `queue` option), unless
304
- * `options.volatile` is set.
305
- * @returns `true` if it was sent immediately, `false` if it was queued or dropped.
306
- *
307
- * @example
308
- * ```ts
309
- * game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
310
- * game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
311
- * ```
312
- */
313
- send(message: Out, options?: SendOptions): boolean;
314
- /**
315
- * Start connecting, and keep the connection open even when no hook is using the
316
- * client, until `close()` is called. Does nothing if already connecting or open.
317
- */
318
- connect(): void;
319
- /**
320
- * Close the connection and stop reconnecting. The status becomes `closed` and the
321
- * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`
322
- * to start again.
323
- * @param code - Close code. @default 1000
324
- */
325
- close(code?: number, reason?: string): void;
326
- /**
327
- * Drop the current socket (if any) and connect again immediately, resetting the
328
- * backoff. Also restarts a `closed` client.
329
- */
330
- reconnect(): void;
331
- /**
332
- * Hold the connection open until the returned function is called. This is what the
333
- * hooks use; call it yourself to integrate with non-React code.
334
- * @returns A release function (safe to call more than once).
335
- */
336
- acquire(): () => void;
310
+ /** The URL (or URL function) the client was created with. */
311
+ readonly url: UrlSource;
312
+ /**
313
+ * A promise that resolves once the socket has opened. Reading it starts an idle client.
314
+ *
315
+ * It stays resolved through automatic reconnects, so `use(client.ready)` shows a
316
+ * Suspense fallback only for the first connection, never on a network blip; use
317
+ * `status` to show reconnects. It rejects when the client gives up reconnecting, when
318
+ * `close()` is called, and during a server render. After that, `connect()` or
319
+ * `reconnect()` make it a new pending promise.
320
+ *
321
+ * It is safe to pass to React 19's `use()`: the same promise is returned until its state
322
+ * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is
323
+ * already settled.
324
+ */
325
+ readonly ready: Promise<void>;
326
+ /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */
327
+ readonly socket: WebSocket | null;
328
+ /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */
329
+ subscribe(listener: () => void): () => void;
330
+ /** The current {@link Snapshot}. Returns the same object until something changes. */
331
+ getSnapshot(): Snapshot<In>;
332
+ /**
333
+ * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown
334
+ * asynchronously, so one failing listener doesn't break the others.
335
+ */
336
+ on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
337
+ /**
338
+ * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
339
+ * open, the message is queued and sent on open (see the `queue` option), unless
340
+ * `options.volatile` is set.
341
+ * @returns `true` if it was sent immediately, `false` if it was queued or dropped
342
+ * (`volatile` while not open, or `maxBuffered` while open).
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
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
349
+ * ```
350
+ */
351
+ send(message: Out, options?: SendOptions): boolean;
352
+ /**
353
+ * Start connecting, and keep the connection open even when no hook is using the
354
+ * client, until `close()` is called. Does nothing if already connecting or open.
355
+ */
356
+ connect(): void;
357
+ /**
358
+ * Close the connection and stop reconnecting. The status becomes `closed` and the
359
+ * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`
360
+ * to start again.
361
+ * @param code - Close code. @default 1000
362
+ */
363
+ close(code?: number, reason?: string): void;
364
+ /**
365
+ * Drop the current socket (if any) and connect again immediately, resetting the
366
+ * backoff. Also restarts a `closed` client.
367
+ */
368
+ reconnect(): void;
369
+ /**
370
+ * Hold the connection open until the returned function is called. This is what the
371
+ * hooks use; call it yourself to integrate with non-React code.
372
+ * @returns A release function (safe to call more than once).
373
+ */
374
+ acquire(): () => void;
337
375
  }
338
-
376
+ //#endregion
377
+ //#region src/client.d.ts
339
378
  /**
340
379
  * Create a WebSocket client, usually at module level, to share across components.
341
380
  *
@@ -355,57 +394,58 @@ interface WebSocketClient<In = unknown, Out = unknown> {
355
394
  * export const chat = createWebSocket<ChatIn, ChatOut>('wss://api.example.com/chat', { json: true });
356
395
  * ```
357
396
  */
358
- declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
359
-
397
+ export declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
398
+ //#endregion
399
+ //#region src/latency.d.ts
360
400
  /** Options for {@link createWebSocketLatency}: how pings and pongs look on the wire. */
361
401
  interface WebSocketLatencyOptions<In, Out> {
362
- /** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */
363
- ping: (id: number) => Out;
364
- /**
365
- * Recognize a pong: return the `id` of the ping it answers, and the server's clock
366
- * (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)
367
- * for any other message. Without `time`, only the round-trip time is measured.
368
- */
369
- pong: (message: In) => {
370
- id: number | string;
371
- time?: number;
372
- } | null | undefined;
373
- /**
374
- * How often to ping while open, in ms. The first ping goes out as soon as the socket opens.
375
- * @default 2000
376
- */
377
- interval?: number;
378
- /**
379
- * How many recent measurements to keep. `rtt` is their median, and `offset` comes from
380
- * the one with the lowest round trip, the most accurate estimate.
381
- * @default 5
382
- */
383
- samples?: number;
402
+ /** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */
403
+ ping: (id: number) => Out;
404
+ /**
405
+ * Recognize a pong: return the `id` of the ping it answers, and the server's clock
406
+ * (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)
407
+ * for any other message. Without `time`, only the round-trip time is measured.
408
+ */
409
+ pong: (message: In) => {
410
+ id: number | string;
411
+ time?: number;
412
+ } | null | undefined;
413
+ /**
414
+ * How often to ping while open, in ms. The first ping goes out as soon as the socket opens.
415
+ * @default 2000
416
+ */
417
+ interval?: number;
418
+ /**
419
+ * How many recent measurements to keep. `rtt` is their median, and `offset` comes from
420
+ * the one with the lowest round trip, the most accurate estimate.
421
+ * @default 5
422
+ */
423
+ samples?: number;
384
424
  }
385
425
  /** Latest latency measurements. A new object is created on every change. */
386
426
  interface Latency {
387
- /** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
388
- readonly rtt: number | undefined;
389
- /**
390
- * Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
391
- * `undefined` until a pong carries `time`.
392
- */
393
- readonly offset: number | undefined;
427
+ /** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
428
+ readonly rtt: number | undefined;
429
+ /**
430
+ * Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
431
+ * `undefined` until a pong carries `time`.
432
+ */
433
+ readonly offset: number | undefined;
394
434
  }
395
435
  /** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */
396
436
  interface WebSocketLatency {
397
- /** The client being measured. Hooks reading the latency keep it connected. */
398
- readonly client: WebSocketClient<any, any>;
399
- /** The current {@link Latency}. Returns the same object until it changes. */
400
- getSnapshot(): Latency;
401
- /** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
402
- subscribe(listener: () => void): () => void;
403
- /** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
404
- serverNow(): number;
405
- /** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
406
- ping(): void;
407
- /** Stop pinging and listening. Only needed for instances created dynamically. */
408
- dispose(): void;
437
+ /** The client being measured. Hooks reading the latency keep it connected. */
438
+ readonly client: WebSocketClient<any, any>;
439
+ /** The current {@link Latency}. Returns the same object until it changes. */
440
+ getSnapshot(): Latency;
441
+ /** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
442
+ subscribe(listener: () => void): () => void;
443
+ /** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
444
+ serverNow(): number;
445
+ /** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
446
+ ping(): void;
447
+ /** Stop pinging and listening. Only needed for instances created dynamically. */
448
+ dispose(): void;
409
449
  }
410
450
  /**
411
451
  * Measure round-trip time and estimate the server's clock, for ping displays, countdowns
@@ -431,55 +471,57 @@ interface WebSocketLatency {
431
471
  * const left = (endsAt - latency.serverNow()) / 1000;
432
472
  * ```
433
473
  */
434
- declare function createWebSocketLatency<In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>): WebSocketLatency;
435
-
474
+ export declare function createWebSocketLatency<In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>): WebSocketLatency;
475
+ //#endregion
476
+ //#region src/requests.d.ts
436
477
  /** Options for {@link createWebSocketRequests}: how requests and replies look on the wire. */
437
478
  interface WebSocketRequestsOptions<Req, In, Out> {
438
- /**
439
- * Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`
440
- * to type what `request()` accepts.
441
- */
442
- send: (message: Req, id: number) => Out;
443
- /**
444
- * The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.
445
- * A string id matches the number it was sent as, so either echo works.
446
- */
447
- reply: (message: In) => number | string | null | undefined;
448
- /**
449
- * Called on a reply. Return something other than `undefined` to reject the request with
450
- * it instead of resolving, e.g. for a server-side error reply.
451
- */
452
- error?: (message: In) => unknown;
453
- /**
454
- * Default time to wait for a reply, in ms. `Infinity` waits forever.
455
- * @default 10000
456
- */
457
- timeout?: number;
479
+ /**
480
+ * Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`
481
+ * to type what `request()` accepts.
482
+ */
483
+ send: (message: Req, id: number) => Out;
484
+ /**
485
+ * The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.
486
+ * A string id matches the number it was sent as, so either echo works.
487
+ */
488
+ reply: (message: In) => number | string | null | undefined;
489
+ /**
490
+ * Called on a reply. Return something other than `undefined` to reject the request with
491
+ * it instead of resolving, e.g. for a server-side error reply.
492
+ */
493
+ error?: (message: In) => unknown;
494
+ /**
495
+ * Default time to wait for a reply, in ms. `Infinity` waits forever.
496
+ * @default 10000
497
+ */
498
+ timeout?: number;
458
499
  }
459
500
  /** Per-call options for {@link WebSocketRequests.request}. */
460
501
  interface RequestOptions extends SendOptions {
461
- /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
462
- timeout?: number;
463
- /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
464
- signal?: AbortSignal;
502
+ /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
503
+ timeout?: number;
504
+ /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
505
+ signal?: AbortSignal;
465
506
  }
466
507
  /** Request/response over one client. Create with {@link createWebSocketRequests}. */
467
508
  interface WebSocketRequests<Req, In> {
468
- /** The client requests are sent on. */
469
- readonly client: WebSocketClient<any, any>;
470
- /**
471
- * Send a request and resolve with its reply. Holds the connection until it settles.
472
- *
473
- * Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
474
- * aborted, and with an `Error` when the connection it was sent on closes (the reply can
475
- * no longer arrive) or the client stops. A request made while disconnected is queued like
476
- * any other message and waits for the connection.
477
- */
478
- request(message: Req, options?: RequestOptions): Promise<In>;
479
- /** Number of requests waiting for a reply. */
480
- pending(): number;
481
- /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
482
- dispose(): void;
509
+ /** The client requests are sent on. */
510
+ readonly client: WebSocketClient<any, any>;
511
+ /**
512
+ * Send a request and resolve with its reply. Holds the connection until it settles.
513
+ *
514
+ * Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
515
+ * aborted, and with an `Error` when the connection it was sent on closes (the reply can
516
+ * no longer arrive) or the client stops. A request made while disconnected is queued like
517
+ * any other message and waits for the connection; it rejects if it is dropped from the
518
+ * queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
519
+ */
520
+ request(message: Req, options?: RequestOptions): Promise<In>;
521
+ /** Number of requests waiting for a reply. */
522
+ pending(): number;
523
+ /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
524
+ dispose(): void;
483
525
  }
484
526
  /**
485
527
  * Send requests and await their replies, matched by an id the library generates.
@@ -499,17 +541,245 @@ interface WebSocketRequests<Req, In> {
499
541
  * const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });
500
542
  * ```
501
543
  */
502
- declare function createWebSocketRequests<Req, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>): WebSocketRequests<Req, In>;
503
-
544
+ export declare function createWebSocketRequests<Req, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>): WebSocketRequests<Req, In>;
545
+ //#endregion
546
+ //#region src/responder.d.ts
547
+ /** What `request` returns for a request: any object with these fields, such as the message itself. */
548
+ type RequestFields = {
549
+ id: number | string;
550
+ method: string;
551
+ params?: unknown;
552
+ timeout?: number;
553
+ };
554
+ /**
555
+ * The type of `params` in `Q`, keeping `undefined` when they're optional and giving `undefined`
556
+ * when there are none. (Inferring `params?: P` directly would drop the `undefined`.)
557
+ */
558
+ type ParamsOf<Q> = Q extends unknown ? 'params' extends keyof Q ? Q['params' & keyof Q] : undefined : never;
559
+ /**
560
+ * Options for {@link createWebSocketResponder}: how the server's requests and your replies look
561
+ * on the wire.
562
+ *
563
+ * @typeParam Q - What `request` returns. Handlers receive its `params`.
564
+ * @typeParam R - What handlers return, from the annotation on `respond`'s `result`.
565
+ */
566
+ interface WebSocketResponderOptions<Q extends RequestFields, R, In, Out> {
567
+ /**
568
+ * Recognize a request from the server: its `id`, the `method` handlers are registered for,
569
+ * and the `params` they receive. Return `undefined` (or `null`) for any other message.
570
+ * A `timeout` (in ms) overrides the manager's for this request.
571
+ *
572
+ * `params` come from the server, so validate them before acting on them.
573
+ */
574
+ request: (message: In) => Q | null | undefined;
575
+ /** Build the reply to request `id`. Annotate `result` to type what handlers return. */
576
+ respond: (id: number | string, result: R) => Out;
577
+ /**
578
+ * Build the error reply to request `id`: what a handler threw, a `NotFoundError` `DOMException`
579
+ * when nothing is registered for the method, a `TimeoutError` after `timeout`, or an
580
+ * `AbortError` from `dispose()`.
581
+ */
582
+ error: (id: number | string, error: unknown) => Out;
583
+ /**
584
+ * The id of a request the server withdrew, or `undefined` (or `null`) if the message isn't a
585
+ * cancellation. The request's `signal` aborts and nothing is sent.
586
+ */
587
+ cancel?: (message: In) => number | string | null | undefined;
588
+ /**
589
+ * Time to answer a request, in ms. When it runs out, the `error` reply (a `TimeoutError`)
590
+ * is sent and the request's `signal` aborts.
591
+ * @default Infinity
592
+ */
593
+ timeout?: number;
594
+ }
595
+ /** A request from the server that hasn't been answered yet. */
596
+ interface IncomingRequest<P = unknown, R = unknown> {
597
+ /** The id the server sent. Replies carry it back. */
598
+ readonly id: number | string;
599
+ /** The method it was sent for. */
600
+ readonly method: string;
601
+ /** The `params` from `request`. They come from the server, so validate them. */
602
+ readonly params: P;
603
+ /**
604
+ * Aborts when the answer is no longer wanted: the server cancelled the request, it timed out,
605
+ * the connection it arrived on closed, the responder was disposed, or it was answered with
606
+ * `respond` or `reject` while its handler was still running.
607
+ */
608
+ readonly signal: AbortSignal;
609
+ /**
610
+ * Reply with `result`. Only the first answer counts.
611
+ * @returns `true` if the reply was sent, `false` if the request was already answered or is gone.
612
+ */
613
+ respond(result: R): boolean;
614
+ /**
615
+ * Reply with an error, built by the `error` option. Only the first answer counts.
616
+ * @returns `true` if the reply was sent, `false` if the request was already answered or is gone.
617
+ */
618
+ reject(error: unknown): boolean;
619
+ }
620
+ /**
621
+ * Answers a request in code. Return (or resolve) the result to reply with it; throw (or reject)
622
+ * to send the `error` reply instead.
623
+ */
624
+ type RequestHandler<P = unknown, R = unknown> = (params: P, request: IncomingRequest<P, R>) => R | PromiseLike<R>;
625
+ /** Answers the server's requests on one client. Create with {@link createWebSocketResponder}. */
626
+ interface WebSocketResponder<P = unknown, R = unknown> {
627
+ /** The client requests arrive on. Hooks reading the responder keep it connected. */
628
+ readonly client: WebSocketClient<any, any>;
629
+ /**
630
+ * Answer requests for `method` until the returned function is called.
631
+ *
632
+ * With a `handler`, requests are answered in code. Without one, they wait in
633
+ * {@link getSnapshot} until something calls `respond` or `reject` on them, e.g. a dialog
634
+ * that asks the user. When several registrations exist for one method, the newest one takes
635
+ * new requests, and unregistering it hands them back to the previous one. Requests for a
636
+ * method with no registration get a `NotFoundError` reply.
637
+ *
638
+ * @returns An unregister function (safe to call more than once). Requests already waiting stay.
639
+ */
640
+ onRequest(method: string, handler?: RequestHandler<P, R>): () => void;
641
+ /**
642
+ * Requests waiting for an answer from outside a handler, oldest first. Returns the same array
643
+ * until it changes.
644
+ */
645
+ getSnapshot(): readonly IncomingRequest<P, R>[];
646
+ /** Called whenever {@link getSnapshot} changes. Compatible with `useSyncExternalStore`. */
647
+ subscribe(listener: () => void): () => void;
648
+ /**
649
+ * Stop answering: send an `AbortError` reply to every request in flight and stop listening.
650
+ * Only needed for responders created dynamically.
651
+ */
652
+ dispose(): void;
653
+ }
654
+ /**
655
+ * Answer requests the server sends: run a handler for tools that only exist in the browser
656
+ * (selection, clipboard, geolocation), or let a person answer, e.g. to approve an agent's action.
657
+ * The reverse of `createWebSocketRequests`.
658
+ *
659
+ * Every request gets exactly one reply while its connection lives: the handler's result, an
660
+ * `error` reply when it throws, has no registration or times out. When the connection a request
661
+ * arrived on closes, the request is dropped and nothing is sent, since the server can't match a
662
+ * reply on a new connection. Replies are never queued.
663
+ *
664
+ * Requests are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * export const tools = createWebSocketResponder(agent, {
669
+ * request: message => (message.type === 'call' ? message : undefined),
670
+ * respond: (id, result: unknown) => ({ type: 'result', id, result }),
671
+ * error: (id, error) => ({ type: 'result', id, error: String(error) })
672
+ * });
673
+ *
674
+ * tools.onRequest('get_selection', () => window.getSelection()?.toString() ?? '');
675
+ *
676
+ * // In a component: requests a person answers.
677
+ * const approvals = useAgentApproval(tools, 'approve');
678
+ * approvals[0]?.respond({ approved: true });
679
+ * ```
680
+ */
681
+ 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>;
682
+ //#endregion
683
+ //#region src/sendrate.d.ts
684
+ /** Options for {@link createWebSocketSendRate}. */
685
+ interface WebSocketSendRateOptions {
686
+ /**
687
+ * Ticks per second while the connection keeps up.
688
+ * @default 30
689
+ */
690
+ maxRate?: number;
691
+ /**
692
+ * The rate never drops below this, in ticks per second.
693
+ * @default 5
694
+ */
695
+ minRate?: number;
696
+ /**
697
+ * Bytes waiting in the browser's send buffer (`socket.bufferedAmount`) above which a tick
698
+ * sends nothing and the rate halves.
699
+ * @default 1024
700
+ */
701
+ maxBuffered?: number;
702
+ /**
703
+ * Measurements from `createWebSocketLatency` on the same client. With them, a round-trip time
704
+ * that rises also lowers the rate. Without them, only the send buffer is watched.
705
+ */
706
+ latency?: Pick<WebSocketLatency, 'getSnapshot'>;
707
+ /**
708
+ * How far `rtt` may rise above the lowest one measured on this connection before the rate
709
+ * halves, in ms. A steady high ping keeps the full rate; only a rising one counts.
710
+ * @default 100
711
+ */
712
+ maxRttIncrease?: number;
713
+ }
714
+ /** The current send rate. A new object is created on every change. */
715
+ interface SendRate {
716
+ /** Ticks per second, rounded. */
717
+ readonly rate: number;
718
+ /**
719
+ * Whether the last tick found the link congested: the send buffer above `maxBuffered`, or
720
+ * `rtt` risen more than `maxRttIncrease`.
721
+ */
722
+ readonly congested: boolean;
723
+ }
724
+ /** Keyed sends coalesced at a rate that adapts to the link. Create with {@link createWebSocketSendRate}. */
725
+ interface WebSocketSendRate<Out> {
726
+ /** The client the messages are sent on. */
727
+ readonly client: WebSocketClient<any, Out>;
728
+ /**
729
+ * Send `message` at the next tick. A message with the same `key` still waiting for it is
730
+ * replaced, so only the newest one goes out. Sent with `volatile`.
731
+ */
732
+ send(message: Out, key: string): void;
733
+ /** Send everything waiting now, e.g. right before a one-shot action that must carry the latest aim. */
734
+ flush(): void;
735
+ /** The current {@link SendRate}. Returns the same object until it changes. */
736
+ getSnapshot(): SendRate;
737
+ /** Called whenever the rate or `congested` changes. Compatible with `useSyncExternalStore`. */
738
+ subscribe(listener: () => void): () => void;
739
+ /** Stop sending and listening. Only needed for instances created dynamically. */
740
+ dispose(): void;
741
+ }
742
+ /**
743
+ * Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
744
+ * connection. Messages are coalesced per key between ticks. The rate drops by half when the
745
+ * send buffer fills up or the round trip grows, and climbs back one tick per second at a time
746
+ * when the link keeps up, so players on a poor connection degrade smoothly instead of building
747
+ * up lag.
748
+ *
749
+ * Ticks run only while the socket is open and something is waiting. Messages waiting during a
750
+ * reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
751
+ *
752
+ * @example
753
+ * ```ts
754
+ * export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
755
+ *
756
+ * // On every pointer move: at most 30 a second, fewer on a congested link.
757
+ * stream.send({ type: 'aim', angle }, 'aim');
758
+ * ```
759
+ */
760
+ export declare function createWebSocketSendRate<Out>(client: WebSocketClient<any, Out>, options?: WebSocketSendRateOptions): WebSocketSendRate<Out>;
761
+ //#endregion
762
+ //#region src/state.d.ts
504
763
  /** Options for {@link createWebSocketState}. */
505
764
  interface WebSocketStateOptions<S, In> {
506
- /** The state before any message arrives, and what `reset()` returns to. */
507
- initialState: S;
508
- /**
509
- * Fold one incoming message into the state. Must be pure: return a new object for a
510
- * change, or the same `state` to ignore the message (subscribers aren't notified).
511
- */
512
- reduce: (state: S, message: In) => S;
765
+ /** The state before any message arrives, and what `reset()` returns to. */
766
+ initialState: S;
767
+ /**
768
+ * Fold one incoming message into the state. Must be pure: return a new object for a
769
+ * change, or the same `state` to ignore the message (subscribers aren't notified).
770
+ */
771
+ reduce: (state: S, message: In) => S;
772
+ /**
773
+ * `'frame'`: notify subscribers at most once per animation frame instead of once per
774
+ * message, so a fast stream (tokens, tickers, game state) renders once per frame.
775
+ * `getState()` is always current; only notifications wait. `setState` and `reset` still
776
+ * notify right away.
777
+ *
778
+ * Browsers pause animation frames in background tabs: the state keeps up, and subscribers
779
+ * hear about it when the tab is visible again. Without `requestAnimationFrame` (Node, some
780
+ * workers), it waits about 16 ms instead.
781
+ */
782
+ batch?: 'frame';
513
783
  }
514
784
  /**
515
785
  * State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
@@ -518,20 +788,20 @@ interface WebSocketStateOptions<S, In> {
518
788
  * @typeParam S - The state type. Don't use a function type: `setState` treats functions as updaters.
519
789
  */
520
790
  interface WebSocketState<S> {
521
- /** The client whose messages this store reduces. Hooks reading the store keep it connected. */
522
- readonly client: WebSocketClient<any, any>;
523
- /** The `initialState` it was created with. Used for server rendering and hydration. */
524
- readonly initialState: S;
525
- /** The current state. Returns the same object until it changes. */
526
- getState(): S;
527
- /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */
528
- subscribe(listener: () => void): () => void;
529
- /** Replace the state, or update it from the current one, e.g. for optimistic updates. */
530
- setState(update: S | ((state: S) => S)): void;
531
- /** Go back to `initialState`. */
532
- reset(): void;
533
- /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */
534
- dispose(): void;
791
+ /** The client whose messages this store reduces. Hooks reading the store keep it connected. */
792
+ readonly client: WebSocketClient<any, any>;
793
+ /** The `initialState` it was created with. Used for server rendering and hydration. */
794
+ readonly initialState: S;
795
+ /** The current state. Returns the same object until it changes. */
796
+ getState(): S;
797
+ /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */
798
+ subscribe(listener: () => void): () => void;
799
+ /** Replace the state, or update it from the current one, e.g. for optimistic updates. */
800
+ setState(update: S | ((state: S) => S)): void;
801
+ /** Go back to `initialState`. */
802
+ reset(): void;
803
+ /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */
804
+ dispose(): void;
535
805
  }
536
806
  /**
537
807
  * Keep state derived from a client's messages, like a reducer that runs on every message.
@@ -551,28 +821,29 @@ interface WebSocketState<S> {
551
821
  * const acme = useWebSocketState(priceState, state => state.ACME);
552
822
  * ```
553
823
  */
554
- declare function createWebSocketState<S, In>(client: WebSocketClient<In, any>, options: WebSocketStateOptions<S, NoInfer<In>>): WebSocketState<S>;
555
-
824
+ export declare function createWebSocketState<S, In>(client: WebSocketClient<In, any>, options: WebSocketStateOptions<S, NoInfer<In>>): WebSocketState<S>;
825
+ //#endregion
826
+ //#region src/topics.d.ts
556
827
  /** Options for {@link createWebSocketTopics}: the messages your server expects. */
557
828
  interface WebSocketTopicsOptions<Out> {
558
- /** The message that subscribes to `topic`. */
559
- subscribe: (topic: string) => Out;
560
- /** The message that unsubscribes from `topic`. Omit it if your server has none. */
561
- unsubscribe?: (topic: string) => Out;
829
+ /** The message that subscribes to `topic`. */
830
+ subscribe: (topic: string) => Out;
831
+ /** The message that unsubscribes from `topic`. Omit it if your server has none. */
832
+ unsubscribe?: (topic: string) => Out;
562
833
  }
563
834
  /** Reference-counted topic subscriptions on one client. Create with {@link createWebSocketTopics}. */
564
835
  interface WebSocketTopics {
565
- /** The client the subscription messages are sent on. */
566
- readonly client: WebSocketClient<any, any>;
567
- /**
568
- * Hold a subscription to `topic`, and the client's connection, until the returned
569
- * function is called. The release function is safe to call more than once.
570
- */
571
- acquire(topic: string): () => void;
572
- /** The topics currently held, in the order they were first acquired. */
573
- active(): string[];
574
- /** Stop managing subscriptions. Only needed for managers created dynamically. */
575
- dispose(): void;
836
+ /** The client the subscription messages are sent on. */
837
+ readonly client: WebSocketClient<any, any>;
838
+ /**
839
+ * Hold a subscription to `topic`, and the client's connection, until the returned
840
+ * function is called. The release function is safe to call more than once.
841
+ */
842
+ acquire(topic: string): () => void;
843
+ /** The topics currently held, in the order they were first acquired. */
844
+ active(): string[];
845
+ /** Stop managing subscriptions. Only needed for managers created dynamically. */
846
+ dispose(): void;
576
847
  }
577
848
  /**
578
849
  * Manage topic subscriptions that survive reconnects.
@@ -596,44 +867,44 @@ interface WebSocketTopics {
596
867
  * useWebSocketTopic(topics, `prices:${symbol}`);
597
868
  * ```
598
869
  */
599
- declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
600
-
870
+ export declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
871
+ //#endregion
872
+ //#region src/react.d.ts
601
873
  /**
602
874
  * Per-component event handlers. They always see the component's latest props and
603
875
  * state, and changing them never reconnects.
604
876
  */
605
877
  interface WebSocketHandlers<In> {
606
- /** The socket opened (initially or after a reconnect). */
607
- onOpen?: (event: Event) => void;
608
- /** A message arrived and passed `parse`/`json` and `schema`. */
609
- onMessage?: (message: In, event: MessageEvent) => void;
610
- /** A socket closed. Called whether or not it will be retried. */
611
- onClose?: (close: CloseInfo) => void;
612
- /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */
613
- onError?: (error: Event | Error) => void;
614
- /**
615
- * `false` releases this component's hold on the connection and stops its handlers.
616
- * The connection closes once nothing else holds it.
617
- * @default true
618
- */
619
- enabled?: boolean;
878
+ /** The socket opened (initially or after a reconnect). */
879
+ onOpen?: (event: Event) => void;
880
+ /** A message arrived and passed `parse`/`json` and `schema`. */
881
+ onMessage?: (message: In, event: MessageEvent) => void;
882
+ /** A socket closed. Called whether or not it will be retried. */
883
+ onClose?: (close: CloseInfo) => void;
884
+ /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */
885
+ onError?: (error: Event | Error) => void;
886
+ /**
887
+ * `false` releases this component's hold on the connection and stops its handlers.
888
+ * The connection closes once nothing else holds it.
889
+ * @default true
890
+ */
891
+ enabled?: boolean;
620
892
  }
621
893
  /** Options for `useWebSocket(url, options)`: connection options plus handlers. */
622
- interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {
623
- }
894
+ interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}
624
895
  /** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */
625
896
  interface UseWebSocketResult<In, Out> extends Snapshot<In> {
626
- /**
627
- * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).
628
- * Returns `true` if sent immediately. Stable identity.
629
- */
630
- send: (message: Out, options?: SendOptions) => boolean;
631
- /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
632
- close: (code?: number, reason?: string) => void;
633
- /** Drop the socket and connect again right away. Stable identity. */
634
- reconnect: () => void;
635
- /** The underlying client, or `null` when the URL is `null`/`undefined`. */
636
- client: WebSocketClient<In, Out> | null;
897
+ /**
898
+ * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).
899
+ * Returns `true` if sent immediately. Stable identity.
900
+ */
901
+ send: (message: Out, options?: SendOptions) => boolean;
902
+ /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
903
+ close: (code?: number, reason?: string) => void;
904
+ /** Drop the socket and connect again right away. Stable identity. */
905
+ reconnect: () => void;
906
+ /** The underlying client, or `null` when the URL is `null`/`undefined`. */
907
+ client: WebSocketClient<In, Out> | null;
637
908
  }
638
909
  /**
639
910
  * Connect to a WebSocket and re-render with its status and latest message.
@@ -658,8 +929,8 @@ interface UseWebSocketResult<In, Out> extends Snapshot<In> {
658
929
  * });
659
930
  * ```
660
931
  */
661
- declare function useWebSocket<In = unknown, Out = unknown>(url: string | null | undefined, options?: UseWebSocketOptions<In, Out>): UseWebSocketResult<In, Out>;
662
- declare function useWebSocket<In, Out>(client: WebSocketClient<In, Out> | null | undefined, options?: WebSocketHandlers<In>): UseWebSocketResult<In, Out>;
932
+ export declare function useWebSocket<In = unknown, Out = unknown>(url: string | null | undefined, options?: UseWebSocketOptions<In, Out>): UseWebSocketResult<In, Out>;
933
+ export declare function useWebSocket<In, Out>(client: WebSocketClient<In, Out> | null | undefined, options?: WebSocketHandlers<In>): UseWebSocketResult<In, Out>;
663
934
  /**
664
935
  * Listen to a client's events without re-rendering the component. The listener always
665
936
  * sees the latest props and state. Keeps the connection open while mounted.
@@ -669,7 +940,7 @@ declare function useWebSocket<In, Out>(client: WebSocketClient<In, Out> | null |
669
940
  * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));
670
941
  * ```
671
942
  */
672
- declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: WebSocketClient<In, any> | null | undefined, type: K, listener: EventMap<In>[K]): void;
943
+ export declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: WebSocketClient<In, any> | null | undefined, type: K, listener: EventMap<In>[K]): void;
673
944
  /**
674
945
  * The connection status of a client. Re-renders only when the status changes, not on
675
946
  * messages. Keeps the connection open while mounted.
@@ -679,7 +950,7 @@ declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: Web
679
950
  * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'
680
951
  * ```
681
952
  */
682
- declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status;
953
+ export declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status;
683
954
  /**
684
955
  * Read state built from messages by `createWebSocketState`. Re-renders only when the
685
956
  * selected value changes (by `isEqual`, default `Object.is`), however often messages
@@ -692,8 +963,8 @@ declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | u
692
963
  * const acme = useWebSocketState(priceState, state => state.ACME);
693
964
  * ```
694
965
  */
695
- declare function useWebSocketState<S>(store: WebSocketState<S>): S;
696
- declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (state: S) => T, isEqual?: (a: T, b: T) => boolean): T;
966
+ export declare function useWebSocketState<S>(store: WebSocketState<S>): S;
967
+ export declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (state: S) => T, isEqual?: (a: T, b: T) => boolean): T;
697
968
  /**
698
969
  * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`
699
970
  * manager. Components holding the same topic share one subscription, and it is sent again
@@ -705,7 +976,7 @@ declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (st
705
976
  * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));
706
977
  * ```
707
978
  */
708
- declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, topic: string | readonly string[] | null | undefined): void;
979
+ export declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, topic: string | readonly string[] | null | undefined): void;
709
980
  /**
710
981
  * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.
711
982
  * Re-renders when a measurement changes, about once per ping interval. Keeps the connection
@@ -717,6 +988,30 @@ declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, t
717
988
  * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;
718
989
  * ```
719
990
  */
720
- declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
721
-
722
- export { type CloseInfo, type EventMap, type HeartbeatOptions, type Latency, MessageError, type QueueOptions, type ReconnectOptions, type RequestOptions, type SendData, type SendOptions, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketClient, type WebSocketHandlers, type WebSocketLatency, type WebSocketLatencyOptions, type WebSocketOptions, type WebSocketRequests, type WebSocketRequestsOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketState, createWebSocketTopics, useWebSocket, useWebSocketEvent, useWebSocketLatency, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
991
+ export declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
992
+ /**
993
+ * Requests for `method` that a person answers, e.g. an agent asking to run a tool. While mounted,
994
+ * it registers `method` on the responder without a handler, so new requests wait here instead of
995
+ * getting a `NotFoundError` reply. Call `respond` or `reject` on one to answer it. Re-renders
996
+ * when the list changes. Keeps the connection open while mounted. `null` holds nothing.
997
+ *
998
+ * Requests stay listed after unmount until they are answered, time out, are cancelled or lose
999
+ * their connection, so a remounted dialog shows them again. On the server the list is empty.
1000
+ *
1001
+ * @example
1002
+ * ```tsx
1003
+ * const [request] = useAgentApproval(tools, 'approve');
1004
+ * if (!request) return null;
1005
+ * return (
1006
+ * <dialog open>
1007
+ * {request.params.summary}
1008
+ * <button onClick={() => request.respond({ approved: true })}>Approve</button>
1009
+ * <button onClick={() => request.respond({ approved: false })}>Deny</button>
1010
+ * </dialog>
1011
+ * );
1012
+ * ```
1013
+ */
1014
+ export declare function useAgentApproval<P, R>(responder: WebSocketResponder<P, R> | null | undefined, method: string | null | undefined): readonly IncomingRequest<P, R>[];
1015
+ //#endregion
1016
+ export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, SendRate, Snapshot, StandardSchemaV1, Status, UrlSource, UseWebSocketOptions, UseWebSocketResult, WebSocketClient, WebSocketHandlers, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketSendRate, WebSocketSendRateOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
1017
+ //# sourceMappingURL=index.d.cts.map