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/core.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ //#region src/types.d.ts
1
2
  /**
2
3
  * Where to connect: a URL, or a function that returns one (sync or async).
3
4
  *
@@ -24,90 +25,111 @@ type UrlSource = string | (() => string | Promise<string>);
24
25
  type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
25
26
  /** The client's state as seen by React. A new object is created on every change. */
26
27
  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;
28
+ /** Connection state. */
29
+ readonly status: Status;
30
+ /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */
31
+ readonly lastMessage: In | undefined;
32
+ /** Consecutive failed attempts since the last successful open. `0` while open. */
33
+ readonly attempt: number;
33
34
  }
34
35
  /** Details of a closed connection, from the browser's `CloseEvent`. */
35
36
  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;
37
+ /** Close code: `1000` is a normal close, `1006` an abnormal one (e.g. network loss), `4000`–`4999` are yours. */
38
+ readonly code: number;
39
+ /** Close reason sent by the side that closed the connection. Often empty. */
40
+ readonly reason: string;
41
+ /** Whether the closing handshake completed. */
42
+ readonly wasClean: boolean;
42
43
  }
43
44
  /** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */
44
45
  type SendData = Parameters<WebSocket['send']>[0];
45
46
  /** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */
46
47
  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;
48
+ /**
49
+ * Maximum consecutive reconnect attempts. When the last one fails too, the client
50
+ * gives up: the status becomes `closed` and `ready` rejects. `0` never retries.
51
+ * @default Infinity
52
+ */
53
+ maxAttempts?: number;
54
+ /**
55
+ * Delay before the first retry, in ms.
56
+ * @default 1000
57
+ */
58
+ minDelay?: number;
59
+ /**
60
+ * Upper bound for the delay, in ms.
61
+ * @default 30000
62
+ */
63
+ maxDelay?: number;
64
+ /**
65
+ * Backoff multiplier applied per attempt.
66
+ * @default 2
67
+ */
68
+ factor?: number;
69
+ /**
70
+ * Randomly shave up to this fraction (0–1) off each delay, so that many clients
71
+ * dropped at once don't all reconnect at the same moment.
72
+ * @default 0.5
73
+ */
74
+ jitter?: number;
75
+ /**
76
+ * Called on every unexpected close. Return `false` to stop reconnecting.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // Don't retry when the server rejects our credentials.
81
+ * shouldReconnect: close => close.code !== 4001
82
+ * ```
83
+ * @default () => true
84
+ */
85
+ shouldReconnect?: (close: CloseInfo) => boolean;
85
86
  }
86
87
  /** Application-level keep-alive. Useful behind proxies that drop idle connections. */
87
88
  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;
89
+ /** Send `message` every `interval` ms while open. */
90
+ interval: number;
91
+ /**
92
+ * Sent as-is (it is not passed through `serialize` or `json`).
93
+ * @default 'ping'
94
+ */
95
+ message?: string;
96
+ /** Incoming data equal to this string is swallowed instead of being delivered as a message. */
97
+ pong?: string;
98
+ /**
99
+ * If nothing at all arrives within this many ms after a ping, the connection is
100
+ * treated as dead: it closes with code `4000` and reconnects.
101
+ */
102
+ timeout?: number;
103
+ }
104
+ /** Per-message options for `send()`. */
105
+ interface SendOptions {
106
+ /**
107
+ * Drop the message instead of queueing it when the socket isn't open. For data that
108
+ * is stale by the time a reconnect finishes, such as player input or cursor positions.
109
+ * @default false
110
+ */
111
+ volatile?: boolean;
112
+ /**
113
+ * Keep only the newest queued message with this key. Sending again with the same key
114
+ * while disconnected replaces the earlier one, which moves to the back of the queue.
115
+ * For state where only the latest value matters, such as "my position is now (x, y)".
116
+ */
117
+ key?: string;
118
+ /**
119
+ * Called if the message is dropped from the queue without being sent: replaced by a
120
+ * newer message with the same `key`, or evicted by `queue.limit`. Not called when the
121
+ * queue is cleared because the client closed or gave up.
122
+ */
123
+ onDrop?: () => void;
102
124
  }
103
125
  /** Bounds for the send queue. */
104
126
  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;
