react-websocket 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+
2
+ export * from "./core.js";
3
+ //#region src/types.d.ts
1
4
  /**
2
5
  * Where to connect: a URL, or a function that returns one (sync or async).
3
6
  *
@@ -24,90 +27,111 @@ type UrlSource = string | (() => string | Promise<string>);
24
27
  type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
25
28
  /** The client's state as seen by React. A new object is created on every change. */
26
29
  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;
30
+ /** Connection state. */
31
+ readonly status: Status;
32
+ /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
33
+ readonly lastMessage: In | undefined;
34
+ /** Consecutive failed attempts since the last successful open. `0` while open. */
35
+ readonly attempt: number;
33
36
  }
34
37
  /** Details of a closed connection, from the browser's `CloseEvent`. */
35
38
  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;
39
+ /** Close code: `1000` is a normal close, `1006` an abnormal one (e.g. network loss), `4000`–`4999` are yours. */
40
+ readonly code: number;
41
+ /** Close reason sent by the side that closed the connection. Often empty. */
42
+ readonly reason: string;
43
+ /** Whether the closing handshake completed. */
44
+ readonly wasClean: boolean;
42
45
  }
43
46
  /** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */
44
47
  type SendData = Parameters<WebSocket['send']>[0];
45
48
  /** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */
46
49
  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;
50
+ /**
51
+ * Maximum consecutive reconnect attempts. When the last one fails too, the client
52
+ * gives up: the status becomes `closed` and `ready` rejects. `0` never retries.
53
+ * @default Infinity
54
+ */
55
+ maxAttempts?: number;
56
+ /**
57
+ * Delay before the first retry, in ms.
58
+ * @default 1000
59
+ */
60
+ minDelay?: number;
61
+ /**
62
+ * Upper bound for the delay, in ms.
63
+ * @default 30000
64
+ */
65
+ maxDelay?: number;
66
+ /**
67
+ * Backoff multiplier applied per attempt.
68
+ * @default 2
69
+ */
70
+ factor?: number;
71
+ /**
72
+ * Randomly shave up to this fraction (0–1) off each delay, so that many clients
73
+ * dropped at once don't all reconnect at the same moment.
74
+ * @default 0.5
75
+ */
76
+ jitter?: number;
77
+ /**
78
+ * Called on every unexpected close. Return `false` to stop reconnecting.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * // Don't retry when the server rejects our credentials.
83
+ * shouldReconnect: close => close.code !== 4001
84
+ * ```
85
+ * @default () => true
86
+ */
87
+ shouldReconnect?: (close: CloseInfo) => boolean;
85
88
  }
86
89
  /** Application-level keep-alive. Useful behind proxies that drop idle connections. */
87
90
  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;
91
+ /** Send `message` every `interval` ms while open. */
92
+ interval: number;
93
+ /**
94
+ * Sent as-is (it is not passed through `serialize` or `json`).
95
+ * @default 'ping'
96
+ */
97
+ message?: string;
98
+ /** Incoming data equal to this string is swallowed instead of being delivered as a message. */
99
+ pong?: string;
100
+ /**
101
+ * If nothing at all arrives within this many ms after a ping, the connection is
102
+ * treated as dead: it closes with code `4000` and reconnects.
103
+ */
104
+ timeout?: number;
105
+ }
106
+ /** Per-message options for `send()`. */
107
+ interface SendOptions {
108
+ /**
109
+ * Drop the message instead of queueing it when the socket isn't open. For data that
110
+ * is stale by the time a reconnect finishes, such as player input or cursor positions.
111
+ * @default false
112
+ */
113
+ volatile?: boolean;
114
+ /**
115
+ * Keep only the newest queued message with this key. Sending again with the same key
116
+ * while disconnected replaces the earlier one, which moves to the back of the queue.
117
+ * For state where only the latest value matters, such as "my position is now (x, y)".
118
+ */
119
+ key?: string;
120
+ /**
121
+ * Called if the message is dropped from the queue without being sent: replaced by a
122
+ * newer message with the same `key`, or evicted by `queue.limit`. Not called when the
123
+ * queue is cleared because the client closed or gave up.
124
+ */
125
+ onDrop?: () => void;
102
126
  }
103
127
  /** Bounds for the send queue. */
104
128
  interface QueueOptions {
105
- /**
106
- * Maximum number of messages held while not connected. When full, the oldest
107
- * message is dropped.
108
- * @default Infinity
109
- */
110
- limit?: number;
129
+ /**
130
+ * Maximum number of messages held while not connected. When full, the oldest
131
+ * message is dropped.
132
+ * @default Infinity
133
+ */
134
+ limit?: number;
111
135
  }
