react-websocket 3.3.0 → 3.6.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
@@ -13,12 +13,13 @@
13
13
 
14
14
  - **Shared connections.** Components using the same URL share one socket. StrictMode, `<Activity>` and route transitions never open duplicates.
15
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.
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.
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.
19
19
  - **Topic subscriptions.** Reference-counted `subscribe`/`unsubscribe` messages that are sent again after every reconnect, so feeds don't silently stop.
20
- - **Message state.** Reduce messages into a store, and let each component re-render only when its slice changes.
21
- - **Requests and latency.** Await replies with timeouts and cancellation, and measure round-trip time and the server's clock for ping displays and synchronized countdowns.
20
+ - **Message state.** Reduce messages into a store, and let each component re-render only when its slice changes, at most once per frame if you like.
21
+ - **Requests, streams and latency.** Await a reply, or iterate over streamed ones such as an LLM's tokens, with timeouts and a cancel that tells the server to stop. Measure round-trip time and the server's clock for ping displays and synchronized countdowns.
22
+ - **Games.** Snapshot interpolation and client-side prediction with server reconciliation, from `react-websocket/timeline`, so other players move smoothly and your own controls respond at once. A send rate that adapts to the connection keeps players on a poor link from building up lag.
22
23
  - **Server requests.** Answer requests from the server, such as an AI agent's, in code for browser-only tools, or from a dialog for human approval. Every request gets exactly one reply, and none is sent to the wrong connection.
23
24
  - **SSR-safe.** Never connects during a server render, hydrates cleanly, and ships a `'use client'` entry for Next.js and other Server Components frameworks.
24
25
  - **Small and typed.** Zero dependencies, ESM + CJS, TypeScript declarations with inline docs, plus a React-free `react-websocket/core` entry.
