react-websocket 3.6.0 → 4.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 +9 -4
- package/dist/core.cjs +39 -10
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +42 -9
- package/dist/core.d.ts +42 -9
- package/dist/core.js +39 -11
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +39 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -10
- package/dist/index.d.ts +30 -10
- package/dist/index.js +40 -4
- package/dist/index.js.map +1 -1
- package/dist/timeline.d.cts +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
**[Documentation](https://mehmetkose.github.io/react-websocket/)** · [Getting started](https://mehmetkose.github.io/react-websocket/guide/getting-started) · [API](https://mehmetkose.github.io/react-websocket/api/) · [Recipes](https://mehmetkose.github.io/react-websocket/recipes) · [Changelog](CHANGELOG.md)
|
|
13
13
|
|
|
14
14
|
- **Shared connections.** Components using the same URL share one socket. StrictMode, `<Activity>` and route transitions never open duplicates.
|
|
15
|
-
- **Auto-reconnect.** Exponential backoff with jitter, plus an immediate retry when the browser comes back online or the tab becomes visible. Optional heartbeats detect dead links.
|
|
15
|
+
- **Auto-reconnect.** Exponential backoff with full jitter, plus an immediate retry when the browser comes back online or the tab becomes visible. It backs off from a server that closes right after accepting, and stops on close codes that retrying can't fix. Optional heartbeats detect dead links.
|
|
16
16
|
- **Send queue.** `send()` before the socket is open just works. The message goes out on connect. Mark input `volatile` so it's never replayed late, or `key` it so only the newest is sent. Put control messages first with `priority`, and drop stream data on a slow link with `maxBuffered`.
|
|
17
17
|
- **Typed messages.** `json: true`, your own codec, or validation with any [Standard Schema](https://standardschema.dev) library (zod, valibot, arktype), with inferred types.
|
|
18
18
|
- **Suspense.** `use(client.ready)` waits for the first connection, and reconnects never flash a fallback.
|
|
@@ -126,6 +126,7 @@ function Room() {
|
|
|
126
126
|
| `createWebSocketSendRate(client, options?)` | `send(message, key)` coalesced per key at a rate that drops on a congested link |
|
|
127
127
|
| `createWebSocketResponder(client, { request, respond, error })` | Answer the server's requests: `onRequest(method, handler)`, or a person through `useAgentApproval` |
|
|
128
128
|
| `MessageError` | Reported to `onError` when a message fails `parse` or `schema` |
|
|
129
|
+
| `retryableClose(close)` | The default `reconnect.shouldReconnect`: retries every close except `1002`, `1003` and `1008` |
|
|
129
130
|
| `createWebSocketTimeline(client, { snapshot, interpolate, input })` | From `react-websocket/timeline`: interpolated server snapshots (`at()`) and predicted inputs (`predicted()`, `input()`) for multiplayer games |
|
|
130
131
|
| `createChaosWebSocket(options)` | From `react-websocket/chaos`: a `WebSocket` class that adds latency, lost messages and dropped connections in development |
|
|
131
132
|
|
|
@@ -136,9 +137,9 @@ function Room() {
|
|
|
136
137
|
| `json` | `false` | `JSON.parse` incoming and `JSON.stringify` outgoing messages |
|
|
137
138
|
| `parse(data)` / `serialize(message)` | | Custom codec; takes precedence over `json` |
|
|
138
139
|
| `schema` | | Standard Schema validation; types are inferred |
|
|
139
|
-
| `reconnect` | `true` | `false`, or `{ maxAttempts, minDelay, maxDelay, factor, jitter, shouldReconnect }` |
|
|
140
|
+
| `reconnect` | `true` | `false`, or `{ maxAttempts, minDelay, maxDelay, factor, jitter, minUptime, shouldReconnect }` |
|
|
140
141
|
| `heartbeat` | | `{ interval, message, pong, timeout }` |
|
|
141
|
-
| `queue` | `true` | `false`, or `{ limit }` |
|
|
142
|
+
| `queue` | `true` (100 messages) | `false`, or `{ limit }` |
|
|
142
143
|
| `logger` | | e.g. `console.debug` |
|
|
143
144
|
| `WebSocket` | global | Custom implementation for tests, Node, or chaos testing |
|
|
144
145
|
|
|
@@ -149,7 +150,7 @@ function Room() {
|
|
|
149
150
|
- [Shared clients](https://mehmetkose.github.io/react-websocket/guide/shared-clients) · [Messages and types](https://mehmetkose.github.io/react-websocket/guide/messages) · [Reconnecting and heartbeats](https://mehmetkose.github.io/react-websocket/guide/reconnecting)
|
|
150
151
|
- [Suspense](https://mehmetkose.github.io/react-websocket/guide/suspense) · [Server rendering and Next.js](https://mehmetkose.github.io/react-websocket/guide/ssr) · [StrictMode, Activity and lifecycle](https://mehmetkose.github.io/react-websocket/guide/lifecycle)
|
|
151
152
|
- [Authentication](https://mehmetkose.github.io/react-websocket/guide/authentication) · [Performance](https://mehmetkose.github.io/react-websocket/guide/performance) · [Real-time games](https://mehmetkose.github.io/react-websocket/guide/games) · [AI agents](https://mehmetkose.github.io/react-websocket/guide/agents) · [Testing](https://mehmetkose.github.io/react-websocket/guide/testing) · [Chaos testing](https://mehmetkose.github.io/react-websocket/guide/chaos) · [Without React](https://mehmetkose.github.io/react-websocket/guide/without-react)
|
|
152
|
-
- [Recipes](https://mehmetkose.github.io/react-websocket/recipes) · [Troubleshooting](https://mehmetkose.github.io/react-websocket/troubleshooting) · [Migrating from v2](https://mehmetkose.github.io/react-websocket/migration)
|
|
153
|
+
- [Recipes](https://mehmetkose.github.io/react-websocket/recipes) · [Troubleshooting](https://mehmetkose.github.io/react-websocket/troubleshooting) · [Migrating from v3](https://mehmetkose.github.io/react-websocket/migration-v3) · [Migrating from v2](https://mehmetkose.github.io/react-websocket/migration)
|
|
153
154
|
|
|
154
155
|
The docs source lives in [`docs/`](docs). Run `npm run docs:dev` to browse it locally.
|
|
155
156
|
|
|
@@ -161,6 +162,10 @@ The docs source lives in [`docs/`](docs). Run `npm run docs:dev` to browse it lo
|
|
|
161
162
|
| Runtimes | Modern browsers; Node via the `WebSocket` option |
|
|
162
163
|
| Frameworks | Vite, Next.js (App and Pages Router), Remix/React Router, Astro islands, and any React setup |
|
|
163
164
|
|
|
165
|
+
## Migrating from v3
|
|
166
|
+
|
|
167
|
+
v4 changes no APIs, only four defaults: the backoff resets once a connection has stayed open for 5 seconds, close codes `1002`, `1003` and `1008` aren't retried, jitter is full, and the send queue holds 100 messages. Each has a one-line opt-out. See [Migrating from v3](https://mehmetkose.github.io/react-websocket/migration-v3).
|
|
168
|
+
|
|
164
169
|
## Migrating from v2
|
|
165
170
|
|
|
166
171
|
v2 was a `<Websocket>` class component. v3 replaces it with hooks: `<Websocket url onMessage />` becomes `useWebSocket(url, { onMessage })`, and `ref.sendMessage(msg)` becomes `send(msg)`. See the [migration guide](https://mehmetkose.github.io/react-websocket/migration) for the full mapping.
|
package/dist/core.cjs
CHANGED
|
@@ -31,8 +31,26 @@ const RECONNECT_DEFAULTS = {
|
|
|
31
31
|
minDelay: 1e3,
|
|
32
32
|
maxDelay: 3e4,
|
|
33
33
|
factor: 2,
|
|
34
|
-
jitter:
|
|
34
|
+
jitter: 1,
|
|
35
|
+
minUptime: 5e3
|
|
35
36
|
};
|
|
37
|
+
const QUEUE_LIMIT = 100;
|
|
38
|
+
/**
|
|
39
|
+
* The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
|
|
40
|
+
* `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
|
|
41
|
+
* retrying the same way can't fix. Call it from your own `shouldReconnect` to keep that rule.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* createWebSocket(url, {
|
|
46
|
+
* // Also stop on our server's "invalid token" code.
|
|
47
|
+
* reconnect: { shouldReconnect: close => close.code !== 4001 && retryableClose(close) }
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function retryableClose(close) {
|
|
52
|
+
return close.code !== 1002 && close.code !== 1003 && close.code !== 1008;
|
|
53
|
+
}
|
|
36
54
|
const READY_GRACE = 5e3;
|
|
37
55
|
function createDeferred() {
|
|
38
56
|
let resolve;
|
|
@@ -84,7 +102,7 @@ function createClient(url, options = {}) {
|
|
|
84
102
|
...RECONNECT_DEFAULTS,
|
|
85
103
|
...typeof options.reconnect === "object" ? options.reconnect : {}
|
|
86
104
|
};
|
|
87
|
-
const queueLimit = options.queue === false ? 0 : typeof options.queue === "object" ?
|
|
105
|
+
const queueLimit = options.queue === false ? 0 : Math.max(0, (typeof options.queue === "object" ? options.queue.limit : void 0) ?? QUEUE_LIMIT);
|
|
88
106
|
let snapshot = {
|
|
89
107
|
status: "idle",
|
|
90
108
|
lastMessage: void 0,
|
|
@@ -109,8 +127,10 @@ function createClient(url, options = {}) {
|
|
|
109
127
|
let inbox = null;
|
|
110
128
|
let generation = 0;
|
|
111
129
|
let windowBound = false;
|
|
130
|
+
let failures = 0;
|
|
112
131
|
let retryTimer;
|
|
113
132
|
let teardownTimer;
|
|
133
|
+
let stableTimer;
|
|
114
134
|
let heartbeatTimer;
|
|
115
135
|
let pongTimer;
|
|
116
136
|
function set(patch) {
|
|
@@ -177,10 +197,12 @@ function createClient(url, options = {}) {
|
|
|
177
197
|
}, heartbeat.timeout);
|
|
178
198
|
}, heartbeat.interval);
|
|
179
199
|
}
|
|
180
|
-
|
|
200
|
+
/** Stop the timers that belong to the current open socket. */
|
|
201
|
+
function stopSocketTimers() {
|
|
181
202
|
clearInterval(heartbeatTimer);
|
|
182
203
|
clearTimeout(pongTimer);
|
|
183
|
-
|
|
204
|
+
clearTimeout(stableTimer);
|
|
205
|
+
heartbeatTimer = pongTimer = stableTimer = void 0;
|
|
184
206
|
}
|
|
185
207
|
/** Stop tracking `socket`. Its eventual close event is still reported unless `reportClose` is false. */
|
|
186
208
|
function detach(socket, reportClose) {
|
|
@@ -251,6 +273,7 @@ function createClient(url, options = {}) {
|
|
|
251
273
|
status: "open",
|
|
252
274
|
attempt: 0
|
|
253
275
|
});
|
|
276
|
+
if (retry) stableTimer = setTimeout(() => failures = 0, retry.minUptime);
|
|
254
277
|
startHeartbeat(socket);
|
|
255
278
|
ready?.resolve();
|
|
256
279
|
if (refs === 0 && !pinned) {
|
|
@@ -275,14 +298,14 @@ function createClient(url, options = {}) {
|
|
|
275
298
|
};
|
|
276
299
|
}
|
|
277
300
|
function handleClose(info) {
|
|
278
|
-
|
|
301
|
+
stopSocketTimers();
|
|
279
302
|
log("closed", info);
|
|
280
303
|
retryOrFail(info);
|
|
281
304
|
emit("close", info);
|
|
282
305
|
}
|
|
283
306
|
function retryOrFail(info, error) {
|
|
284
|
-
if (wanted && retry &&
|
|
285
|
-
const attempt =
|
|
307
|
+
if (wanted && retry && failures < retry.maxAttempts && (retry.shouldReconnect ?? retryableClose)(info)) {
|
|
308
|
+
const attempt = ++failures;
|
|
286
309
|
const delay = backoff(attempt);
|
|
287
310
|
set({
|
|
288
311
|
status: "reconnecting",
|
|
@@ -297,7 +320,10 @@ function createClient(url, options = {}) {
|
|
|
297
320
|
opened = false;
|
|
298
321
|
unbindWindow();
|
|
299
322
|
pending = [];
|
|
300
|
-
set({
|
|
323
|
+
set({
|
|
324
|
+
status: "closed",
|
|
325
|
+
attempt: failures
|
|
326
|
+
});
|
|
301
327
|
rejectReady(reason);
|
|
302
328
|
}
|
|
303
329
|
function receive(event, socket) {
|
|
@@ -402,6 +428,7 @@ function createClient(url, options = {}) {
|
|
|
402
428
|
opened = false;
|
|
403
429
|
if (ready?.settled) ready = null;
|
|
404
430
|
}
|
|
431
|
+
failures = 0;
|
|
405
432
|
snapshot = {
|
|
406
433
|
...snapshot,
|
|
407
434
|
attempt: 0
|
|
@@ -423,7 +450,8 @@ function createClient(url, options = {}) {
|
|
|
423
450
|
generation++;
|
|
424
451
|
clearTimeout(retryTimer);
|
|
425
452
|
retryTimer = void 0;
|
|
426
|
-
|
|
453
|
+
failures = 0;
|
|
454
|
+
stopSocketTimers();
|
|
427
455
|
unbindWindow();
|
|
428
456
|
pending = [];
|
|
429
457
|
if (ws) {
|
|
@@ -446,7 +474,7 @@ function createClient(url, options = {}) {
|
|
|
446
474
|
if (!getImpl()) return;
|
|
447
475
|
clearTimeout(retryTimer);
|
|
448
476
|
retryTimer = void 0;
|
|
449
|
-
|
|
477
|
+
stopSocketTimers();
|
|
450
478
|
if (ws) {
|
|
451
479
|
const socket = ws;
|
|
452
480
|
detach(socket, true);
|
|
@@ -1232,5 +1260,6 @@ exports.createWebSocketResponder = createWebSocketResponder;
|
|
|
1232
1260
|
exports.createWebSocketSendRate = createWebSocketSendRate;
|
|
1233
1261
|
exports.createWebSocketState = createWebSocketState;
|
|
1234
1262
|
exports.createWebSocketTopics = createWebSocketTopics;
|
|
1263
|
+
exports.retryableClose = retryableClose;
|
|
1235
1264
|
|
|
1236
1265
|
//# sourceMappingURL=core.cjs.map
|