react-websocket 3.3.0 → 3.5.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 +12 -4
- package/dist/chaos.cjs +284 -0
- package/dist/chaos.cjs.map +1 -0
- package/dist/chaos.d.cts +131 -0
- package/dist/chaos.d.ts +131 -0
- package/dist/chaos.js +283 -0
- package/dist/chaos.js.map +1 -0
- package/dist/core.cjs +161 -17
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +125 -5
- package/dist/core.d.ts +125 -5
- package/dist/core.js +161 -18
- package/dist/core.js.map +1 -1
- package/dist/index.d.cts +125 -5
- package/dist/index.d.ts +34 -4
- package/dist/timeline.cjs +162 -0
- package/dist/timeline.cjs.map +1 -0
- package/dist/timeline.d.cts +379 -0
- package/dist/timeline.d.ts +140 -0
- package/dist/timeline.js +161 -0
- package/dist/timeline.js.map +1 -0
- package/package.json +56 -15
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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"}
|
package/dist/core.cjs
CHANGED
|
@@ -349,13 +349,18 @@ function createClient(url, options = {}) {
|
|
|
349
349
|
set({ lastMessage: message });
|
|
350
350
|
emit("message", message, event);
|
|
351
351
|
}
|
|
352
|
-
function send(message, sendOptions) {
|
|
352
|
+
function send(message, sendOptions = {}) {
|
|
353
353
|
const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
|
|
354
|
+
const { key, priority, maxBuffered } = sendOptions;
|
|
354
355
|
if (ws && ws.readyState === OPEN) {
|
|
356
|
+
if (maxBuffered !== void 0 && ws.bufferedAmount > maxBuffered) {
|
|
357
|
+
log("dropped message: send buffer above maxBuffered");
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
355
360
|
ws.send(data);
|
|
356
361
|
return true;
|
|
357
362
|
}
|
|
358
|
-
if (queueLimit === 0 || sendOptions
|
|
363
|
+
if (queueLimit === 0 || sendOptions.volatile) {
|
|
359
364
|
log("dropped message: socket is not open");
|
|
360
365
|
return false;
|
|
361
366
|
}
|
|
@@ -363,19 +368,23 @@ function createClient(url, options = {}) {
|
|
|
363
368
|
log("dropped message: no WebSocket implementation available");
|
|
364
369
|
return false;
|
|
365
370
|
}
|
|
366
|
-
const key = sendOptions?.key;
|
|
367
371
|
if (key !== void 0) pending = pending.filter((entry) => {
|
|
368
372
|
if (entry.key !== key) return true;
|
|
369
373
|
dropped(entry);
|
|
370
374
|
return false;
|
|
371
375
|
});
|
|
372
|
-
pending.
|
|
376
|
+
let head = pending.findIndex((entry) => !entry.priority);
|
|
377
|
+
if (head < 0) head = pending.length;
|
|
378
|
+
const entry = {
|
|
373
379
|
data,
|
|
374
380
|
key,
|
|
375
|
-
|
|
376
|
-
|
|
381
|
+
priority,
|
|
382
|
+
onDrop: sendOptions.onDrop
|
|
383
|
+
};
|
|
384
|
+
if (priority) pending.splice(head++, 0, entry);
|
|
385
|
+
else pending.push(entry);
|
|
377
386
|
if (pending.length > queueLimit) {
|
|
378
|
-
dropped(pending.
|
|
387
|
+
dropped(pending.splice(head < pending.length ? head : 0, 1)[0]);
|
|
379
388
|
log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
|
|
380
389
|
}
|
|
381
390
|
return false;
|
|
@@ -744,6 +753,7 @@ function createWebSocketRequests(client, options) {
|
|
|
744
753
|
return;
|
|
745
754
|
}
|
|
746
755
|
if (sent) entry.socket = client.socket;
|
|
756
|
+
else if (client.socket?.readyState === 1) entry.reject(/* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
|
|
747
757
|
else if (requestOptions.volatile) entry.reject(/* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
|
|
748
758
|
});
|
|
749
759
|
},
|
|
@@ -756,7 +766,7 @@ function createWebSocketRequests(client, options) {
|
|
|
756
766
|
}
|
|
757
767
|
//#endregion
|
|
758
768
|
//#region src/responder.ts
|
|
759
|
-
const VOLATILE = { volatile: true };
|
|
769
|
+
const VOLATILE$1 = { volatile: true };
|
|
760
770
|
const abortError = (message) => new DOMException(message, "AbortError");
|
|
761
771
|
/**
|
|
762
772
|
* Answer requests the server sends: run a handler for tools that only exist in the browser
|
|
@@ -797,9 +807,9 @@ function createWebSocketResponder(client, options) {
|
|
|
797
807
|
/** Send a reply, or an `error` reply if building or encoding it throws. */
|
|
798
808
|
function reply(id, build) {
|
|
799
809
|
try {
|
|
800
|
-
return client.send(build(), VOLATILE);
|
|
810
|
+
return client.send(build(), VOLATILE$1);
|
|
801
811
|
} catch (error) {
|
|
802
|
-
return client.send(options.error(id, error), VOLATILE);
|
|
812
|
+
return client.send(options.error(id, error), VOLATILE$1);
|
|
803
813
|
}
|
|
804
814
|
}
|
|
805
815
|
function receive(incoming) {
|
|
@@ -898,6 +908,123 @@ function createWebSocketResponder(client, options) {
|
|
|
898
908
|
};
|
|
899
909
|
}
|
|
900
910
|
//#endregion
|
|
911
|
+
//#region src/sendrate.ts
|
|
912
|
+
const VOLATILE = { volatile: true };
|
|
913
|
+
/**
|
|
914
|
+
* Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
|
|
915
|
+
* connection. Messages are coalesced per key between ticks. The rate drops by half when the
|
|
916
|
+
* send buffer fills up or the round trip grows, and climbs back one tick per second at a time
|
|
917
|
+
* when the link keeps up, so players on a poor connection degrade smoothly instead of building
|
|
918
|
+
* up lag.
|
|
919
|
+
*
|
|
920
|
+
* Ticks run only while the socket is open and something is waiting. Messages waiting during a
|
|
921
|
+
* reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```ts
|
|
925
|
+
* export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
|
|
926
|
+
*
|
|
927
|
+
* // On every pointer move: at most 30 a second, fewer on a congested link.
|
|
928
|
+
* stream.send({ type: 'aim', angle }, 'aim');
|
|
929
|
+
* ```
|
|
930
|
+
*/
|
|
931
|
+
function createWebSocketSendRate(client, options = {}) {
|
|
932
|
+
const { maxBuffered = 1024, latency, maxRttIncrease = 100 } = options;
|
|
933
|
+
const maxRate = options.maxRate ?? 30;
|
|
934
|
+
const minRate = Math.min(options.minRate ?? 5, maxRate);
|
|
935
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
936
|
+
const pending = /* @__PURE__ */ new Map();
|
|
937
|
+
let rate = maxRate;
|
|
938
|
+
let snapshot = {
|
|
939
|
+
rate: maxRate,
|
|
940
|
+
congested: false
|
|
941
|
+
};
|
|
942
|
+
let last = -Infinity;
|
|
943
|
+
let best = Infinity;
|
|
944
|
+
let slow = false;
|
|
945
|
+
let measured;
|
|
946
|
+
let timer;
|
|
947
|
+
let disposed = false;
|
|
948
|
+
function flush() {
|
|
949
|
+
for (const [key, message] of pending) {
|
|
950
|
+
if (!client.send(message, VOLATILE)) return;
|
|
951
|
+
pending.delete(key);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function publish(congested) {
|
|
955
|
+
const rounded = Math.round(rate);
|
|
956
|
+
if (rounded === snapshot.rate && congested === snapshot.congested) return;
|
|
957
|
+
snapshot = {
|
|
958
|
+
rate: rounded,
|
|
959
|
+
congested
|
|
960
|
+
};
|
|
961
|
+
for (const listener of [...listeners]) listener();
|
|
962
|
+
}
|
|
963
|
+
function schedule() {
|
|
964
|
+
if (disposed || timer !== void 0 || !pending.size || client.getSnapshot().status !== "open") return;
|
|
965
|
+
timer = setTimeout(tick, Math.max(0, last + 1e3 / rate - performance.now()));
|
|
966
|
+
}
|
|
967
|
+
function stop() {
|
|
968
|
+
clearTimeout(timer);
|
|
969
|
+
timer = void 0;
|
|
970
|
+
}
|
|
971
|
+
function tick() {
|
|
972
|
+
timer = void 0;
|
|
973
|
+
last = performance.now();
|
|
974
|
+
const measurement = latency?.getSnapshot();
|
|
975
|
+
if (measurement !== measured) {
|
|
976
|
+
measured = measurement;
|
|
977
|
+
const rtt = measurement?.rtt;
|
|
978
|
+
if (rtt !== void 0) {
|
|
979
|
+
best = Math.min(best, rtt);
|
|
980
|
+
slow = rtt - best > maxRttIncrease;
|
|
981
|
+
if (slow) rate /= 2;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const full = (client.socket?.bufferedAmount ?? 0) > maxBuffered;
|
|
985
|
+
if (full) rate /= 2;
|
|
986
|
+
else {
|
|
987
|
+
flush();
|
|
988
|
+
if (!slow) rate++;
|
|
989
|
+
}
|
|
990
|
+
rate = Math.min(maxRate, Math.max(minRate, rate));
|
|
991
|
+
publish(full || slow);
|
|
992
|
+
schedule();
|
|
993
|
+
}
|
|
994
|
+
const offs = [client.on("open", () => {
|
|
995
|
+
best = Infinity;
|
|
996
|
+
slow = false;
|
|
997
|
+
measured = latency?.getSnapshot();
|
|
998
|
+
schedule();
|
|
999
|
+
}), client.subscribe(() => {
|
|
1000
|
+
const { status } = client.getSnapshot();
|
|
1001
|
+
if (status !== "open") stop();
|
|
1002
|
+
if (status === "closed" || status === "idle") pending.clear();
|
|
1003
|
+
})];
|
|
1004
|
+
return {
|
|
1005
|
+
client,
|
|
1006
|
+
send(message, key) {
|
|
1007
|
+
pending.delete(key);
|
|
1008
|
+
pending.set(key, message);
|
|
1009
|
+
schedule();
|
|
1010
|
+
},
|
|
1011
|
+
flush,
|
|
1012
|
+
getSnapshot: () => snapshot,
|
|
1013
|
+
subscribe(listener) {
|
|
1014
|
+
listeners.add(listener);
|
|
1015
|
+
return () => {
|
|
1016
|
+
listeners.delete(listener);
|
|
1017
|
+
};
|
|
1018
|
+
},
|
|
1019
|
+
dispose() {
|
|
1020
|
+
disposed = true;
|
|
1021
|
+
stop();
|
|
1022
|
+
pending.clear();
|
|
1023
|
+
offs.forEach((off) => off());
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
901
1028
|
//#region src/state.ts
|
|
902
1029
|
/**
|
|
903
1030
|
* Keep state derived from a client's messages, like a reducer that runs on every message.
|
|
@@ -918,14 +1045,30 @@ function createWebSocketResponder(client, options) {
|
|
|
918
1045
|
* ```
|
|
919
1046
|
*/
|
|
920
1047
|
function createWebSocketState(client, options) {
|
|
921
|
-
const { initialState, reduce } = options;
|
|
1048
|
+
const { initialState, reduce, batch } = options;
|
|
922
1049
|
let state = initialState;
|
|
1050
|
+
let notified = state;
|
|
1051
|
+
let frame = false;
|
|
923
1052
|
const listeners = /* @__PURE__ */ new Set();
|
|
924
|
-
function
|
|
925
|
-
|
|
1053
|
+
function notify() {
|
|
1054
|
+
if (Object.is(state, notified)) return;
|
|
1055
|
+
notified = state;
|
|
1056
|
+
for (const listener of [...listeners]) listener();
|
|
1057
|
+
}
|
|
1058
|
+
function flush() {
|
|
1059
|
+
frame = false;
|
|
1060
|
+
notify();
|
|
1061
|
+
}
|
|
1062
|
+
function update(change, later) {
|
|
1063
|
+
const next = typeof change === "function" ? change(state) : change;
|
|
926
1064
|
if (Object.is(next, state)) return;
|
|
927
1065
|
state = next;
|
|
928
|
-
|
|
1066
|
+
if (!later) notify();
|
|
1067
|
+
else if (!frame) {
|
|
1068
|
+
frame = true;
|
|
1069
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
|
|
1070
|
+
else setTimeout(flush, 16);
|
|
1071
|
+
}
|
|
929
1072
|
}
|
|
930
1073
|
return {
|
|
931
1074
|
client,
|
|
@@ -937,9 +1080,9 @@ function createWebSocketState(client, options) {
|
|
|
937
1080
|
listeners.delete(listener);
|
|
938
1081
|
};
|
|
939
1082
|
},
|
|
940
|
-
setState,
|
|
941
|
-
reset: () =>
|
|
942
|
-
dispose: client.on("message", (message) =>
|
|
1083
|
+
setState: (change) => update(change),
|
|
1084
|
+
reset: () => update(initialState),
|
|
1085
|
+
dispose: client.on("message", (message) => update((current) => reduce(current, message), batch === "frame"))
|
|
943
1086
|
};
|
|
944
1087
|
}
|
|
945
1088
|
//#endregion
|
|
@@ -1031,6 +1174,7 @@ exports.createWebSocket = createWebSocket;
|
|
|
1031
1174
|
exports.createWebSocketLatency = createWebSocketLatency;
|
|
1032
1175
|
exports.createWebSocketRequests = createWebSocketRequests;
|
|
1033
1176
|
exports.createWebSocketResponder = createWebSocketResponder;
|
|
1177
|
+
exports.createWebSocketSendRate = createWebSocketSendRate;
|
|
1034
1178
|
exports.createWebSocketState = createWebSocketState;
|
|
1035
1179
|
exports.createWebSocketTopics = createWebSocketTopics;
|
|
1036
1180
|
|