112
136
  /**
113
137
  * The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented
@@ -115,24 +139,24 @@ interface QueueOptions {
115
139
  * import this; pass your schema object directly.
116
140
  */
117
141
  interface StandardSchemaV1<Input = unknown, Output = Input> {
118
- readonly '~standard': {
119
- readonly version: 1;
120
- readonly vendor: string;
121
- readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
122
- readonly types?: {
123
- readonly input: Input;
124
- readonly output: Output;
125
- } | undefined;
126
- };
142
+ readonly '~standard': {
143
+ readonly version: 1;
144
+ readonly vendor: string;
145
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
146
+ readonly types?: {
147
+ readonly input: Input;
148
+ readonly output: Output;
149
+ } | undefined;
150
+ };
127
151
  }
128
152
  /** The result of a Standard Schema `validate` call. */
129
153
  type StandardSchemaResult<Output> = {
130
- readonly value: Output;
131
- readonly issues?: undefined;
154
+ readonly value: Output;
155
+ readonly issues?: undefined;
132
156
  } | {
133
- readonly issues: ReadonlyArray<{
134
- readonly message: string;
135
- }>;
157
+ readonly issues: ReadonlyArray<{
158
+ readonly message: string;
159
+ }>;
136
160
  };
137
161
  /**
138
162
  * Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.
@@ -141,66 +165,66 @@ type StandardSchemaResult<Output> = {
141
165
  * @typeParam Out - Type accepted by `send()`.
142
166
  */
143
167
  interface WebSocketOptions<In = unknown, Out = unknown> {
144
- /** Sub-protocol(s) passed to the `WebSocket` constructor. */
145
- protocols?: string | string[];
146
- /**
147
- * How binary messages are exposed.
148
- * @default 'blob'
149
- */
150
- binaryType?: BinaryType;
151
- /**
152
- * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail
153
- * to parse are reported to `onError` as a {@link MessageError} and not delivered.
154
- * @default false
155
- */
156
- json?: boolean;
157
- /**
158
- * Custom decoder for incoming `event.data`. Takes precedence over `json`.
159
- * Throwing reports a {@link MessageError} and drops the message.
160
- */
161
- parse?: (data: unknown) => unknown;
162
- /** Custom encoder for outgoing messages. Takes precedence over `json`. */
163
- serialize?: (message: Out) => SendData;
164
- /**
165
- * Validate incoming messages (after parsing) with any Standard Schema library.
166
- * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.
167
- * The message type `In` is inferred from it.
168
- *
169
- * @example
170
- * ```ts
171
- * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });
172
- * ```
173
- */
174
- schema?: StandardSchemaV1<unknown, In>;
175
- /**
176
- * Reconnect with exponential backoff after an unexpected close. `false` disables it.
177
- * @default true
178
- */
179
- reconnect?: boolean | ReconnectOptions;
180
- /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */
181
- heartbeat?: HeartbeatOptions;
182
- /**
183
- * Buffer `send()` calls made while not connected and send them, in order, on open
184
- * (right after `open` listeners run).
185
- * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
186
- * The queue is cleared by `close()` or when the client gives up.
187
- * @default true
188
- */
189
- queue?: boolean | QueueOptions;
190
- /**
191
- * Receives debug messages about connection attempts, retries and dropped messages.
192
- * @example
193
- * ```ts
194
- * logger: console.debug
195
- * ```
196
- */
197
- logger?: (message: string, ...details: unknown[]) => void;
198
- /**
199
- * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.
200
- * When omitted, the global `WebSocket` is used in browsers. During a server render
201
- * (no `window`), nothing connects unless an implementation is passed.
202
- */
203
- WebSocket?: typeof WebSocket;
168
+ /** Sub-protocol(s) passed to the `WebSocket` constructor. */
169
+ protocols?: string | string[];
170
+ /**
171
+ * How binary messages are exposed.
172
+ * @default 'blob'
173
+ */
174
+ binaryType?: BinaryType;
175
+ /**
176
+ * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail
177
+ * to parse are reported to `onError` as a {@link MessageError} and not delivered.
178
+ * @default false
179
+ */
180
+ json?: boolean;
181
+ /**
182
+ * Custom decoder for incoming `event.data`. Takes precedence over `json`.
183
+ * Throwing reports a {@link MessageError} and drops the message.
184
+ */
185
+ parse?: (data: unknown) => unknown;
186
+ /** Custom encoder for outgoing messages. Takes precedence over `json`. */
187
+ serialize?: (message: Out) => SendData;
188
+ /**
189
+ * Validate incoming messages (after parsing) with any Standard Schema library.
190
+ * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.
191
+ * The message type `In` is inferred from it.
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });
196
+ * ```
197
+ */
198
+ schema?: StandardSchemaV1<unknown, In>;
199
+ /**
200
+ * Reconnect with exponential backoff after an unexpected close. `false` disables it.
201
+ * @default true
202
+ */
203
+ reconnect?: boolean | ReconnectOptions;
204
+ /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */
205
+ heartbeat?: HeartbeatOptions;
206
+ /**
207
+ * Buffer `send()` calls made while not connected and send them, in order, on open
208
+ * (right after `open` listeners run).
209
+ * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
210
+ * The queue is cleared by `close()` or when the client gives up.
211
+ * @default true
212
+ */
213
+ queue?: boolean | QueueOptions;
214
+ /**
215
+ * Receives debug messages about connection attempts, retries and dropped messages.
216
+ * @example
217
+ * ```ts
218
+ * logger: console.debug
219
+ * ```
220
+ */
221
+ logger?: (message: string, ...details: unknown[]) => void;
222
+ /**
223
+ * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.
224
+ * When omitted, the global `WebSocket` is used in browsers. During a server render
225
+ * (no `window`), nothing connects unless an implementation is passed.
226
+ */
227
+ WebSocket?: typeof WebSocket;
204
228
  }