127
+ /**
128
+ * Maximum number of messages held while not connected. When full, the oldest
129
+ * message is dropped.
130
+ * @default Infinity
131
+ */
132
+ limit?: number;
111
133
  }
112
134
  /**
113
135
  * The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented
@@ -115,24 +137,24 @@ interface QueueOptions {
115
137
  * import this; pass your schema object directly.
116
138
  */
117
139
  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
- };
140
+ readonly '~standard': {
141
+ readonly version: 1;
142
+ readonly vendor: string;
143
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
144
+ readonly types?: {
145
+ readonly input: Input;
146
+ readonly output: Output;
147
+ } | undefined;
148
+ };
127
149
  }
128
150
  /** The result of a Standard Schema `validate` call. */
129
151
  type StandardSchemaResult<Output> = {
130
- readonly value: Output;
131
- readonly issues?: undefined;
152
+ readonly value: Output;
153
+ readonly issues?: undefined;
132
154
  } | {
133
- readonly issues: ReadonlyArray<{
134
- readonly message: string;
135
- }>;
155
+ readonly issues: ReadonlyArray<{
156
+ readonly message: string;
157
+ }>;
136
158
  };
137
159
  /**
138
160
  * Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.
@@ -141,66 +163,66 @@ type StandardSchemaResult<Output> = {
141
163
  * @typeParam Out - Type accepted by `send()`.
142
164
  */
143
165
  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;
166
+ /** Sub-protocol(s) passed to the `WebSocket` constructor. */
167
+ protocols?: string | string[];
168
+ /**
169
+ * How binary messages are exposed.
170
+ * @default 'blob'
171
+ */
172
+ binaryType?: BinaryType;
173
+ /**
174
+ * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail
175
+ * to parse are reported to `onError` as a {@link MessageError} and not delivered.
176
+ * @default false
177
+ */
178
+ json?: boolean;
179
+ /**
180
+ * Custom decoder for incoming `event.data`. Takes precedence over `json`.
181
+ * Throwing reports a {@link MessageError} and drops the message.
182
+ */
183
+ parse?: (data: unknown) => unknown;
184
+ /** Custom encoder for outgoing messages. Takes precedence over `json`. */
185
+ serialize?: (message: Out) => SendData;
186
+ /**
187
+ * Validate incoming messages (after parsing) with any Standard Schema library.
188
+ * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.
189
+ * The message type `In` is inferred from it.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });
194
+ * ```
195
+ */
196
+ schema?: StandardSchemaV1<unknown, In>;
197
+ /**
198
+ * Reconnect with exponential backoff after an unexpected close. `false` disables it.
199
+ * @default true
200
+ */
201
+ reconnect?: boolean | ReconnectOptions;
202
+ /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */
203
+ heartbeat?: HeartbeatOptions;
204
+ /**
205
+ * Buffer `send()` calls made while not connected and send them, in order, on open
206
+ * (right after `open` listeners run).
207
+ * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.
208
+ * The queue is cleared by `close()` or when the client gives up.
209
+ * @default true
210
+ */
211
+ queue?: boolean | QueueOptions;
212
+ /**
213
+ * Receives debug messages about connection attempts, retries and dropped messages.
214
+ * @example
215
+ * ```ts
216
+ * logger: console.debug
217
+ * ```
218
+ */
219
+ logger?: (message: string, ...details: unknown[]) => void;
220
+ /**
221
+ * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.
222
+ * When omitted, the global `WebSocket` is used in browsers. During a server render
223
+ * (no `window`), nothing connects unless an implementation is passed.
224
+ */
225
+ WebSocket?: typeof WebSocket;
204
226
  }
205
227
  /**
206
228
  * Reported via the `error` event (and `onError`) when an incoming message can't be
@@ -213,36 +235,36 @@ interface WebSocketOptions<In = unknown, Out = unknown> {
213
235
  * }
214
236
  * ```
215
237
  */
216
- 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
- });
238
+ export declare class MessageError extends Error {
239
+ /** The raw `event.data` that failed. */
240
+ readonly data: unknown;
241
+ /** Schema validation issues, when the message failed `schema`. */
242
+ readonly issues: ReadonlyArray<{
243
+ readonly message: string;
244
+ }> | undefined;
245
+ constructor(message: string, data: unknown, options?: {
246
+ cause?: unknown;
247
+ issues?: ReadonlyArray<{
248
+ readonly message: string;
249
+ }>;
250
+ });
229
251
  }
