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 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: .5
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" ? Math.max(0, options.queue.limit ?? Infinity) : Infinity;
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
- function stopHeartbeat() {
200
+ /** Stop the timers that belong to the current open socket. */
201
+ function stopSocketTimers() {
181
202
  clearInterval(heartbeatTimer);
182
203
  clearTimeout(pongTimer);
183
- heartbeatTimer = pongTimer = void 0;
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
- stopHeartbeat();
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 && snapshot.attempt < retry.maxAttempts && (retry.shouldReconnect?.(info) ?? true)) {
285
- const attempt = snapshot.attempt + 1;
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({ status: "closed" });
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
- stopHeartbeat();
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
- stopHeartbeat();
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