205
229
  /**
206
230
  * Reported via the `error` event (and `onError`) when an incoming message can't be
@@ -214,35 +238,35 @@ interface WebSocketOptions<In = unknown, Out = unknown> {
214
238
  * ```
215
239
  */
216
240
  declare class MessageError extends Error {
217
- /** The raw `event.data` that failed. */
218
- readonly data: unknown;
219
- /** Schema validation issues, when the message failed `schema`. */
220
- readonly issues: ReadonlyArray<{
221
- readonly message: string;
222
- }> | undefined;
223
- constructor(message: string, data: unknown, options?: {
224
- cause?: unknown;
225
- issues?: ReadonlyArray<{
226
- readonly message: string;
227
- }>;
228
- });
241
+ /** The raw `event.data` that failed. */
242
+ readonly data: unknown;
243
+ /** Schema validation issues, when the message failed `schema`. */
244
+ readonly issues: ReadonlyArray<{
245
+ readonly message: string;
246
+ }> | undefined;
247
+ constructor(message: string, data: unknown, options?: {
248
+ cause?: unknown;
249
+ issues?: ReadonlyArray<{
250
+ readonly message: string;
251
+ }>;
252
+ });
229
253
  }
230
254
  /** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */
231
255
  interface EventMap<In> {
232
- /**
233
- * The socket opened (initially or after a reconnect). Listeners run before queued
234
- * messages are sent, so a handshake sent from here goes out first.
235
- */
236
- open: (event: Event) => void;
237
- /** A message arrived and passed `parse` and `schema`. */
238
- message: (message: In, event: MessageEvent) => void;
239
- /** A socket closed, whether it will be retried or not. */
240
- close: (close: CloseInfo) => void;
241
- /**
242
- * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,
243
- * or the `Error` thrown by the `WebSocket` constructor or a `url` function.
244
- */
245
- error: (error: Event | MessageError | Error) => void;
256
+ /**
257
+ * The socket opened (initially or after a reconnect). Listeners run before queued
258
+ * messages are sent, so a handshake sent from here goes out first.
259
+ */
260
+ open: (event: Event) => void;
261
+ /** A message arrived and passed `parse` and `schema`. */
262
+ message: (message: In, event: MessageEvent) => void;
263
+ /** A socket closed, whether it will be retried or not. */
264
+ close: (close: CloseInfo) => void;
265
+ /**
266
+ * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,
267
+ * or the `Error` thrown by the `WebSocket` constructor or a `url` function.
268
+ */
269
+ error: (error: Event | MessageError | Error) => void;
246
270
  }
247
271
  /**
248
272
  * A WebSocket connection that reconnects, queues, decodes and can be shared.
@@ -256,95 +280,160 @@ interface EventMap<In> {
256
280
  * @typeParam Out - Type accepted by `send()`.
257
281
  */