230
252
  /** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */
231
253
  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;
254
+ /**
255
+ * The socket opened (initially or after a reconnect). Listeners run before queued
256
+ * messages are sent, so a handshake sent from here goes out first.
257
+ */
258
+ open: (event: Event) => void;
259
+ /** A message arrived and passed `parse` and `schema`. */
260
+ message: (message: In, event: MessageEvent) => void;
261
+ /** A socket closed, whether it will be retried or not. */
262
+ close: (close: CloseInfo) => void;
263
+ /**
264
+ * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,
265
+ * or the `Error` thrown by the `WebSocket` constructor or a `url` function.
266
+ */
267
+ error: (error: Event | MessageError | Error) => void;
246
268
  }
247
269
  /**
248
270
  * A WebSocket connection that reconnects, queues, decodes and can be shared.
@@ -256,64 +278,72 @@ interface EventMap<In> {
256
278
  * @typeParam Out - Type accepted by `send()`.
257
279
  */
258
280
  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;
281
+ /** The URL (or URL function) the client was created with. */
282
+ readonly url: UrlSource;
283
+ /**
284
+ * A promise that resolves once the socket has opened. Reading it starts an idle client.
285
+ *
286
+ * It stays resolved through automatic reconnects, so `use(client.ready)` shows a
287
+ * Suspense fallback only for the first connection, never on a network blip; use
288
+ * `status` to show reconnects. It rejects when the client gives up reconnecting, when
289
+ * `close()` is called, and during a server render. After that, `connect()` or
290
+ * `reconnect()` make it a new pending promise.
291
+ *
292
+ * It is safe to pass to React 19's `use()`: the same promise is returned until its state
293
+ * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is
294
+ * already settled.
295
+ */
296
+ readonly ready: Promise<void>;
297
+ /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */
298
+ readonly socket: WebSocket | null;
299
+ /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */
300
+ subscribe(listener: () => void): () => void;
301
+ /** The current {@link Snapshot}. Returns the same object until something changes. */
302
+ getSnapshot(): Snapshot<In>;
303
+ /**
304
+ * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown
305
+ * asynchronously, so one failing listener doesn't break the others.
306
+ */
307
+ on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
308
+ /**
309
+ * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
310
+ * open, the message is queued and sent on open (see the `queue` option), unless
311
+ * `options.volatile` is set.
312
+ * @returns `true` if it was sent immediately, `false` if it was queued or dropped.
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
317
+ * game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
318
+ * ```
319
+ */
320
+ send(message: Out, options?: SendOptions): boolean;
321
+ /**
322
+ * Start connecting, and keep the connection open even when no hook is using the
323
+ * client, until `close()` is called. Does nothing if already connecting or open.
324
+ */
325
+ connect(): void;
326
+ /**
327
+ * Close the connection and stop reconnecting. The status becomes `closed` and the
328
+ * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`
329
+ * to start again.
330
+ * @param code - Close code. @default 1000
331
+ */
332
+ close(code?: number, reason?: string): void;
333
+ /**
334
+ * Drop the current socket (if any) and connect again immediately, resetting the
335
+ * backoff. Also restarts a `closed` client.
336
+ */
337
+ reconnect(): void;
338
+ /**
339
+ * Hold the connection open until the returned function is called. This is what the
340
+ * hooks use; call it yourself to integrate with non-React code.
341
+ * @returns A release function (safe to call more than once).
342
+ */
343
+ acquire(): () => void;
315
344
  }
316
-
345
+ //#endregion
346
+ //#region src/client.d.ts
317
347
  /**
318
348
  * Create a WebSocket client, usually at module level, to share across components.
319
349
  *
@@ -333,17 +363,302 @@ interface WebSocketClient<In = unknown, Out = unknown> {
333
363
  * export const chat = createWebSocket<ChatIn, ChatOut>('wss://api.example.com/chat', { json: true });
334
364
  * ```
335
365
  */