@@ -116,13 +117,17 @@ function Room() {
116
117
  | `useWebSocketState(store, selector?)` | Read state reduced from messages; re-renders only when the selection changes |
117
118
  | `useWebSocketLatency(latency)` | Round-trip time and server clock offset |
118
119
  | `useAgentApproval(responder, method)` | Requests from the server waiting for a person to answer |
120
+ | `useWebSocketStream(rpc, options?)` | Stream the replies to a request into a value, e.g. tokens into an answer; returns `{ value, status, error, start, abort }` |
119
121
  | `createWebSocket(url, options?)` | A shareable client: `send`, `on`, `connect`, `close`, `reconnect`, `ready`, `getSnapshot`, `subscribe` |
120
122
  | `createWebSocketTopics(client, { subscribe, unsubscribe })` | Topic subscription manager for `useWebSocketTopic` |
121
123
  | `createWebSocketState(client, { initialState, reduce })` | Message-state store for `useWebSocketState` |
122
- | `createWebSocketRequests(client, { send, reply })` | `request(message)` returns a promise of the reply, with timeouts |
124
+ | `createWebSocketRequests(client, { send, reply })` | `request(message)` returns a promise of the reply, and `stream(message)` an async iterator of replies, with timeouts and `cancel` |
123
125
  | `createWebSocketLatency(client, { ping, pong })` | Ping tracker for `useWebSocketLatency`, plus `serverNow()` |
126
+ | `createWebSocketSendRate(client, options?)` | `send(message, key)` coalesced per key at a rate that drops on a congested link |
124
127
  | `createWebSocketResponder(client, { request, respond, error })` | Answer the server's requests: `onRequest(method, handler)`, or a person through `useAgentApproval` |
125
128
  | `MessageError` | Reported to `onError` when a message fails `parse` or `schema` |
129
+ | `createWebSocketTimeline(client, { snapshot, interpolate, input })` | From `react-websocket/timeline`: interpolated server snapshots (`at()`) and predicted inputs (`predicted()`, `input()`) for multiplayer games |
130
+ | `createChaosWebSocket(options)` | From `react-websocket/chaos`: a `WebSocket` class that adds latency, lost messages and dropped connections in development |
126
131
 
127
132
  | Option | Default | |
128
133
  | --- | --- | --- |
@@ -135,7 +140,7 @@ function Room() {
135
140
  | `heartbeat` | | `{ interval, message, pong, timeout }` |
136
141
  | `queue` | `true` | `false`, or `{ limit }` |
137
142
  | `logger` | | e.g. `console.debug` |
138
- | `WebSocket` | global | Custom implementation for tests or Node |
143
+ | `WebSocket` | global | Custom implementation for tests, Node, or chaos testing |
139
144
 
140
145
  `useWebSocket` also takes `onOpen`, `onMessage`, `onClose`, `onError` and `enabled`. The [API reference](https://mehmetkose.github.io/react-websocket/api/) documents everything.
141
146
 
@@ -143,7 +148,7 @@ function Room() {
143
148
 
144
149
  - [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)
145
150
  - [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)
146
- - [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) · [Without React](https://mehmetkose.github.io/react-websocket/guide/without-react)
151
+ - [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)
147
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)
148
153
 
149
154
  The docs source lives in [`docs/`](docs). Run `npm run docs:dev` to browse it locally.
@@ -163,6 +168,10 @@ v2 was a `<Websocket>` class component. v3 replaces it with hooks: `<Websocket u
163
168
  ## Examples
164
169
 
165
170
  - [`examples/nextjs`](examples/nextjs) is a demo of every feature in a Next.js App Router app, with its own WebSocket server. Run `npm run example:nextjs` and open http://localhost:3000.
171
+ - [`examples/astro`](examples/astro) is the same demo of every feature as Astro islands sharing one connection, with a page that keeps it open across navigation. Run `npm run example:astro` and open http://localhost:4321.
172
+ - [`examples/remix`](examples/remix) is the same demo in a Remix 3 app, which doesn't use React: every card uses `react-websocket/core` through small bindings that stand in for the hooks. Run `npm run example:remix` and open http://localhost:44100.
173
+ - [`examples/hono`](examples/hono) is a chat room with presence: a Hono server on Node.js and a Vite + React 19 client sharing one set of message types. Run `npm run example:hono` and open http://localhost:5173.
174
+ - [`examples/game`](examples/game) is a multiplayer game: a Node.js server with a fixed tick and a Vite + React canvas client, with a panel that adds lag and packet loss so you can see interpolation and prediction at work. Run `npm run example:game` and open http://localhost:5180.
166
175
  - [`examples/tornado`](examples/tornado) is a small broadcast chat: a Tornado server and a Vite + React 19 client.
167
176
 
168
177
  ## Contributing
package/dist/chaos.cjs ADDED
@@ -0,0 +1,284 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/chaos.ts
3
+ const CONNECTING = 0;
4
+ const OPEN = 1;
5
+ const CLOSING = 2;
6
+ const CLOSED = 3;
7
+ function mulberry32(seed) {
8
+ return () => {
9
+ seed = seed + 1831565813 | 0;
10
+ let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
11
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
12
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
13
+ };
14
+ }
15
+ function pick(range, random) {
16
+ return typeof range === "number" ? range : range[0] + (range[1] - range[0]) * random();
17
+ }
18
+ function size(data) {
19
+ return new Blob([data]).size;
20
+ }
21
+ /** Cut `data` short at a random point. Text also gets U+FFFD, which no JSON document ends with. */
22
+ function garble(data, random) {
23
+ if (typeof data === "string") return data.slice(0, Math.floor(random() * data.length)) + "�";
24
+ const binary = data;
25
+ if (typeof binary?.slice !== "function") return "�";
26
+ const length = binary.size ?? data.byteLength;
27
+ return binary.slice(0, Math.floor(random() * length));
28
+ }
29
+ function closeEvent(code, reason, wasClean) {
30
+ const init = {
31
+ code,
32
+ reason,
33
+ wasClean
34
+ };
35
+ return typeof CloseEvent === "function" ? new CloseEvent("close", init) : Object.assign(new Event("close"), init);
36
+ }
37
+ /**
38
+ * Events delayed in order: each runs in its own task, no earlier than the one before it,
39
+ * because a WebSocket never reorders. With no delay and nothing waiting, it runs right away.
40
+ */
41
+ function createQueue() {
42
+ let items = [];
43
+ let timer;
44
+ function next() {
45
+ const head = items[0];
46
+ if (timer !== void 0 || !head) return;
47
+ timer = setTimeout(() => {
48
+ timer = void 0;
49
+ try {
50
+ items.shift().run();
51
+ } finally {
52
+ next();
53
+ }
54
+ }, head.at - Date.now());
55
+ }
56
+ return {
57
+ push(delay, run, data) {
58
+ if (delay <= 0 && !items.length) return run();
59
+ items.push({
60
+ at: Math.max(Date.now() + delay, items.at(-1)?.at ?? 0),
61
+ run,
62
+ data
63
+ });
64
+ next();
65
+ },
66
+ clear() {
67
+ items = [];
68
+ clearTimeout(timer);
69
+ timer = void 0;
70
+ },
71
+ bytes: () => items.reduce((sum, item) => sum + (item.data === void 0 ? 0 : size(item.data)), 0)
72
+ };
73
+ }
74
+ /**
75
+ * Create a `WebSocket` class that injects network faults: latency, lost and corrupt messages,
76
+ * refused, dropped and silently stalled connections. Pass it as the `WebSocket` option in
77
+ * development to see how your app behaves on a bad network.
78
+ *
79
+ * Each call returns a new class with its own faults, controls and random sequence.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { createWebSocket } from 'react-websocket';
84
+ * import { createChaosWebSocket } from 'react-websocket/chaos';
85
+ *
86
+ * export const Chaos = createChaosWebSocket({ latency: [50, 400], drop: [10_000, 30_000] });
87
+ *
88
+ * export const live = createWebSocket('wss://api.example.com/live', {
89
+ * WebSocket: import.meta.env.DEV ? Chaos : undefined
90
+ * });
91
+ *
92
+ * Chaos.stall(); // later, from a dev panel or the console
93
+ * ```
94
+ */
95
+ function createChaosWebSocket(options = {}) {
96
+ const { WebSocket: wrapped, seed, logger, ...initial } = options;
97
+ const log = logger ?? (() => {});
98
+ let faults = initial;
99
+ const live = /* @__PURE__ */ new Set();
100
+ let count = 0;
101
+ const enabled = () => faults.enabled !== false;
102
+ const chance = (probability, random) => enabled() && !!probability && random() < probability;
103
+ const delay = (random) => enabled() && faults.latency ? pick(faults.latency, random) : 0;
104
+ const loss = (direction) => typeof faults.loss === "object" ? faults.loss[direction] : faults.loss;
105
+ const stream = (n, k) => seed === void 0 ? Math.random : mulberry32(seed + Math.imul(3 * n + k, 2654435769));
106
+ class ChaosSocket extends EventTarget {
107
+ static CONNECTING = CONNECTING;
108
+ static OPEN = OPEN;
109
+ static CLOSING = CLOSING;
110
+ static CLOSED = CLOSED;
111
+ CONNECTING = CONNECTING;
112
+ OPEN = OPEN;
113
+ CLOSING = CLOSING;
114
+ CLOSED = CLOSED;
115
+ url;
116
+ onopen = null;
117
+ onmessage = null;
118
+ onerror = null;
119
+ onclose = null;
120
+ #state = CONNECTING;
121
+ #inner;
122
+ #binaryType = "blob";
123
+ #stalled = false;
124
+ #unsent = 0;
125
+ #life;
126
+ #in;
127
+ #out;
128
+ #incoming = createQueue();
129
+ #outgoing = createQueue();
130
+ #timers = [];
131
+ static configure(next) {
132
+ faults = {
133
+ ...faults,
134
+ ...next
135
+ };
136
+ if (!("drop" in next || "stall" in next || "enabled" in next)) return;
137
+ for (const socket of [...live]) if (socket.#state === OPEN) socket.#arm();
138
+ }
139
+ static drop(code = 1006, reason = "") {
140
+ for (const socket of [...live]) socket.#drop(code, reason);
141
+ }
142
+ static stall() {
143
+ for (const socket of [...live]) socket.#stall();
144
+ }
145
+ static inject(data) {
146
+ for (const socket of [...live]) if (socket.#state === OPEN) socket.#receive(() => socket.#message(data));
147
+ }
148
+ constructor(url, protocols) {
149
+ super();
150
+ const Impl = wrapped ?? (typeof window === "undefined" ? void 0 : globalThis.WebSocket);
151
+ if (!Impl) throw new Error("react-websocket/chaos: no WebSocket to wrap here (server render?)");
152
+ for (const type of [
153
+ "open",
154
+ "message",
155
+ "error",
156
+ "close"
157
+ ]) this.addEventListener(type, (event) => this[`on${type}`]?.call(this, event));
158
+ const n = ++count;
159
+ this.#life = stream(n, 0);
160
+ this.#in = stream(n, 1);
161
+ this.#out = stream(n, 2);
162
+ if (chance(faults.refuse, this.#life)) {
163
+ log("chaos: refused", String(url));
164
+ this.url = String(url);
165
+ live.add(this);
166
+ setTimeout(() => this.#end(1006, "", false, true), delay(this.#life));
167
+ return;
168
+ }
169
+ const inner = new Impl(url, protocols);
170
+ this.#inner = inner;
171
+ this.url = inner.url;
172
+ live.add(this);
173
+ inner.onopen = () => this.#receive(() => {
174
+ if (this.#state !== CONNECTING) return;
175
+ this.#state = OPEN;
176
+ this.#arm();
177
+ this.dispatchEvent(new Event("open"));
178
+ }, this.#life);
179
+ inner.onmessage = (event) => {
180
+ if (this.#state === CLOSED || this.#stalled) return;
181
+ if (chance(loss("in"), this.#in)) return log("chaos: lost an incoming message", event.data);
182
+ let data = event.data;
183
+ if (chance(faults.corrupt, this.#in)) {
184
+ data = garble(data, this.#in);
185
+ log("chaos: corrupted a message", event.data);
186
+ }
187
+ this.#receive(() => this.#message(data));
188
+ };
189
+ inner.onerror = () => this.#receive(() => this.dispatchEvent(new Event("error")));
190
+ inner.onclose = (event) => this.#receive(() => this.#end(event.code, event.reason, event.wasClean));
191
+ }
192
+ get readyState() {
193
+ return this.#state;
194
+ }
195
+ get binaryType() {
196
+ return this.#inner?.binaryType ?? this.#binaryType;
197
+ }
198
+ set binaryType(value) {
199
+ this.#binaryType = value;
200
+ if (this.#inner) this.#inner.binaryType = value;
201
+ }
202
+ get bufferedAmount() {
203
+ return (this.#inner?.bufferedAmount ?? 0) + this.#outgoing.bytes() + this.#unsent;
204
+ }
205
+ get protocol() {
206
+ return this.#inner?.protocol ?? "";
207
+ }
208
+ get extensions() {
209
+ return this.#inner?.extensions ?? "";
210
+ }
211
+ send(data) {
212
+ if (this.#state === CONNECTING) throw new DOMException("WebSocket is still in CONNECTING state", "InvalidStateError");
213
+ if (this.#state !== OPEN) return;
214
+ if (this.#stalled) {
215
+ this.#unsent += size(data);
216
+ return;
217
+ }
218
+ if (chance(loss("out"), this.#out)) return log("chaos: lost an outgoing message", data);
219
+ const inner = this.#inner;
220
+ this.#outgoing.push(delay(this.#out), () => {
221
+ if (inner.readyState === OPEN) inner.send(data);
222
+ }, data);
223
+ }
224
+ close(code, reason) {
225
+ if (code !== void 0 && code !== 1e3 && !(code >= 3e3 && code <= 4999)) throw new DOMException(`Invalid close code ${code}`, "InvalidAccessError");
226
+ if (this.#state >= CLOSING) return;
227
+ const connecting = this.#state === CONNECTING;
228
+ this.#state = CLOSING;
229
+ const inner = this.#inner;
230
+ if (inner && !this.#stalled) {
231
+ this.#outgoing.push(delay(this.#out), () => inner.close(code, reason));
232
+ return;
233
+ }
234
+ inner?.close(code, reason);
235
+ setTimeout(() => this.#end(1006, "", false, connecting), 0);
236
+ }
237
+ /** Queue an event from the server, unless the connection is gone or silent. */
238
+ #receive(run, random = this.#in) {
239
+ if (this.#state === CLOSED || this.#stalled) return;
240
+ this.#incoming.push(delay(random), () => {
241
+ if (!this.#stalled) run();
242
+ });
243
+ }
244
+ #message(data) {
245
+ if (this.#state === OPEN) this.dispatchEvent(new MessageEvent("message", { data }));
246
+ }
247
+ /** (Re)start the `drop` and `stall` timers. */
248
+ #arm() {
249
+ for (const timer of this.#timers) clearTimeout(timer);
250
+ this.#timers = [];
251
+ if (!enabled()) return;
252
+ const { drop, stall } = faults;
253
+ if (drop !== void 0 && drop !== false) this.#timers.push(setTimeout(() => this.#drop(1006, ""), pick(drop, this.#life)));
254
+ if (stall !== void 0 && stall !== false) this.#timers.push(setTimeout(() => this.#stall(), pick(stall, this.#life)));
255
+ }
256
+ #drop(code, reason) {
257
+ log("chaos: dropped", this.url);
258
+ this.#end(code, reason, code !== 1006, code === 1006);
259
+ }
260
+ #stall() {
261
+ if (this.#state !== OPEN || this.#stalled) return;
262
+ this.#stalled = true;
263
+ log("chaos: stalled", this.url);
264
+ }
265
+ /** The only way to CLOSED, so it happens once. */
266
+ #end(code, reason, wasClean, error = false) {
267
+ if (this.#state === CLOSED) return;
268
+ this.#state = CLOSED;
269
+ live.delete(this);
270
+ this.#incoming.clear();
271
+ this.#outgoing.clear();
272
+ for (const timer of this.#timers) clearTimeout(timer);
273
+ const inner = this.#inner;
274
+ if (inner && inner.readyState < CLOSING) inner.close(4999, "chaos");
275
+ if (error) this.dispatchEvent(new Event("error"));
276
+ this.dispatchEvent(closeEvent(code, reason, wasClean));
277
+ }
278
+ }
279
+ return ChaosSocket;
280
+ }
281
+ //#endregion
282
+ exports.createChaosWebSocket = createChaosWebSocket;
283
+
284
+ //# sourceMappingURL=chaos.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chaos.cjs","names":[],"sources":["../src/chaos.ts"],"sourcesContent":["/** A duration in ms, or `[min, max]` for a uniformly random one each time it's used. */\nexport type ChaosRange = number | readonly [min: number, max: number];\n\n/** Faults to inject. All are off by default. Change them on a running class with `configure()`. */\nexport interface ChaosFaults {\n /**\n * Turn the random faults off without forgetting them. The controls (`drop()`, `stall()`,\n * `inject()`) work either way.\n * @default true\n */\n enabled?: boolean;\n /**\n * One-way delay in ms, added to the handshake (`open`), to every incoming message and to every\n * send. Order is preserved, as on a real connection. A round trip takes about twice this.\n * @default 0\n * @example\n * ```ts\n * latency: [50, 400]\n * ```\n */\n latency?: ChaosRange;\n /**\n * Probability (0–1) that a message is lost. A number applies to both directions.\n * @default 0\n * @example\n * ```ts\n * loss: { in: 0.05 } // lose 5% of incoming messages, none of the sends\n * ```\n */\n loss?: number | { in?: number; out?: number };\n /**\n * Probability (0–1) that a connection attempt fails before opening: `error`, then `close`\n * with code `1006`. The server never sees the attempt.\n * @default 0\n */\n refuse?: number;\n /**\n * Each open connection drops after a lifetime in this range, in ms: `error`, then `close` with\n * code `1006`, as when the network goes away.\n * @default false\n * @example\n * ```ts\n * drop: [10_000, 30_000]\n * ```\n */\n drop?: ChaosRange | false;\n /**\n * Each open connection goes silent after a lifetime in this range, in ms, like a socket left\n * half-open by a sleeping laptop: nothing arrives, sends vanish, `readyState` stays `OPEN`, and\n * no `close` event fires. Only a heartbeat timeout (or `close()`) gets out of it.\n * @default false\n */\n stall?: ChaosRange | false;\n /**\n * Probability (0–1) that an incoming message is cut short at a random point. Text also gets a\n * U+FFFD appended, so it never parses as JSON.\n * @default 0\n */\n corrupt?: number;\n}\n\n/** Options for {@link createChaosWebSocket}. */\nexport interface ChaosOptions extends ChaosFaults {\n /**\n * The `WebSocket` implementation to wrap, e.g. a mock in tests or the `ws` package in Node.\n * @default the global `WebSocket`, in browsers only\n */\n WebSocket?: typeof WebSocket;\n /**\n * Seed the random faults, so a run can be repeated. Each connection gets its own sequence, so\n * \"the 3rd connection is refused\" holds however much traffic the others carried.\n * @default unseeded (`Math.random`)\n */\n seed?: number;\n /**\n * Receives a message for every injected fault.\n * @example\n * ```ts\n * logger: console.warn\n * ```\n */\n logger?: (message: string, ...details: unknown[]) => void;\n}\n\n/**\n * Inject faults by hand into every connection made by one chaos class: from a dev panel, a\n * keyboard shortcut, the console or a test.\n */\nexport interface ChaosControls {\n /** Change faults while running, merged into the current ones. Open connections restart their `drop` and `stall` timers. */\n configure(faults: ChaosFaults): void;\n /**\n * Drop every connection now: `error`, then `close` with code `1006`. Pass a code to simulate the\n * server closing instead, e.g. `drop(4001, 'unauthorized')`: a clean close with no `error`.\n */\n drop(code?: number, reason?: string): void;\n /** Make every open connection go silent (see the `stall` option). */\n stall(): void;\n /**\n * Deliver `data` to every open connection as if the server had sent it, after any `latency`.\n * It is never lost or corrupted. Use it to try messages your server doesn't send.\n */\n inject(data: unknown): void;\n}\n\n/** A `WebSocket` class to pass as the `WebSocket` option, with {@link ChaosControls} as static methods. */\nexport type ChaosWebSocket = typeof WebSocket & ChaosControls;\n\ntype Data = Parameters<WebSocket['send']>[0];\ntype Timer = ReturnType<typeof setTimeout>;\n\nconst CONNECTING = 0;\nconst OPEN = 1;\nconst CLOSING = 2;\nconst CLOSED = 3;\n\nfunction mulberry32(seed: number) {\n return () => {\n seed = (seed + 0x6d2b79f5) | 0;\n let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n}\n\nfunction pick(range: ChaosRange, random: () => number) {\n return typeof range === 'number' ? range : range[0] + (range[1] - range[0]) * random();\n}\n\nfunction size(data: Data) {\n return new Blob([data as BlobPart]).size;\n}\n\n/** Cut `data` short at a random point. Text also gets U+FFFD, which no JSON document ends with. */\nfunction garble(data: unknown, random: () => number): unknown {\n if (typeof data === 'string') return data.slice(0, Math.floor(random() * data.length)) + '�';\n const binary = data as { slice?: (start: number, end: number) => unknown } & Partial<Blob>;\n if (typeof binary?.slice !== 'function') return '�';\n const length = binary.size ?? (data as ArrayBuffer).byteLength;\n return binary.slice(0, Math.floor(random() * length));\n}\n\nfunction closeEvent(code: number, reason: string, wasClean: boolean): Event {\n const init = { code, reason, wasClean };\n // Node 22 has no CloseEvent.\n return typeof CloseEvent === 'function'\n ? new CloseEvent('close', init)\n : Object.assign(new Event('close'), init);\n}\n\n/**\n * Events delayed in order: each runs in its own task, no earlier than the one before it,\n * because a WebSocket never reorders. With no delay and nothing waiting, it runs right away.\n */\nfunction createQueue() {\n let items: { at: number; run: () => void; data?: Data }[] = [];\n let timer: Timer | undefined;\n function next() {\n const head = items[0];\n if (timer !== undefined || !head) return;\n timer = setTimeout(() => {\n timer = undefined;\n try {\n items.shift()!.run();\n } finally {\n next();\n }\n }, head.at - Date.now());\n }\n return {\n push(delay: number, run: () => void, data?: Data) {\n if (delay <= 0 && !items.length) return run();\n items.push({ at: Math.max(Date.now() + delay, items.at(-1)?.at ?? 0), run, data });\n next();\n },\n clear() {\n items = [];\n clearTimeout(timer);\n timer = undefined;\n },\n bytes: () =>\n items.reduce((sum, item) => sum + (item.data === undefined ? 0 : size(item.data)), 0)\n };\n}\n\n/**\n * Create a `WebSocket` class that injects network faults: latency, lost and corrupt messages,\n * refused, dropped and silently stalled connections. Pass it as the `WebSocket` option in\n * development to see how your app behaves on a bad network.\n *\n * Each call returns a new class with its own faults, controls and random sequence.\n *\n * @example\n * ```ts\n * import { createWebSocket } from 'react-websocket';\n * import { createChaosWebSocket } from 'react-websocket/chaos';\n *\n * export const Chaos = createChaosWebSocket({ latency: [50, 400], drop: [10_000, 30_000] });\n *\n * export const live = createWebSocket('wss://api.example.com/live', {\n * WebSocket: import.meta.env.DEV ? Chaos : undefined\n * });\n *\n * Chaos.stall(); // later, from a dev panel or the console\n * ```\n */\nexport function createChaosWebSocket(options: ChaosOptions = {}): ChaosWebSocket {\n const { WebSocket: wrapped, seed, logger, ...initial } = options;\n const log = logger ?? (() => {});\n let faults: ChaosFaults = initial;\n const live = new Set<ChaosSocket>();\n let count = 0;\n\n const enabled = () => faults.enabled !== false;\n const chance = (probability: number | undefined, random: () => number) =>\n enabled() && !!probability && random() < probability;\n const delay = (random: () => number) =>\n enabled() && faults.latency ? pick(faults.latency, random) : 0;\n const loss = (direction: 'in' | 'out') =>\n typeof faults.loss === 'object' ? faults.loss[direction] : faults.loss;\n // Independent sequences per connection and purpose, so one connection's traffic doesn't shift\n // the faults of the next.\n const stream = (n: number, k: number) =>\n seed === undefined ? Math.random : mulberry32(seed + Math.imul(3 * n + k, 0x9e3779b9));\n\n class ChaosSocket extends EventTarget implements WebSocket {\n static readonly CONNECTING = CONNECTING;\n static readonly OPEN = OPEN;\n static readonly CLOSING = CLOSING;\n static readonly CLOSED = CLOSED;\n readonly CONNECTING = CONNECTING;\n readonly OPEN = OPEN;\n readonly CLOSING = CLOSING;\n readonly CLOSED = CLOSED;\n\n readonly url: string;\n onopen: WebSocket['onopen'] = null;\n onmessage: WebSocket['onmessage'] = null;\n onerror: WebSocket['onerror'] = null;\n onclose: WebSocket['onclose'] = null;\n\n #state: WebSocket['readyState'] = CONNECTING;\n #inner: WebSocket | undefined;\n #binaryType: BinaryType = 'blob';\n #stalled = false;\n // Bytes sent into a stall: they never leave, like data stuck in a dead connection's buffer.\n #unsent = 0;\n #life: () => number;\n #in: () => number;\n #out: () => number;\n #incoming = createQueue();\n #outgoing = createQueue();\n #timers: Timer[] = [];\n\n static configure(next: ChaosFaults) {\n faults = { ...faults, ...next };\n if (!('drop' in next || 'stall' in next || 'enabled' in next)) return;\n for (const socket of [...live]) if (socket.#state === OPEN) socket.#arm();\n }\n\n static drop(code = 1006, reason = '') {\n // A copy: a close listener may connect again, and the new socket must survive.\n for (const socket of [...live]) socket.#drop(code, reason);\n }\n\n static stall() {\n for (const socket of [...live]) socket.#stall();\n }\n\n static inject(data: unknown) {\n for (const socket of [...live]) {\n if (socket.#state === OPEN) socket.#receive(() => socket.#message(data));\n }\n }\n\n constructor(url: string | URL, protocols?: string | string[]) {\n super();\n const Impl = wrapped ?? (typeof window === 'undefined' ? undefined : globalThis.WebSocket);\n if (!Impl) {\n throw new Error('react-websocket/chaos: no WebSocket to wrap here (server render?)');\n }\n for (const type of ['open', 'message', 'error', 'close'] as const) {\n this.addEventListener(type, event =>\n (this[`on${type}`] as ((event: Event) => void) | null)?.call(this, event)\n );\n }\n const n = ++count;\n this.#life = stream(n, 0);\n this.#in = stream(n, 1);\n this.#out = stream(n, 2);\n\n if (chance(faults.refuse, this.#life)) {\n log('chaos: refused', String(url));\n this.url = String(url);\n live.add(this);\n // Never synchronously: the caller attaches its handlers after construction.\n setTimeout(() => this.#end(1006, '', false, true), delay(this.#life));\n return;\n }\n\n const inner = new Impl(url, protocols);\n this.#inner = inner;\n this.url = inner.url;\n live.add(this);\n // Never set back to null: the `ws` package throws an unhandled error without an error handler.\n inner.onopen = () =>\n this.#receive(() => {\n if (this.#state !== CONNECTING) return;\n this.#state = OPEN;\n this.#arm();\n this.dispatchEvent(new Event('open'));\n }, this.#life);\n inner.onmessage = event => {\n if (this.#state === CLOSED || this.#stalled) return;\n if (chance(loss('in'), this.#in)) return log('chaos: lost an incoming message', event.data);\n let data: unknown = event.data;\n if (chance(faults.corrupt, this.#in)) {\n data = garble(data, this.#in);\n log('chaos: corrupted a message', event.data);\n }\n this.#receive(() => this.#message(data));\n };\n inner.onerror = () => this.#receive(() => this.dispatchEvent(new Event('error')));\n inner.onclose = event =>\n this.#receive(() => this.#end(event.code, event.reason, event.wasClean));\n }\n\n get readyState() {\n return this.#state;\n }\n\n get binaryType() {\n return this.#inner?.binaryType ?? this.#binaryType;\n }\n\n set binaryType(value) {\n this.#binaryType = value;\n if (this.#inner) this.#inner.binaryType = value;\n }\n\n get bufferedAmount() {\n return (this.#inner?.bufferedAmount ?? 0) + this.#outgoing.bytes() + this.#unsent;\n }\n\n get protocol() {\n return this.#inner?.protocol ?? '';\n }\n\n get extensions() {\n return this.#inner?.extensions ?? '';\n }\n\n send(data: Data) {\n if (this.#state === CONNECTING) {\n throw new DOMException('WebSocket is still in CONNECTING state', 'InvalidStateError');\n }\n if (this.#state !== OPEN) return;\n if (this.#stalled) {\n this.#unsent += size(data);\n return;\n }\n if (chance(loss('out'), this.#out)) return log('chaos: lost an outgoing message', data);\n const inner = this.#inner!;\n this.#outgoing.push(\n delay(this.#out),\n () => {\n if (inner.readyState === OPEN) inner.send(data);\n },\n data\n );\n }\n\n close(code?: number, reason?: string) {\n if (code !== undefined && code !== 1000 && !(code >= 3000 && code <= 4999)) {\n throw new DOMException(`Invalid close code ${code}`, 'InvalidAccessError');\n }\n if (this.#state >= CLOSING) return;\n const connecting = this.#state === CONNECTING;\n this.#state = CLOSING;\n const inner = this.#inner;\n if (inner && !this.#stalled) {\n // After anything still being sent, like a real close frame.\n this.#outgoing.push(delay(this.#out), () => inner.close(code, reason));\n return;\n }\n // Refused or stalled: the server can't answer, so the close handshake never completes.\n inner?.close(code, reason);\n setTimeout(() => this.#end(1006, '', false, connecting), 0);\n }\n\n /** Queue an event from the server, unless the connection is gone or silent. */\n #receive(run: () => void, random = this.#in) {\n if (this.#state === CLOSED || this.#stalled) return;\n this.#incoming.push(delay(random), () => {\n if (!this.#stalled) run();\n });\n }\n\n #message(data: unknown) {\n // Browsers don't deliver messages after `close()`.\n if (this.#state === OPEN) this.dispatchEvent(new MessageEvent('message', { data }));\n }\n\n /** (Re)start the `drop` and `stall` timers. */\n #arm() {\n for (const timer of this.#timers) clearTimeout(timer);\n this.#timers = [];\n if (!enabled()) return;\n const { drop, stall } = faults;\n if (drop !== undefined && drop !== false) {\n this.#timers.push(setTimeout(() => this.#drop(1006, ''), pick(drop, this.#life)));\n }\n if (stall !== undefined && stall !== false) {\n this.#timers.push(setTimeout(() => this.#stall(), pick(stall, this.#life)));\n }\n }\n\n #drop(code: number, reason: string) {\n log('chaos: dropped', this.url);\n this.#end(code, reason, code !== 1006, code === 1006);\n }\n\n #stall() {\n if (this.#state !== OPEN || this.#stalled) return;\n this.#stalled = true;\n log('chaos: stalled', this.url);\n }\n\n /** The only way to CLOSED, so it happens once. */\n #end(code: number, reason: string, wasClean: boolean, error = false) {\n if (this.#state === CLOSED) return;\n this.#state = CLOSED;\n live.delete(this);\n this.#incoming.clear();\n this.#outgoing.clear();\n for (const timer of this.#timers) clearTimeout(timer);\n // Free the server side of a connection we dropped, and say why in its logs.\n const inner = this.#inner;\n if (inner && inner.readyState < CLOSING) inner.close(4999, 'chaos');\n if (error) this.dispatchEvent(new Event('error'));\n this.dispatchEvent(closeEvent(code, reason, wasClean));\n }\n }\n\n return ChaosSocket;\n}\n"],"mappings":"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"}
@@ -0,0 +1,131 @@
1
+ //#region src/chaos.d.ts
2
+ /** A duration in ms, or `[min, max]` for a uniformly random one each time it's used. */
3
+ export type ChaosRange = number | readonly [min: number, max: number];
4
+ /** Faults to inject. All are off by default. Change them on a running class with `configure()`. */
5
+ export interface ChaosFaults {
6
+ /**
7
+ * Turn the random faults off without forgetting them. The controls (`drop()`, `stall()`,
8
+ * `inject()`) work either way.
9
+ * @default true
10
+ */
11
+ enabled?: boolean;
12
+ /**
13
+ * One-way delay in ms, added to the handshake (`open`), to every incoming message and to every
14
+ * send. Order is preserved, as on a real connection. A round trip takes about twice this.
15
+ * @default 0
16
+ * @example
17
+ * ```ts
18
+ * latency: [50, 400]
19
+ * ```
20
+ */
21
+ latency?: ChaosRange;
22
+ /**
23
+ * Probability (0–1) that a message is lost. A number applies to both directions.
24
+ * @default 0
25
+ * @example
26
+ * ```ts
27
+ * loss: { in: 0.05 } // lose 5% of incoming messages, none of the sends
28
+ * ```
29
+ */
30
+ loss?: number | {
31
+ in?: number;
32
+ out?: number;
33
+ };
34
+ /**
35
+ * Probability (0–1) that a connection attempt fails before opening: `error`, then `close`
36
+ * with code `1006`. The server never sees the attempt.
37
+ * @default 0
38
+ */
39
+ refuse?: number;
40
+ /**
41
+ * Each open connection drops after a lifetime in this range, in ms: `error`, then `close` with
42
+ * code `1006`, as when the network goes away.
43
+ * @default false
44
+ * @example
45
+ * ```ts
46
+ * drop: [10_000, 30_000]
47
+ * ```
48
+ */
49
+ drop?: ChaosRange | false;
50
+ /**
51
+ * Each open connection goes silent after a lifetime in this range, in ms, like a socket left
52
+ * half-open by a sleeping laptop: nothing arrives, sends vanish, `readyState` stays `OPEN`, and
53
+ * no `close` event fires. Only a heartbeat timeout (or `close()`) gets out of it.
54
+ * @default false
55
+ */
56
+ stall?: ChaosRange | false;
57
+ /**
58
+ * Probability (0–1) that an incoming message is cut short at a random point. Text also gets a
59
+ * U+FFFD appended, so it never parses as JSON.
60
+ * @default 0
61
+ */
62
+ corrupt?: number;
63
+ }
64
+ /** Options for {@link createChaosWebSocket}. */
65
+ export interface ChaosOptions extends ChaosFaults {
66
+ /**
67
+ * The `WebSocket` implementation to wrap, e.g. a mock in tests or the `ws` package in Node.
68
+ * @default the global `WebSocket`, in browsers only
69
+ */
70
+ WebSocket?: typeof WebSocket;
71
+ /**
72
+ * Seed the random faults, so a run can be repeated. Each connection gets its own sequence, so
73
+ * "the 3rd connection is refused" holds however much traffic the others carried.
74
+ * @default unseeded (`Math.random`)
75
+ */
76
+ seed?: number;
77
+ /**
78
+ * Receives a message for every injected fault.
79
+ * @example
80
+ * ```ts
81
+ * logger: console.warn
82
+ * ```
83
+ */
84
+ logger?: (message: string, ...details: unknown[]) => void;
85
+ }
86
+ /**
87
+ * Inject faults by hand into every connection made by one chaos class: from a dev panel, a
88
+ * keyboard shortcut, the console or a test.
89
+ */
90
+ export interface ChaosControls {
91
+ /** Change faults while running, merged into the current ones. Open connections restart their `drop` and `stall` timers. */
92
+ configure(faults: ChaosFaults): void;
93
+ /**
94
+ * Drop every connection now: `error`, then `close` with code `1006`. Pass a code to simulate the
95
+ * server closing instead, e.g. `drop(4001, 'unauthorized')`: a clean close with no `error`.
96
+ */
97
+ drop(code?: number, reason?: string): void;
98
+ /** Make every open connection go silent (see the `stall` option). */
99
+ stall(): void;
100
+ /**
101
+ * Deliver `data` to every open connection as if the server had sent it, after any `latency`.
102
+ * It is never lost or corrupted. Use it to try messages your server doesn't send.
103
+ */
104
+ inject(data: unknown): void;
105
+ }
106
+ /** A `WebSocket` class to pass as the `WebSocket` option, with {@link ChaosControls} as static methods. */
107
+ export type ChaosWebSocket = typeof WebSocket & ChaosControls;
108
+ /**
109
+ * Create a `WebSocket` class that injects network faults: latency, lost and corrupt messages,
110
+ * refused, dropped and silently stalled connections. Pass it as the `WebSocket` option in
111
+ * development to see how your app behaves on a bad network.
112
+ *
113
+ * Each call returns a new class with its own faults, controls and random sequence.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { createWebSocket } from 'react-websocket';
118
+ * import { createChaosWebSocket } from 'react-websocket/chaos';
119
+ *
120
+ * export const Chaos = createChaosWebSocket({ latency: [50, 400], drop: [10_000, 30_000] });
121
+ *
122
+ * export const live = createWebSocket('wss://api.example.com/live', {
123
+ * WebSocket: import.meta.env.DEV ? Chaos : undefined
124
+ * });
125
+ *
126
+ * Chaos.stall(); // later, from a dev panel or the console
127
+ * ```
128
+ */
129
+ export declare function createChaosWebSocket(options?: ChaosOptions): ChaosWebSocket;
130
+ //#endregion
131
+ //# sourceMappingURL=chaos.d.cts.map
@@ -0,0 +1,131 @@
1
+ //#region src/chaos.d.ts
2
+ /** A duration in ms, or `[min, max]` for a uniformly random one each time it's used. */
3
+ export type ChaosRange = number | readonly [min: number, max: number];
4
+ /** Faults to inject. All are off by default. Change them on a running class with `configure()`. */
5
+ export interface ChaosFaults {
6
+ /**
7
+ * Turn the random faults off without forgetting them. The controls (`drop()`, `stall()`,
8
+ * `inject()`) work either way.
9
+ * @default true
10
+ */
11
+ enabled?: boolean;
12
+ /**
13
+ * One-way delay in ms, added to the handshake (`open`), to every incoming message and to every
14
+ * send. Order is preserved, as on a real connection. A round trip takes about twice this.
15
+ * @default 0
16
+ * @example
17
+ * ```ts
18
+ * latency: [50, 400]
19
+ * ```
20
+ */
21
+ latency?: ChaosRange;
22
+ /**
23
+ * Probability (0–1) that a message is lost. A number applies to both directions.
24
+ * @default 0
25
+ * @example
26
+ * ```ts
27
+ * loss: { in: 0.05 } // lose 5% of incoming messages, none of the sends
28
+ * ```
29
+ */
30
+ loss?: number | {
31
+ in?: number;
32
+ out?: number;
33
+ };
34
+ /**
35
+ * Probability (0–1) that a connection attempt fails before opening: `error`, then `close`
36
+ * with code `1006`. The server never sees the attempt.
37
+ * @default 0
38
+ */
39
+ refuse?: number;
40
+ /**
41
+ * Each open connection drops after a lifetime in this range, in ms: `error`, then `close` with
42
+ * code `1006`, as when the network goes away.
43
+ * @default false
44
+ * @example
45
+ * ```ts
46
+ * drop: [10_000, 30_000]
47
+ * ```
48
+ */
49
+ drop?: ChaosRange | false;
50
+ /**
51
+ * Each open connection goes silent after a lifetime in this range, in ms, like a socket left
52
+ * half-open by a sleeping laptop: nothing arrives, sends vanish, `readyState` stays `OPEN`, and
53
+ * no `close` event fires. Only a heartbeat timeout (or `close()`) gets out of it.
54
+ * @default false
55
+ */
56
+ stall?: ChaosRange | false;
57
+ /**
58
+ * Probability (0–1) that an incoming message is cut short at a random point. Text also gets a
59
+ * U+FFFD appended, so it never parses as JSON.
60
+ * @default 0
61
+ */
62
+ corrupt?: number;
63
+ }
64
+ /** Options for {@link createChaosWebSocket}. */
65
+ export interface ChaosOptions extends ChaosFaults {
66
+ /**
67
+ * The `WebSocket` implementation to wrap, e.g. a mock in tests or the `ws` package in Node.
68
+ * @default the global `WebSocket`, in browsers only
69
+ */
70
+ WebSocket?: typeof WebSocket;
71
+ /**
72
+ * Seed the random faults, so a run can be repeated. Each connection gets its own sequence, so
73
+ * "the 3rd connection is refused" holds however much traffic the others carried.
74
+ * @default unseeded (`Math.random`)
75
+ */
76
+ seed?: number;
77
+ /**
78
+ * Receives a message for every injected fault.
79
+ * @example
80
+ * ```ts
81
+ * logger: console.warn
82
+ * ```
83
+ */
84
+ logger?: (message: string, ...details: unknown[]) => void;
85
+ }
86
+ /**
87
+ * Inject faults by hand into every connection made by one chaos class: from a dev panel, a
88
+ * keyboard shortcut, the console or a test.
89
+ */
90
+ export interface ChaosControls {
91
+ /** Change faults while running, merged into the current ones. Open connections restart their `drop` and `stall` timers. */
92
+ configure(faults: ChaosFaults): void;
93
+ /**
94
+ * Drop every connection now: `error`, then `close` with code `1006`. Pass a code to simulate the
95
+ * server closing instead, e.g. `drop(4001, 'unauthorized')`: a clean close with no `error`.
96
+ */
97
+ drop(code?: number, reason?: string): void;
98
+ /** Make every open connection go silent (see the `stall` option). */
99
+ stall(): void;
100
+ /**
101
+ * Deliver `data` to every open connection as if the server had sent it, after any `latency`.
102
+ * It is never lost or corrupted. Use it to try messages your server doesn't send.
103
+ */
104
+ inject(data: unknown): void;
105
+ }
106
+ /** A `WebSocket` class to pass as the `WebSocket` option, with {@link ChaosControls} as static methods. */
107
+ export type ChaosWebSocket = typeof WebSocket & ChaosControls;
108
+ /**
109
+ * Create a `WebSocket` class that injects network faults: latency, lost and corrupt messages,
110
+ * refused, dropped and silently stalled connections. Pass it as the `WebSocket` option in
111
+ * development to see how your app behaves on a bad network.
112
+ *
113
+ * Each call returns a new class with its own faults, controls and random sequence.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { createWebSocket } from 'react-websocket';
118
+ * import { createChaosWebSocket } from 'react-websocket/chaos';
119
+ *
120
+ * export const Chaos = createChaosWebSocket({ latency: [50, 400], drop: [10_000, 30_000] });
121
+ *
122
+ * export const live = createWebSocket('wss://api.example.com/live', {
123
+ * WebSocket: import.meta.env.DEV ? Chaos : undefined
124
+ * });
125
+ *
126
+ * Chaos.stall(); // later, from a dev panel or the console
127
+ * ```
128
+ */
129
+ export declare function createChaosWebSocket(options?: ChaosOptions): ChaosWebSocket;
130
+ //#endregion
131
+ //# sourceMappingURL=chaos.d.ts.map