258
282
  interface WebSocketClient<In = unknown, Out = unknown> {
259
- /** The URL (or URL function) the client was created with. */
260
- readonly url: UrlSource;
261
- /**
262
- * A promise that resolves once the socket has opened. Reading it starts an idle client.
263
- *
264
- * It stays resolved through automatic reconnects, so `use(client.ready)` shows a
265
- * Suspense fallback only for the first connection, never on a network blip; use
266
- * `status` to show reconnects. It rejects when the client gives up reconnecting, when
267
- * `close()` is called, and during a server render. After that, `connect()` or
268
- * `reconnect()` make it a new pending promise.
269
- *
270
- * It is safe to pass to React 19's `use()`: the same promise is returned until its state
271
- * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is
272
- * already settled.
273
- */
274
- readonly ready: Promise<void>;
275
- /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */
276
- readonly socket: WebSocket | null;
277
- /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */
278
- subscribe(listener: () => void): () => void;
279
- /** The current {@link Snapshot}. Returns the same object until something changes. */
280
- getSnapshot(): Snapshot<In>;
281
- /**
282
- * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown
283
- * asynchronously, so one failing listener doesn't break the others.
284
- */
285
- on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
286
- /**
287
- * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
288
- * open, the message is queued and sent on open (see the `queue` option).
289
- * @returns `true` if it was sent immediately, `false` if it was queued or dropped.
290
- */
291
- send(message: Out): boolean;
292
- /**
293
- * Start connecting, and keep the connection open even when no hook is using the
294
- * client, until `close()` is called. Does nothing if already connecting or open.
295
- */
296
- connect(): void;
297
- /**
298
- * Close the connection and stop reconnecting. The status becomes `closed` and the
299
- * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`
300
- * to start again.
301
- * @param code - Close code. @default 1000
302
- */
303
- close(code?: number, reason?: string): void;
304
- /**
305
- * Drop the current socket (if any) and connect again immediately, resetting the
306
- * backoff. Also restarts a `closed` client.
307
- */
308
- reconnect(): void;
309
- /**
310
- * Hold the connection open until the returned function is called. This is what the
311
- * hooks use; call it yourself to integrate with non-React code.
312
- * @returns A release function (safe to call more than once).
313
- */
314
- acquire(): () => void;
283
+ /** The URL (or URL function) the client was created with. */
284
+ readonly url: UrlSource;
285
+ /**
286
+ * A promise that resolves once the socket has opened. Reading it starts an idle client.
287
+ *
288
+ * It stays resolved through automatic reconnects, so `use(client.ready)` shows a
289
+ * Suspense fallback only for the first connection, never on a network blip; use
290
+ * `status` to show reconnects. It rejects when the client gives up reconnecting, when
291
+ * `close()` is called, and during a server render. After that, `connect()` or
292
+ * `reconnect()` make it a new pending promise.
293
+ *
294
+ * It is safe to pass to React 19's `use()`: the same promise is returned until its state
295
+ * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is
296
+ * already settled.
297
+ */
298
+ readonly ready: Promise<void>;
299
+ /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */
300
+ readonly socket: WebSocket | null;
301
+ /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */
302
+ subscribe(listener: () => void): () => void;
303
+ /** The current {@link Snapshot}. Returns the same object until something changes. */
304
+ getSnapshot(): Snapshot<In>;
305
+ /**
306
+ * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown
307
+ * asynchronously, so one failing listener doesn't break the others.
308
+ */
309
+ on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
310
+ /**
311
+ * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
312
+ * open, the message is queued and sent on open (see the `queue` option), unless
313
+ * `options.volatile` is set.
314
+ * @returns `true` if it was sent immediately, `false` if it was queued or dropped.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
319
+ * game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
320
+ * ```
321
+ */
322
+ send(message: Out, options?: SendOptions): boolean;
323
+ /**
324
+ * Start connecting, and keep the connection open even when no hook is using the
325
+ * client, until `close()` is called. Does nothing if already connecting or open.
326
+ */
327
+ connect(): void;
328
+ /**
329
+ * Close the connection and stop reconnecting. The status becomes `closed` and the
330
+ * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`
331
+ * to start again.
332
+ * @param code - Close code. @default 1000
333
+ */
334
+ close(code?: number, reason?: string): void;
335
+ /**
336
+ * Drop the current socket (if any) and connect again immediately, resetting the
337
+ * backoff. Also restarts a `closed` client.
338
+ */
339
+ reconnect(): void;
340
+ /**
341
+ * Hold the connection open until the returned function is called. This is what the
342
+ * hooks use; call it yourself to integrate with non-React code.
343
+ * @returns A release function (safe to call more than once).
344
+ */
345
+ acquire(): () => void;
346
+ }
347
+ //#endregion
348
+ //#region src/latency.d.ts
349
+ /** Latest latency measurements. A new object is created on every change. */
350
+ interface Latency {
351
+ /** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
352
+ readonly rtt: number | undefined;
353
+ /**
354
+ * Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
355
+ * `undefined` until a pong carries `time`.
356
+ */
357
+ readonly offset: number | undefined;
358
+ }
359
+ /** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */
360
+ interface WebSocketLatency {
361
+ /** The client being measured. Hooks reading the latency keep it connected. */
362
+ readonly client: WebSocketClient<any, any>;
363
+ /** The current {@link Latency}. Returns the same object until it changes. */
364
+ getSnapshot(): Latency;
365
+ /** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
366
+ subscribe(listener: () => void): () => void;
367
+ /** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
368
+ serverNow(): number;
369
+ /** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
370
+ ping(): void;
371
+ /** Stop pinging and listening. Only needed for instances created dynamically. */
372
+ dispose(): void;
373
+ }
374
+ //#endregion
375
+ //#region src/responder.d.ts
376
+ /** A request from the server that hasn't been answered yet. */
377
+ interface IncomingRequest<P = unknown, R = unknown> {
378
+ /** The id the server sent. Replies carry it back. */
379
+ readonly id: number | string;
380
+ /** The method it was sent for. */
381
+ readonly method: string;
382
+ /** The `params` from `request`. They come from the server, so validate them. */
383
+ readonly params: P;
384
+ /**
385
+ * Aborts when the answer is no longer wanted: the server cancelled the request, it timed out,
386
+ * the connection it arrived on closed, the responder was disposed, or it was answered with
387
+ * `respond` or `reject` while its handler was still running.
388
+ */
389
+ readonly signal: AbortSignal;
390
+ /**
391
+ * Reply with `result`. Only the first answer counts.
392
+ * @returns `true` if the reply was sent, `false` if the request was already answered or is gone.
393
+ */
394
+ respond(result: R): boolean;
395
+ /**
396
+ * Reply with an error, built by the `error` option. Only the first answer counts.
397
+ * @returns `true` if the reply was sent, `false` if the request was already answered or is gone.
398
+ */
399
+ reject(error: unknown): boolean;
315
400
  }