336
- declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
337
-
366
+ export declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
367
+ //#endregion
368
+ //#region src/latency.d.ts
369
+ /** Options for {@link createWebSocketLatency}: how pings and pongs look on the wire. */
370
+ interface WebSocketLatencyOptions<In, Out> {
371
+ /** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */
372
+ ping: (id: number) => Out;
373
+ /**
374
+ * Recognize a pong: return the `id` of the ping it answers, and the server's clock
375
+ * (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)
376
+ * for any other message. Without `time`, only the round-trip time is measured.
377
+ */
378
+ pong: (message: In) => {
379
+ id: number | string;
380
+ time?: number;
381
+ } | null | undefined;
382
+ /**
383
+ * How often to ping while open, in ms. The first ping goes out as soon as the socket opens.
384
+ * @default 2000
385
+ */
386
+ interval?: number;
387
+ /**
388
+ * How many recent measurements to keep. `rtt` is their median, and `offset` comes from
389
+ * the one with the lowest round trip, the most accurate estimate.
390
+ * @default 5
391
+ */
392
+ samples?: number;
393
+ }
394
+ /** Latest latency measurements. A new object is created on every change. */
395
+ interface Latency {
396
+ /** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
397
+ readonly rtt: number | undefined;
398
+ /**
399
+ * Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
400
+ * `undefined` until a pong carries `time`.
401
+ */
402
+ readonly offset: number | undefined;
403
+ }
404
+ /** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */
405
+ interface WebSocketLatency {
406
+ /** The client being measured. Hooks reading the latency keep it connected. */
407
+ readonly client: WebSocketClient<any, any>;
408
+ /** The current {@link Latency}. Returns the same object until it changes. */
409
+ getSnapshot(): Latency;
410
+ /** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
411
+ subscribe(listener: () => void): () => void;
412
+ /** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
413
+ serverNow(): number;
414
+ /** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
415
+ ping(): void;
416
+ /** Stop pinging and listening. Only needed for instances created dynamically. */
417
+ dispose(): void;
418
+ }
419
+ /**
420
+ * Measure round-trip time and estimate the server's clock, for ping displays, countdowns
421
+ * and timestamps that must agree across players.
422
+ *
423
+ * Pings are sent on an interval while the socket is open, and paused otherwise. Measurements
424
+ * from a previous connection are discarded on reconnect, but the last values stay readable
425
+ * until new ones arrive. It doesn't open the connection by itself.
426
+ *
427
+ * Pongs are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * export const latency = createWebSocketLatency(game, {
432
+ * ping: id => ({ type: 'ping', id }),
433
+ * pong: message => (message.type === 'pong' ? { id: message.id, time: message.time } : undefined)
434
+ * });
435
+ *
436
+ * // In a component:
437
+ * const { rtt } = useWebSocketLatency(latency);
438
+ *
439
+ * // Anywhere: seconds left in a round that ends at the server's `endsAt`.
440
+ * const left = (endsAt - latency.serverNow()) / 1000;
441
+ * ```
442
+ */
443
+ export declare function createWebSocketLatency<In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>): WebSocketLatency;
444
+ //#endregion
445
+ //#region src/requests.d.ts
446
+ /** Options for {@link createWebSocketRequests}: how requests and replies look on the wire. */
447
+ interface WebSocketRequestsOptions<Req, In, Out> {
448
+ /**
449
+ * Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`
450
+ * to type what `request()` accepts.
451
+ */
452
+ send: (message: Req, id: number) => Out;
453
+ /**
454
+ * The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.
455
+ * A string id matches the number it was sent as, so either echo works.
456
+ */
457
+ reply: (message: In) => number | string | null | undefined;
458
+ /**
459
+ * Called on a reply. Return something other than `undefined` to reject the request with
460
+ * it instead of resolving, e.g. for a server-side error reply.
461
+ */
462
+ error?: (message: In) => unknown;
463
+ /**
464
+ * Default time to wait for a reply, in ms. `Infinity` waits forever.
465
+ * @default 10000
466
+ */
467
+ timeout?: number;
468
+ }
469
+ /** Per-call options for {@link WebSocketRequests.request}. */
470
+ interface RequestOptions extends SendOptions {
471
+ /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
472
+ timeout?: number;
473
+ /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
474
+ signal?: AbortSignal;
475
+ }
476
+ /** Request/response over one client. Create with {@link createWebSocketRequests}. */
477
+ interface WebSocketRequests<Req, In> {
478
+ /** The client requests are sent on. */
479
+ readonly client: WebSocketClient<any, any>;
480
+ /**
481
+ * Send a request and resolve with its reply. Holds the connection until it settles.
482
+ *
483
+ * Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
484
+ * aborted, and with an `Error` when the connection it was sent on closes (the reply can
485
+ * no longer arrive) or the client stops. A request made while disconnected is queued like
486
+ * any other message and waits for the connection; it rejects if it is dropped from the
487
+ * queue (replaced by a newer request with the same `key`, or evicted by `queue.limit`).
488
+ */
489
+ request(message: Req, options?: RequestOptions): Promise<In>;
490
+ /** Number of requests waiting for a reply. */
491
+ pending(): number;
492
+ /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
493
+ dispose(): void;
494
+ }
495
+ /**
496
+ * Send requests and await their replies, matched by an id the library generates.
497
+ *
498
+ * Replies are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * type Request = { type: 'join'; room: string } | { type: 'leave' };
503
+ *
504
+ * export const rpc = createWebSocketRequests(game, {
505
+ * send: (message: Request, id) => ({ ...message, id }),
506
+ * reply: message => (message.type === 'reply' ? message.id : undefined),
507
+ * error: message => (message.type === 'reply' && message.error ? new Error(message.error) : undefined)
508
+ * });
509
+ *
510
+ * const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });
511
+ * ```
512
+ */
513
+ export declare function createWebSocketRequests<Req, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>): WebSocketRequests<Req, In>;
514
+ //#endregion
515
+ //#region src/responder.d.ts
516
+ /** What `request` returns for a request: any object with these fields, such as the message itself. */
517
+ type RequestFields = {
518
+ id: number | string;
519
+ method: string;
520
+ params?: unknown;
521
+ timeout?: number;
522
+ };
523
+ /**
524
+ * The type of `params` in `Q`, keeping `undefined` when they're optional and giving `undefined`
525
+ * when there are none. (Inferring `params?: P` directly would drop the `undefined`.)
526
+ */
527
+ type ParamsOf<Q> = Q extends unknown ? 'params' extends keyof Q ? Q['params' & keyof Q] : undefined : never;
528
+ /**
529
+ * Options for {@link createWebSocketResponder}: how the server's requests and your replies look
530
+ * on the wire.
531
+ *
532
+ * @typeParam Q - What `request` returns. Handlers receive its `params`.
533
+ * @typeParam R - What handlers return, from the annotation on `respond`'s `result`.
534
+ */
535
+ interface WebSocketResponderOptions<Q extends RequestFields, R, In, Out> {
536
+ /**
537
+ * Recognize a request from the server: its `id`, the `method` handlers are registered for,
538
+ * and the `params` they receive. Return `undefined` (or `null`) for any other message.
539
+ * A `timeout` (in ms) overrides the manager's for this request.
540
+ *
541
+ * `params` come from the server, so validate them before acting on them.
542
+ */
543
+ request: (message: In) => Q | null | undefined;
544
+ /** Build the reply to request `id`. Annotate `result` to type what handlers return. */
545
+ respond: (id: number | string, result: R) => Out;
546
+ /**
547
+ * Build the error reply to request `id`: what a handler threw, a `NotFoundError` `DOMException`
548
+ * when nothing is registered for the method, a `TimeoutError` after `timeout`, or an
549
+ * `AbortError` from `dispose()`.
550
+ */
551
+ error: (id: number | string, error: unknown) => Out;
552
+ /**
553
+ * The id of a request the server withdrew, or `undefined` (or `null`) if the message isn't a
554
+ * cancellation. The request's `signal` aborts and nothing is sent.
555
+ */
556
+ cancel?: (message: In) => number | string | null | undefined;
557
+ /**
558
+ * Time to answer a request, in ms. When it runs out, the `error` reply (a `TimeoutError`)
559
+ * is sent and the request's `signal` aborts.
560
+ * @default Infinity
561
+ */
562
+ timeout?: number;
563
+ }
564
+ /** A request from the server that hasn't been answered yet. */
565
+ interface IncomingRequest<P = unknown, R = unknown> {
566
+ /** The id the server sent. Replies carry it back. */
567
+ readonly id: number | string;
568
+ /** The method it was sent for. */
569
+ readonly method: string;
570
+ /** The `params` from `request`. They come from the server, so validate them. */
571
+ readonly params: P;
572
+ /**
573
+ * Aborts when the answer is no longer wanted: the server cancelled the request, it timed out,
574
+ * the connection it arrived on closed, the responder was disposed, or it was answered with
575
+ * `respond` or `reject` while its handler was still running.
576
+ */
577
+ readonly signal: AbortSignal;
578
+ /**
579
+ * Reply with `result`. Only the first answer counts.
580
+ * @returns `true` if the reply was sent, `false` if the request was already answered or is gone.
581
+ */
582
+ respond(result: R): boolean;
583
+ /**
584
+ * Reply with an error, built by the `error` option. Only the first answer counts.
585
+ * @returns `true` if the reply was sent, `false` if the request was already answered or is gone.
586
+ */
587
+ reject(error: unknown): boolean;
588
+ }
589
+ /**
590
+ * Answers a request in code. Return (or resolve) the result to reply with it; throw (or reject)
591
+ * to send the `error` reply instead.
592
+ */
593
+ type RequestHandler<P = unknown, R = unknown> = (params: P, request: IncomingRequest<P, R>) => R | PromiseLike<R>;
594
+ /** Answers the server's requests on one client. Create with {@link createWebSocketResponder}. */
595
+ interface WebSocketResponder<P = unknown, R = unknown> {
596
+ /** The client requests arrive on. Hooks reading the responder keep it connected. */
597
+ readonly client: WebSocketClient<any, any>;
598
+ /**
599
+ * Answer requests for `method` until the returned function is called.
600
+ *
601
+ * With a `handler`, requests are answered in code. Without one, they wait in
602
+ * {@link getSnapshot} until something calls `respond` or `reject` on them, e.g. a dialog
603
+ * that asks the user. When several registrations exist for one method, the newest one takes
604
+ * new requests, and unregistering it hands them back to the previous one. Requests for a
605
+ * method with no registration get a `NotFoundError` reply.
606
+ *
607
+ * @returns An unregister function (safe to call more than once). Requests already waiting stay.
608
+ */
609
+ onRequest(method: string, handler?: RequestHandler<P, R>): () => void;
610
+ /**
611
+ * Requests waiting for an answer from outside a handler, oldest first. Returns the same array
612
+ * until it changes.
613
+ */
614
+ getSnapshot(): readonly IncomingRequest<P, R>[];
615
+ /** Called whenever {@link getSnapshot} changes. Compatible with `useSyncExternalStore`. */
616
+ subscribe(listener: () => void): () => void;
617
+ /**
618
+ * Stop answering: send an `AbortError` reply to every request in flight and stop listening.
619
+ * Only needed for responders created dynamically.
620
+ */
621
+ dispose(): void;
622
+ }
623
+ /**
624
+ * Answer requests the server sends: run a handler for tools that only exist in the browser
625
+ * (selection, clipboard, geolocation), or let a person answer, e.g. to approve an agent's action.
626
+ * The reverse of `createWebSocketRequests`.
627
+ *
628
+ * Every request gets exactly one reply while its connection lives: the handler's result, an
629
+ * `error` reply when it throws, has no registration or times out. When the connection a request
630
+ * arrived on closes, the request is dropped and nothing is sent, since the server can't match a
631
+ * reply on a new connection. Replies are never queued.
632
+ *
633
+ * Requests are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
634
+ *
635
+ * @example
636
+ * ```ts
637
+ * export const tools = createWebSocketResponder(agent, {
638
+ * request: message => (message.type === 'call' ? message : undefined),
639
+ * respond: (id, result: unknown) => ({ type: 'result', id, result }),
640
+ * error: (id, error) => ({ type: 'result', id, error: String(error) })
641
+ * });
642
+ *
643
+ * tools.onRequest('get_selection', () => window.getSelection()?.toString() ?? '');
644
+ *
645
+ * // In a component: requests a person answers.
646
+ * const approvals = useAgentApproval(tools, 'approve');
647
+ * approvals[0]?.respond({ approved: true });
648
+ * ```
649
+ */
650
+ 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>;
651
+ //#endregion
652
+ //#region src/state.d.ts
338
653
  /** Options for {@link createWebSocketState}. */
