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/dist/chaos.js ADDED
@@ -0,0 +1,283 @@
1
+ //#region src/chaos.ts
2
+ const CONNECTING = 0;
3
+ const OPEN = 1;
4
+ const CLOSING = 2;
5
+ const CLOSED = 3;
6
+ function mulberry32(seed) {
7
+ return () => {
8
+ seed = seed + 1831565813 | 0;
9
+ let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
10
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
11
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
12
+ };
13
+ }
14
+ function pick(range, random) {
15
+ return typeof range === "number" ? range : range[0] + (range[1] - range[0]) * random();
16
+ }
17
+ function size(data) {
18
+ return new Blob([data]).size;
19
+ }
20
+ /** Cut `data` short at a random point. Text also gets U+FFFD, which no JSON document ends with. */
21
+ function garble(data, random) {
22
+ if (typeof data === "string") return data.slice(0, Math.floor(random() * data.length)) + "�";
23
+ const binary = data;
24
+ if (typeof binary?.slice !== "function") return "�";
25
+ const length = binary.size ?? data.byteLength;
26
+ return binary.slice(0, Math.floor(random() * length));
27
+ }
28
+ function closeEvent(code, reason, wasClean) {
29
+ const init = {
30
+ code,
31
+ reason,
32
+ wasClean
33
+ };
34
+ return typeof CloseEvent === "function" ? new CloseEvent("close", init) : Object.assign(new Event("close"), init);
35
+ }
36
+ /**
37
+ * Events delayed in order: each runs in its own task, no earlier than the one before it,
38
+ * because a WebSocket never reorders. With no delay and nothing waiting, it runs right away.
39
+ */
40
+ function createQueue() {
41
+ let items = [];
42
+ let timer;
43
+ function next() {
44
+ const head = items[0];
45
+ if (timer !== void 0 || !head) return;
46
+ timer = setTimeout(() => {
47
+ timer = void 0;
48
+ try {
49
+ items.shift().run();
50
+ } finally {
51
+ next();
52
+ }
53
+ }, head.at - Date.now());
54
+ }
55
+ return {
56
+ push(delay, run, data) {
57
+ if (delay <= 0 && !items.length) return run();
58
+ items.push({
59
+ at: Math.max(Date.now() + delay, items.at(-1)?.at ?? 0),
60
+ run,
61
+ data
62
+ });
63
+ next();
64
+ },
65
+ clear() {
66
+ items = [];
67
+ clearTimeout(timer);
68
+ timer = void 0;
69
+ },
70
+ bytes: () => items.reduce((sum, item) => sum + (item.data === void 0 ? 0 : size(item.data)), 0)
71
+ };
72
+ }
73
+ /**
74
+ * Create a `WebSocket` class that injects network faults: latency, lost and corrupt messages,
75
+ * refused, dropped and silently stalled connections. Pass it as the `WebSocket` option in
76
+ * development to see how your app behaves on a bad network.
77
+ *
78
+ * Each call returns a new class with its own faults, controls and random sequence.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import { createWebSocket } from 'react-websocket';
83
+ * import { createChaosWebSocket } from 'react-websocket/chaos';
84
+ *
85
+ * export const Chaos = createChaosWebSocket({ latency: [50, 400], drop: [10_000, 30_000] });
86
+ *
87
+ * export const live = createWebSocket('wss://api.example.com/live', {
88
+ * WebSocket: import.meta.env.DEV ? Chaos : undefined
89
+ * });
90
+ *
91
+ * Chaos.stall(); // later, from a dev panel or the console
92
+ * ```
93
+ */
94
+ function createChaosWebSocket(options = {}) {
95
+ const { WebSocket: wrapped, seed, logger, ...initial } = options;
96
+ const log = logger ?? (() => {});
97
+ let faults = initial;
98
+ const live = /* @__PURE__ */ new Set();
99
+ let count = 0;
100
+ const enabled = () => faults.enabled !== false;
101
+ const chance = (probability, random) => enabled() && !!probability && random() < probability;
102
+ const delay = (random) => enabled() && faults.latency ? pick(faults.latency, random) : 0;
103
+ const loss = (direction) => typeof faults.loss === "object" ? faults.loss[direction] : faults.loss;
104
+ const stream = (n, k) => seed === void 0 ? Math.random : mulberry32(seed + Math.imul(3 * n + k, 2654435769));
105
+ class ChaosSocket extends EventTarget {
106
+ static CONNECTING = CONNECTING;
107
+ static OPEN = OPEN;
108
+ static CLOSING = CLOSING;
109
+ static CLOSED = CLOSED;
110
+ CONNECTING = CONNECTING;
111
+ OPEN = OPEN;
112
+ CLOSING = CLOSING;
113
+ CLOSED = CLOSED;
114
+ url;
115
+ onopen = null;
116
+ onmessage = null;
117
+ onerror = null;
118
+ onclose = null;
119
+ #state = CONNECTING;
120
+ #inner;
121
+ #binaryType = "blob";
122
+ #stalled = false;
123
+ #unsent = 0;
124
+ #life;
125
+ #in;
126
+ #out;
127
+ #incoming = createQueue();
128
+ #outgoing = createQueue();
129
+ #timers = [];
130
+ static configure(next) {
131
+ faults = {
132
+ ...faults,
133
+ ...next
134
+ };
135
+ if (!("drop" in next || "stall" in next || "enabled" in next)) return;
136
+ for (const socket of [...live]) if (socket.#state === OPEN) socket.#arm();
137
+ }
138
+ static drop(code = 1006, reason = "") {
139
+ for (const socket of [...live]) socket.#drop(code, reason);
140
+ }
141
+ static stall() {
142
+ for (const socket of [...live]) socket.#stall();
143
+ }
144
+ static inject(data) {
145
+ for (const socket of [...live]) if (socket.#state === OPEN) socket.#receive(() => socket.#message(data));
146
+ }
147
+ constructor(url, protocols) {
148
+ super();
149
+ const Impl = wrapped ?? (typeof window === "undefined" ? void 0 : globalThis.WebSocket);
150
+ if (!Impl) throw new Error("react-websocket/chaos: no WebSocket to wrap here (server render?)");
151
+ for (const type of [
152
+ "open",
153
+ "message",
154
+ "error",
155
+ "close"
156
+ ]) this.addEventListener(type, (event) => this[`on${type}`]?.call(this, event));
157
+ const n = ++count;
158
+ this.#life = stream(n, 0);
159
+ this.#in = stream(n, 1);
160
+ this.#out = stream(n, 2);
161
+ if (chance(faults.refuse, this.#life)) {
162
+ log("chaos: refused", String(url));
163
+ this.url = String(url);
164
+ live.add(this);
165
+ setTimeout(() => this.#end(1006, "", false, true), delay(this.#life));
166
+ return;
167
+ }
168
+ const inner = new Impl(url, protocols);
169
+ this.#inner = inner;
170
+ this.url = inner.url;
171
+ live.add(this);
172
+ inner.onopen = () => this.#receive(() => {
173
+ if (this.#state !== CONNECTING) return;
174
+ this.#state = OPEN;
175
+ this.#arm();
176
+ this.dispatchEvent(new Event("open"));
177
+ }, this.#life);
178
+ inner.onmessage = (event) => {
179
+ if (this.#state === CLOSED || this.#stalled) return;
180
+ if (chance(loss("in"), this.#in)) return log("chaos: lost an incoming message", event.data);
181
+ let data = event.data;
182
+ if (chance(faults.corrupt, this.#in)) {
183
+ data = garble(data, this.#in);
184
+ log("chaos: corrupted a message", event.data);
185
+ }
186
+ this.#receive(() => this.#message(data));
187
+ };
188
+ inner.onerror = () => this.#receive(() => this.dispatchEvent(new Event("error")));
189
+ inner.onclose = (event) => this.#receive(() => this.#end(event.code, event.reason, event.wasClean));
190
+ }
191
+ get readyState() {
192
+ return this.#state;
193
+ }
194
+ get binaryType() {
195
+ return this.#inner?.binaryType ?? this.#binaryType;
196
+ }
197
+ set binaryType(value) {
198
+ this.#binaryType = value;
199
+ if (this.#inner) this.#inner.binaryType = value;
200
+ }
201
+ get bufferedAmount() {
202
+ return (this.#inner?.bufferedAmount ?? 0) + this.#outgoing.bytes() + this.#unsent;
203
+ }
204
+ get protocol() {
205
+ return this.#inner?.protocol ?? "";
206
+ }
207
+ get extensions() {
208
+ return this.#inner?.extensions ?? "";
209
+ }
210
+ send(data) {
211
+ if (this.#state === CONNECTING) throw new DOMException("WebSocket is still in CONNECTING state", "InvalidStateError");
212
+ if (this.#state !== OPEN) return;
213
+ if (this.#stalled) {
214
+ this.#unsent += size(data);
215
+ return;
216
+ }
217
+ if (chance(loss("out"), this.#out)) return log("chaos: lost an outgoing message", data);
218
+ const inner = this.#inner;
219
+ this.#outgoing.push(delay(this.#out), () => {
220
+ if (inner.readyState === OPEN) inner.send(data);
221
+ }, data);
222
+ }
223
+ close(code, reason) {
224
+ if (code !== void 0 && code !== 1e3 && !(code >= 3e3 && code <= 4999)) throw new DOMException(`Invalid close code ${code}`, "InvalidAccessError");
225
+ if (this.#state >= CLOSING) return;
226
+ const connecting = this.#state === CONNECTING;
227
+ this.#state = CLOSING;
228
+ const inner = this.#inner;
229
+ if (inner && !this.#stalled) {
230
+ this.#outgoing.push(delay(this.#out), () => inner.close(code, reason));
231
+ return;
232
+ }
233
+ inner?.close(code, reason);
234
+ setTimeout(() => this.#end(1006, "", false, connecting), 0);
235
+ }
236
+ /** Queue an event from the server, unless the connection is gone or silent. */
237
+ #receive(run, random = this.#in) {
238
+ if (this.#state === CLOSED || this.#stalled) return;
239
+ this.#incoming.push(delay(random), () => {
240
+ if (!this.#stalled) run();
241
+ });
242
+ }
243
+ #message(data) {
244
+ if (this.#state === OPEN) this.dispatchEvent(new MessageEvent("message", { data }));
245
+ }
246
+ /** (Re)start the `drop` and `stall` timers. */
247
+ #arm() {
248
+ for (const timer of this.#timers) clearTimeout(timer);
249
+ this.#timers = [];
250
+ if (!enabled()) return;
251
+ const { drop, stall } = faults;
252
+ if (drop !== void 0 && drop !== false) this.#timers.push(setTimeout(() => this.#drop(1006, ""), pick(drop, this.#life)));
253
+ if (stall !== void 0 && stall !== false) this.#timers.push(setTimeout(() => this.#stall(), pick(stall, this.#life)));
254
+ }
255
+ #drop(code, reason) {
256
+ log("chaos: dropped", this.url);
257
+ this.#end(code, reason, code !== 1006, code === 1006);
258
+ }
259
+ #stall() {
260
+ if (this.#state !== OPEN || this.#stalled) return;
261
+ this.#stalled = true;
262
+ log("chaos: stalled", this.url);
263
+ }
264
+ /** The only way to CLOSED, so it happens once. */
265
+ #end(code, reason, wasClean, error = false) {
266
+ if (this.#state === CLOSED) return;
267
+ this.#state = CLOSED;
268
+ live.delete(this);
269
+ this.#incoming.clear();
270
+ this.#outgoing.clear();
271
+ for (const timer of this.#timers) clearTimeout(timer);
272
+ const inner = this.#inner;
273
+ if (inner && inner.readyState < CLOSING) inner.close(4999, "chaos");
274
+ if (error) this.dispatchEvent(new Event("error"));
275
+ this.dispatchEvent(closeEvent(code, reason, wasClean));
276
+ }
277
+ }
278
+ return ChaosSocket;
279
+ }
280
+ //#endregion
281
+ export { createChaosWebSocket };
282
+
283
+ //# sourceMappingURL=chaos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chaos.js","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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"}