316
-
317
401
  /**
318
- * Create a WebSocket client, usually at module level, to share across components.
319
- *
320
- * Nothing connects until a hook uses it, `ready` is read, or `connect()` is called.
321
- * It closes one tick after the last hook unmounts, unless `connect()` was called.
322
- *
323
- * @param url - A URL, or a function (sync or async) called before every connection
324
- * attempt, e.g. to put a fresh auth token in the URL on each reconnect.
325
- * @typeParam In - Type of incoming messages. Inferred from `options.schema` if given.
326
- * @typeParam Out - Type accepted by `send()`.
327
- *
328
- * @example
329
- * ```ts
330
- * type ChatIn = { user: string; text: string };
331
- * type ChatOut = { text: string };
332
- *
333
- * export const chat = createWebSocket<ChatIn, ChatOut>('wss://api.example.com/chat', { json: true });
334
- * ```
402
+ * Answers a request in code. Return (or resolve) the result to reply with it; throw (or reject)
403
+ * to send the `error` reply instead.
335
404
  */
336
- declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
337
-
338
- /** Options for {@link createWebSocketState}. */
339
- interface WebSocketStateOptions<S, In> {
340
- /** The state before any message arrives, and what `reset()` returns to. */
341
- initialState: S;
342
- /**
343
- * Fold one incoming message into the state. Must be pure: return a new object for a
344
- * change, or the same `state` to ignore the message (subscribers aren't notified).
345
- */
346
- reduce: (state: S, message: In) => S;
405
+ type RequestHandler<P = unknown, R = unknown> = (params: P, request: IncomingRequest<P, R>) => R | PromiseLike<R>;
406
+ /** Answers the server's requests on one client. Create with {@link createWebSocketResponder}. */
407
+ interface WebSocketResponder<P = unknown, R = unknown> {
408
+ /** The client requests arrive on. Hooks reading the responder keep it connected. */
409
+ readonly client: WebSocketClient<any, any>;
410
+ /**
411
+ * Answer requests for `method` until the returned function is called.
412
+ *
413
+ * With a `handler`, requests are answered in code. Without one, they wait in
414
+ * {@link getSnapshot} until something calls `respond` or `reject` on them, e.g. a dialog
415
+ * that asks the user. When several registrations exist for one method, the newest one takes
416
+ * new requests, and unregistering it hands them back to the previous one. Requests for a
417
+ * method with no registration get a `NotFoundError` reply.
418
+ *
419
+ * @returns An unregister function (safe to call more than once). Requests already waiting stay.
420
+ */
421
+ onRequest(method: string, handler?: RequestHandler<P, R>): () => void;
422
+ /**
423
+ * Requests waiting for an answer from outside a handler, oldest first. Returns the same array
424
+ * until it changes.
425
+ */
426
+ getSnapshot(): readonly IncomingRequest<P, R>[];
427
+ /** Called whenever {@link getSnapshot} changes. Compatible with `useSyncExternalStore`. */
428
+ subscribe(listener: () => void): () => void;
429
+ /**
430
+ * Stop answering: send an `AbortError` reply to every request in flight and stop listening.
431
+ * Only needed for responders created dynamically.
432
+ */
433
+ dispose(): void;
347
434
  }
435
+ //#endregion
436
+ //#region src/state.d.ts
348
437
  /**
349
438
  * State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
350
439
  * or with `getState`/`subscribe` outside React.
@@ -352,119 +441,74 @@ interface WebSocketStateOptions<S, In> {
352
441
  * @typeParam S - The state type. Don't use a function type: `setState` treats functions as updaters.
353
442
  */
354
443
  interface WebSocketState<S> {
355
- /** The client whose messages this store reduces. Hooks reading the store keep it connected. */
356
- readonly client: WebSocketClient<any, any>;
357
- /** The `initialState` it was created with. Used for server rendering and hydration. */
358
- readonly initialState: S;
359
- /** The current state. Returns the same object until it changes. */
360
- getState(): S;
361
- /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */
362
- subscribe(listener: () => void): () => void;
363
- /** Replace the state, or update it from the current one, e.g. for optimistic updates. */
364
- setState(update: S | ((state: S) => S)): void;
365
- /** Go back to `initialState`. */
366
- reset(): void;
367
- /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */
368
- dispose(): void;
369
- }
370
- /**
371
- * Keep state derived from a client's messages, like a reducer that runs on every message.
372
- * It starts listening immediately and keeps the state across reconnects; it doesn't open
373
- * the connection by itself.
374
- *
375
- * @example
376
- * ```ts
377
- * const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });
378
- *
379
- * export const priceState = createWebSocketState(prices, {
380
- * initialState: {} as Record<string, number>,
381
- * reduce: (state, tick) => ({ ...state, [tick.symbol]: tick.price })
382
- * });
383
- *
384
- * // In a component: re-renders only when ACME's price changes.
385
- * const acme = useWebSocketState(priceState, state => state.ACME);
386
- * ```
387
- */
388
- declare function createWebSocketState<S, In>(client: WebSocketClient<In, any>, options: WebSocketStateOptions<S, NoInfer<In>>): WebSocketState<S>;
389
-
390
- /** Options for {@link createWebSocketTopics}: the messages your server expects. */
391
- interface WebSocketTopicsOptions<Out> {
392
- /** The message that subscribes to `topic`. */
393
- subscribe: (topic: string) => Out;
394
- /** The message that unsubscribes from `topic`. Omit it if your server has none. */
395
- unsubscribe?: (topic: string) => Out;
444
+ /** The client whose messages this store reduces. Hooks reading the store keep it connected. */
445
+ readonly client: WebSocketClient<any, any>;
446
+ /** The `initialState` it was created with. Used for server rendering and hydration. */
447
+ readonly initialState: S;
448
+ /** The current state. Returns the same object until it changes. */
449
+ getState(): S;
450
+ /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */
451
+ subscribe(listener: () => void): () => void;
452
+ /** Replace the state, or update it from the current one, e.g. for optimistic updates. */
453
+ setState(update: S | ((state: S) => S)): void;
454
+ /** Go back to `initialState`. */
455
+ reset(): void;
456
+ /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */
457
+ dispose(): void;
396
458
  }
459
+ //#endregion
460
+ //#region src/topics.d.ts
397
461
  /** Reference-counted topic subscriptions on one client. Create with {@link createWebSocketTopics}. */
398
462
  interface WebSocketTopics {
399
- /** The client the subscription messages are sent on. */
400
- readonly client: WebSocketClient<any, any>;
401
- /**
402
- * Hold a subscription to `topic`, and the client's connection, until the returned
403
- * function is called. The release function is safe to call more than once.
404
- */
405
- acquire(topic: string): () => void;
406
- /** The topics currently held, in the order they were first acquired. */
407
- active(): string[];
408
- /** Stop managing subscriptions. Only needed for managers created dynamically. */
409
- dispose(): void;
463
+ /** The client the subscription messages are sent on. */
464
+ readonly client: WebSocketClient<any, any>;
465
+ /**
466
+ * Hold a subscription to `topic`, and the client's connection, until the returned
467
+ * function is called. The release function is safe to call more than once.
468
+ */
469
+ acquire(topic: string): () => void;
470
+ /** The topics currently held, in the order they were first acquired. */
471
+ active(): string[];
472
+ /** Stop managing subscriptions. Only needed for managers created dynamically. */
473
+ dispose(): void;
410
474
  }
411
- /**
412
- * Manage topic subscriptions that survive reconnects.
413
- *
414
- * - The first holder of a topic sends `subscribe(topic)`. Later holders share it.
415
- * - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode
416
- * and route transitions don't churn subscriptions.
417
- * - After every reconnect, every held topic is subscribed again on the new socket, before
418
- * messages queued while disconnected are sent.
419
- *
420
- * Subscriptions are only sent while the socket is open; nothing is queued for them.
421
- *
422
- * @example
423
- * ```ts
424
- * export const topics = createWebSocketTopics(live, {
425
- * subscribe: topic => ({ type: 'subscribe', topic }),
426
- * unsubscribe: topic => ({ type: 'unsubscribe', topic })
427
- * });
428
- *
429
- * // In a component:
430
- * useWebSocketTopic(topics, `prices:${symbol}`);
431
- * ```
432
- */
433
- declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
434
-
475
+ //#endregion
476
+ //#region src/react.d.ts
435
477
  /**
436
478
  * Per-component event handlers. They always see the component's latest props and
437
479
  * state, and changing them never reconnects.
438
480
  */
439
481
  interface WebSocketHandlers<In> {
440
- /** The socket opened (initially or after a reconnect). */
441
- onOpen?: (event: Event) => void;
442
- /** A message arrived and passed `parse`/`json` and `schema`. */
443
- onMessage?: (message: In, event: MessageEvent) => void;
444
- /** A socket closed. Called whether or not it will be retried. */
445
- onClose?: (close: CloseInfo) => void;
446
- /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */
447
- onError?: (error: Event | Error) => void;
448
- /**
449
- * `false` releases this component's hold on the connection and stops its handlers.
450
- * The connection closes once nothing else holds it.
451
- * @default true
452
- */
453
- enabled?: boolean;
482
+ /** The socket opened (initially or after a reconnect). */
483
+ onOpen?: (event: Event) => void;
484
+ /** A message arrived and passed `parse`/`json` and `schema`. */
485
+ onMessage?: (message: In, event: MessageEvent) => void;
486
+ /** A socket closed. Called whether or not it will be retried. */
487
+ onClose?: (close: CloseInfo) => void;
488
+ /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */
489
+ onError?: (error: Event | Error) => void;
490
+ /**
491
+ * `false` releases this component's hold on the connection and stops its handlers.
492
+ * The connection closes once nothing else holds it.
493
+ * @default true
494
+ */
495
+ enabled?: boolean;
454
496
  }
455
497
  /** Options for `useWebSocket(url, options)`: connection options plus handlers. */
456
- interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {
457
- }
498
+ interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}
458
499
  /** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */
459
500
  interface UseWebSocketResult<In, Out> extends Snapshot<In> {
460
- /** Send now, or queue until open. Returns `true` if sent immediately. Stable identity. */
461
- send: (message: Out) => boolean;
462
- /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
463
- close: (code?: number, reason?: string) => void;
464
- /** Drop the socket and connect again right away. Stable identity. */
465
- reconnect: () => void;
466
- /** The underlying client, or `null` when the URL is `null`/`undefined`. */
467
- client: WebSocketClient<In, Out> | null;
501
+ /**
502
+ * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).
503
+ * Returns `true` if sent immediately. Stable identity.
504
+ */
505
+ send: (message: Out, options?: SendOptions) => boolean;
506
+ /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
507
+ close: (code?: number, reason?: string) => void;
508
+ /** Drop the socket and connect again right away. Stable identity. */
509
+ reconnect: () => void;
510
+ /** The underlying client, or `null` when the URL is `null`/`undefined`. */
511
+ client: WebSocketClient<In, Out> | null;
468
512
  }
469
513
  /**
470
514
  * Connect to a WebSocket and re-render with its status and latest message.
@@ -489,8 +533,8 @@ interface UseWebSocketResult<In, Out> extends Snapshot<In> {
489
533
  * });
490
534
  * ```
491
535
  */