339
654
  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;
655
+ /** The state before any message arrives, and what `reset()` returns to. */
656
+ initialState: S;
657
+ /**
658
+ * Fold one incoming message into the state. Must be pure: return a new object for a
659
+ * change, or the same `state` to ignore the message (subscribers aren't notified).
660
+ */
661
+ reduce: (state: S, message: In) => S;
347
662
  }
348
663
  /**
349
664
  * State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
@@ -352,20 +667,20 @@ interface WebSocketStateOptions<S, In> {
352
667
  * @typeParam S - The state type. Don't use a function type: `setState` treats functions as updaters.
353
668
  */
354
669
  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;
670
+ /** The client whose messages this store reduces. Hooks reading the store keep it connected. */
671
+ readonly client: WebSocketClient<any, any>;
672
+ /** The `initialState` it was created with. Used for server rendering and hydration. */
673
+ readonly initialState: S;
674
+ /** The current state. Returns the same object until it changes. */
675
+ getState(): S;
676
+ /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */
677
+ subscribe(listener: () => void): () => void;
678
+ /** Replace the state, or update it from the current one, e.g. for optimistic updates. */
679
+ setState(update: S | ((state: S) => S)): void;
680
+ /** Go back to `initialState`. */
681
+ reset(): void;
682
+ /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */
683
+ dispose(): void;
369
684
  }
