react-websocket 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -49
- package/dist/core.cjs +497 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +338 -0
- package/dist/core.d.ts +338 -0
- package/dist/core.js +469 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +129 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +418 -0
- package/dist/index.d.ts +418 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/package.json +112 -27
- package/build/index.js +0 -1344
- package/build/index.map +0 -1
- package/index.jsx +0 -106
- package/server.js +0 -18
- package/webpack.config.js +0 -35
- package/webpack.prod.config.js +0 -37
package/dist/core.d.cts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where to connect: a URL, or a function that returns one (sync or async).
|
|
3
|
+
*
|
|
4
|
+
* A function is called before **every** connection attempt, including reconnects,
|
|
5
|
+
* so it can put a fresh auth token in the URL. If it throws or rejects, that counts
|
|
6
|
+
* as a failed attempt and is retried with backoff.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* createWebSocket(async () => `wss://api.example.com/live?token=${await getToken()}`);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
type UrlSource = string | (() => string | Promise<string>);
|
|
14
|
+
/**
|
|
15
|
+
* Connection state.
|
|
16
|
+
*
|
|
17
|
+
* - `idle`: not connected and not trying to be. Nobody is using the client yet, or everyone stopped.
|
|
18
|
+
* - `connecting`: the first attempt is in progress.
|
|
19
|
+
* - `open`: connected. `send()` goes out immediately.
|
|
20
|
+
* - `reconnecting`: the connection dropped (or an attempt failed) and a retry is scheduled or in progress.
|
|
21
|
+
* - `closed`: stopped for good, by `close()` or after giving up on reconnecting.
|
|
22
|
+
* It stays closed until `connect()` or `reconnect()` is called, or every hook unmounts.
|
|
23
|
+
*/
|
|
24
|
+
type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
|
|
25
|
+
/** The client's state as seen by React. A new object is created on every change. */
|
|
26
|
+
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;
|
|
33
|
+
}
|
|
34
|
+
/** Details of a closed connection, from the browser's `CloseEvent`. */
|
|
35
|
+
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;
|
|
42
|
+
}
|
|
43
|
+
/** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */
|
|
44
|
+
type SendData = Parameters<WebSocket['send']>[0];
|
|
45
|
+
/** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */
|
|
46
|
+
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;
|
|
85
|
+
}
|
|
86
|
+
/** Application-level keep-alive. Useful behind proxies that drop idle connections. */
|
|
87
|
+
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;
|
|
102
|
+
}
|
|
103
|
+
/** Bounds for the send queue. */
|
|
104
|
+
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;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented
|
|
114
|
+
* by zod (3.24+), valibot (1.0+), arktype (2.0+) and others. You don't need to
|
|
115
|
+
* import this; pass your schema object directly.
|
|
116
|
+
*/
|
|
117
|
+
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
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** The result of a Standard Schema `validate` call. */
|
|
129
|
+
type StandardSchemaResult<Output> = {
|
|
130
|
+
readonly value: Output;
|
|
131
|
+
readonly issues?: undefined;
|
|
132
|
+
} | {
|
|
133
|
+
readonly issues: ReadonlyArray<{
|
|
134
|
+
readonly message: string;
|
|
135
|
+
}>;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.
|
|
139
|
+
*
|
|
140
|
+
* @typeParam In - Type of incoming messages (after `parse`/`json`/`schema`).
|
|
141
|
+
* @typeParam Out - Type accepted by `send()`.
|
|
142
|
+
*/
|
|
143
|
+
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;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Reported via the `error` event (and `onError`) when an incoming message can't be
|
|
207
|
+
* decoded by `parse`/`json` or fails `schema` validation. The message is not delivered.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* onError(error) {
|
|
212
|
+
* if (error instanceof MessageError) report(error.data, error.issues);
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
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
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */
|
|
231
|
+
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;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A WebSocket connection that reconnects, queues, decodes and can be shared.
|
|
249
|
+
* Create one with `createWebSocket()`.
|
|
250
|
+
*
|
|
251
|
+
* The connection is reference-counted: it opens when the first hook mounts (or
|
|
252
|
+
* `ready` is read) and closes one tick after the last hook unmounts. Calling
|
|
253
|
+
* `connect()` keeps it open regardless, until `close()`.
|
|
254
|
+
*
|
|
255
|
+
* @typeParam In - Type of incoming messages.
|
|
256
|
+
* @typeParam Out - Type accepted by `send()`.
|
|
257
|
+
*/
|
|
258
|
+
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;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
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
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
|
|
337
|
+
|
|
338
|
+
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, createWebSocket };
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where to connect: a URL, or a function that returns one (sync or async).
|
|
3
|
+
*
|
|
4
|
+
* A function is called before **every** connection attempt, including reconnects,
|
|
5
|
+
* so it can put a fresh auth token in the URL. If it throws or rejects, that counts
|
|
6
|
+
* as a failed attempt and is retried with backoff.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* createWebSocket(async () => `wss://api.example.com/live?token=${await getToken()}`);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
type UrlSource = string | (() => string | Promise<string>);
|
|
14
|
+
/**
|
|
15
|
+
* Connection state.
|
|
16
|
+
*
|
|
17
|
+
* - `idle`: not connected and not trying to be. Nobody is using the client yet, or everyone stopped.
|
|
18
|
+
* - `connecting`: the first attempt is in progress.
|
|
19
|
+
* - `open`: connected. `send()` goes out immediately.
|
|
20
|
+
* - `reconnecting`: the connection dropped (or an attempt failed) and a retry is scheduled or in progress.
|
|
21
|
+
* - `closed`: stopped for good, by `close()` or after giving up on reconnecting.
|
|
22
|
+
* It stays closed until `connect()` or `reconnect()` is called, or every hook unmounts.
|
|
23
|
+
*/
|
|
24
|
+
type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
|
|
25
|
+
/** The client's state as seen by React. A new object is created on every change. */
|
|
26
|
+
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;
|
|
33
|
+
}
|
|
34
|
+
/** Details of a closed connection, from the browser's `CloseEvent`. */
|
|
35
|
+
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;
|
|
42
|
+
}
|
|
43
|
+
/** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */
|
|
44
|
+
type SendData = Parameters<WebSocket['send']>[0];
|
|
45
|
+
/** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */
|
|
46
|
+
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;
|
|
85
|
+
}
|
|
86
|
+
/** Application-level keep-alive. Useful behind proxies that drop idle connections. */
|
|
87
|
+
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;
|
|
102
|
+
}
|
|
103
|
+
/** Bounds for the send queue. */
|
|
104
|
+
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;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented
|
|
114
|
+
* by zod (3.24+), valibot (1.0+), arktype (2.0+) and others. You don't need to
|
|
115
|
+
* import this; pass your schema object directly.
|
|
116
|
+
*/
|
|
117
|
+
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
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** The result of a Standard Schema `validate` call. */
|
|
129
|
+
type StandardSchemaResult<Output> = {
|
|
130
|
+
readonly value: Output;
|
|
131
|
+
readonly issues?: undefined;
|
|
132
|
+
} | {
|
|
133
|
+
readonly issues: ReadonlyArray<{
|
|
134
|
+
readonly message: string;
|
|
135
|
+
}>;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.
|
|
139
|
+
*
|
|
140
|
+
* @typeParam In - Type of incoming messages (after `parse`/`json`/`schema`).
|
|
141
|
+
* @typeParam Out - Type accepted by `send()`.
|
|
142
|
+
*/
|
|
143
|
+
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;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Reported via the `error` event (and `onError`) when an incoming message can't be
|
|
207
|
+
* decoded by `parse`/`json` or fails `schema` validation. The message is not delivered.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* onError(error) {
|
|
212
|
+
* if (error instanceof MessageError) report(error.data, error.issues);
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
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
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */
|
|
231
|
+
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;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A WebSocket connection that reconnects, queues, decodes and can be shared.
|
|
249
|
+
* Create one with `createWebSocket()`.
|
|
250
|
+
*
|
|
251
|
+
* The connection is reference-counted: it opens when the first hook mounts (or
|
|
252
|
+
* `ready` is read) and closes one tick after the last hook unmounts. Calling
|
|
253
|
+
* `connect()` keeps it open regardless, until `close()`.
|
|
254
|
+
*
|
|
255
|
+
* @typeParam In - Type of incoming messages.
|
|
256
|
+
* @typeParam Out - Type accepted by `send()`.
|
|
257
|
+
*/
|
|
258
|
+
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;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
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
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
|
|
337
|
+
|
|
338
|
+
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, createWebSocket };
|