492
- declare function useWebSocket<In = unknown, Out = unknown>(url: string | null | undefined, options?: UseWebSocketOptions<In, Out>): UseWebSocketResult<In, Out>;
493
- declare function useWebSocket<In, Out>(client: WebSocketClient<In, Out> | null | undefined, options?: WebSocketHandlers<In>): UseWebSocketResult<In, Out>;
536
+ export declare function useWebSocket<In = unknown, Out = unknown>(url: string | null | undefined, options?: UseWebSocketOptions<In, Out>): UseWebSocketResult<In, Out>;
537
+ export declare function useWebSocket<In, Out>(client: WebSocketClient<In, Out> | null | undefined, options?: WebSocketHandlers<In>): UseWebSocketResult<In, Out>;
494
538
  /**
495
539
  * Listen to a client's events without re-rendering the component. The listener always
496
540
  * sees the latest props and state. Keeps the connection open while mounted.
@@ -500,7 +544,7 @@ declare function useWebSocket<In, Out>(client: WebSocketClient<In, Out> | null |
500
544
  * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));
501
545
  * ```
502
546
  */
503
- declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: WebSocketClient<In, any> | null | undefined, type: K, listener: EventMap<In>[K]): void;
547
+ export declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: WebSocketClient<In, any> | null | undefined, type: K, listener: EventMap<In>[K]): void;
504
548
  /**
505
549
  * The connection status of a client. Re-renders only when the status changes, not on
506
550
  * messages. Keeps the connection open while mounted.
@@ -510,7 +554,7 @@ declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: Web
510
554
  * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'
511
555
  * ```
512
556
  */