370
685
  /**
371
686
  * Keep state derived from a client's messages, like a reducer that runs on every message.
@@ -385,28 +700,29 @@ interface WebSocketState<S> {
385
700
  * const acme = useWebSocketState(priceState, state => state.ACME);
386
701
  * ```
387
702
  */
388
- declare function createWebSocketState<S, In>(client: WebSocketClient<In, any>, options: WebSocketStateOptions<S, NoInfer<In>>): WebSocketState<S>;
389
-
703
+ export declare function createWebSocketState<S, In>(client: WebSocketClient<In, any>, options: WebSocketStateOptions<S, NoInfer<In>>): WebSocketState<S>;
704
+ //#endregion
705
+ //#region src/topics.d.ts
390
706
  /** Options for {@link createWebSocketTopics}: the messages your server expects. */
391
707
  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;
708
+ /** The message that subscribes to `topic`. */
709
+ subscribe: (topic: string) => Out;
710
+ /** The message that unsubscribes from `topic`. Omit it if your server has none. */
711
+ unsubscribe?: (topic: string) => Out;
396
712
  }
397
713
  /** Reference-counted topic subscriptions on one client. Create with {@link createWebSocketTopics}. */
398
714
  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;
715
+ /** The client the subscription messages are sent on. */
716
+ readonly client: WebSocketClient<any, any>;
717
+ /**
718
+ * Hold a subscription to `topic`, and the client's connection, until the returned
719
+ * function is called. The release function is safe to call more than once.
720
+ */
721
+ acquire(topic: string): () => void;
722
+ /** The topics currently held, in the order they were first acquired. */
723
+ active(): string[];
724
+ /** Stop managing subscriptions. Only needed for managers created dynamically. */
725
+ dispose(): void;
410
726
  }
411
727
  /**
412
728
  * Manage topic subscriptions that survive reconnects.
@@ -430,6 +746,7 @@ interface WebSocketTopics {
430
746
  * useWebSocketTopic(topics, `prices:${symbol}`);
431
747
  * ```
432
748
  */
433
- declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
434
-
435
- export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketState, createWebSocketTopics };
749
+ export declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
750
+ //#endregion
751
+ export type { CloseInfo, EventMap, HeartbeatOptions, IncomingRequest, Latency, QueueOptions, ReconnectOptions, RequestHandler, RequestOptions, SendData, SendOptions, Snapshot, StandardSchemaV1, Status, UrlSource, WebSocketClient, WebSocketLatency, WebSocketLatencyOptions, WebSocketOptions, WebSocketRequests, WebSocketRequestsOptions, WebSocketResponder, WebSocketResponderOptions, WebSocketState, WebSocketStateOptions, WebSocketTopics, WebSocketTopicsOptions };
752
+ //# sourceMappingURL=core.d.cts.map