513
- declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status;
557
+ export declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status;
514
558
  /**
515
559
  * Read state built from messages by `createWebSocketState`. Re-renders only when the
516
560
  * selected value changes (by `isEqual`, default `Object.is`), however often messages
@@ -523,8 +567,8 @@ declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | u
523
567
  * const acme = useWebSocketState(priceState, state => state.ACME);
524
568
  * ```
525
569
  */
526
- declare function useWebSocketState<S>(store: WebSocketState<S>): S;
527
- declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (state: S) => T, isEqual?: (a: T, b: T) => boolean): T;
570
+ export declare function useWebSocketState<S>(store: WebSocketState<S>): S;
571
+ export declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (state: S) => T, isEqual?: (a: T, b: T) => boolean): T;
528
572
  /**
529
573
  * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`
530
574
  * manager. Components holding the same topic share one subscription, and it is sent again
@@ -536,6 +580,42 @@ declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (st
536
580
  * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));
537
581
  * ```
538
582
  */
539
- declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, topic: string | readonly string[] | null | undefined): void;
540
-
541
- export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketClient, type WebSocketHandlers, type WebSocketOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketState, createWebSocketTopics, useWebSocket, useWebSocketEvent, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
583
+ export declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, topic: string | readonly string[] | null | undefined): void;
584
+ /**
585
+ * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.
586
+ * Re-renders when a measurement changes, about once per ping interval. Keeps the connection
587
+ * open while mounted. On the server, and during hydration, both values are `undefined`.
588
+ *
589
+ * @example
590
+ * ```tsx
591
+ * const { rtt } = useWebSocketLatency(latency);
592
+ * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;
593
+ * ```
594
+ */
595
+ export declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
596
+ /**
597
+ * Requests for `method` that a person answers, e.g. an agent asking to run a tool. While mounted,
598
+ * it registers `method` on the responder without a handler, so new requests wait here instead of
599
+ * getting a `NotFoundError` reply. Call `respond` or `reject` on one to answer it. Re-renders
600
+ * when the list changes. Keeps the connection open while mounted. `null` holds nothing.
601
+ *
602
+ * Requests stay listed after unmount until they are answered, time out, are cancelled or lose
603
+ * their connection, so a remounted dialog shows them again. On the server the list is empty.
604
+ *
605
+ * @example
606
+ * ```tsx
607
+ * const [request] = useAgentApproval(tools, 'approve');
608
+ * if (!request) return null;
609
+ * return (
610
+ * <dialog open>
611
+ * {request.params.summary}
612
+ * <button onClick={() => request.respond({ approved: true })}>Approve</button>
613
+ * <button onClick={() => request.respond({ approved: false })}>Deny</button>
614
+ * </dialog>
615
+ * );
616
+ * ```
617
+ */
618
+ export declare function useAgentApproval<P, R>(responder: WebSocketResponder<P, R> | null | undefined, method: string | null | undefined): readonly IncomingRequest<P, R>[];
619
+ //#endregion
620
+ export type { UseWebSocketOptions, UseWebSocketResult, WebSocketHandlers };
621
+ //# sourceMappingURL=index.d.ts.map