react-websocket 4.0.0 → 4.18.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 +27 -3
- package/dist/ag-ui.cjs +421 -0
- package/dist/ag-ui.cjs.map +1 -0
- package/dist/ag-ui.d.cts +493 -0
- package/dist/ag-ui.d.ts +171 -0
- package/dist/ag-ui.js +418 -0
- package/dist/ag-ui.js.map +1 -0
- package/dist/core.cjs +689 -153
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +459 -53
- package/dist/core.d.ts +459 -53
- package/dist/core.js +686 -154
- package/dist/core.js.map +1 -1
- package/dist/devtools.cjs +186 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +32 -0
- package/dist/devtools.d.ts +32 -0
- package/dist/devtools.js +185 -0
- package/dist/devtools.js.map +1 -0
- package/dist/index.cjs +182 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +663 -48
- package/dist/index.d.ts +393 -22
- package/dist/index.js +178 -24
- package/dist/index.js.map +1 -1
- package/dist/outbox.cjs +222 -0
- package/dist/outbox.cjs.map +1 -0
- package/dist/outbox.d.cts +127 -0
- package/dist/outbox.d.ts +127 -0
- package/dist/outbox.js +219 -0
- package/dist/outbox.js.map +1 -0
- package/dist/replay.cjs +324 -0
- package/dist/replay.cjs.map +1 -0
- package/dist/replay.d.cts +124 -0
- package/dist/replay.d.ts +124 -0
- package/dist/replay.js +322 -0
- package/dist/replay.js.map +1 -0
- package/dist/tabs.cjs +336 -0
- package/dist/tabs.cjs.map +1 -0
- package/dist/tabs.d.cts +81 -0
- package/dist/tabs.d.ts +81 -0
- package/dist/tabs.js +335 -0
- package/dist/tabs.js.map +1 -0
- package/dist/timeline.d.cts +1 -216
- package/dist/types-BEecFqIB.d.cts +268 -0
- package/dist/webtransport.cjs +243 -0
- package/dist/webtransport.cjs.map +1 -0
- package/dist/webtransport.d.cts +67 -0
- package/dist/webtransport.d.ts +67 -0
- package/dist/webtransport.js +242 -0
- package/dist/webtransport.js.map +1 -0
- package/package.json +139 -3
package/dist/replay.cjs
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/replay.ts
|
|
3
|
+
const CONNECTING = 0;
|
|
4
|
+
const OPEN = 1;
|
|
5
|
+
const CLOSING = 2;
|
|
6
|
+
const CLOSED = 3;
|
|
7
|
+
function closeEvent(code, reason, wasClean) {
|
|
8
|
+
const init = {
|
|
9
|
+
code,
|
|
10
|
+
reason,
|
|
11
|
+
wasClean
|
|
12
|
+
};
|
|
13
|
+
return typeof CloseEvent === "function" ? new CloseEvent("close", init) : Object.assign(new Event("close"), init);
|
|
14
|
+
}
|
|
15
|
+
function encode(bytes) {
|
|
16
|
+
const view = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
17
|
+
let text = "";
|
|
18
|
+
for (const byte of view) text += String.fromCharCode(byte);
|
|
19
|
+
return { binary: btoa(text) };
|
|
20
|
+
}
|
|
21
|
+
function decode(data, binaryType) {
|
|
22
|
+
if (typeof data === "string") return data;
|
|
23
|
+
const buffer = Uint8Array.from(atob(data.binary), (char) => char.charCodeAt(0)).buffer;
|
|
24
|
+
return binaryType === "blob" ? new Blob([buffer]) : buffer;
|
|
25
|
+
}
|
|
26
|
+
/** Shared by both classes: the `WebSocket` surface over a state and some events. */
|
|
27
|
+
var Base = class extends EventTarget {
|
|
28
|
+
static CONNECTING = CONNECTING;
|
|
29
|
+
static OPEN = OPEN;
|
|
30
|
+
static CLOSING = CLOSING;
|
|
31
|
+
static CLOSED = CLOSED;
|
|
32
|
+
CONNECTING = CONNECTING;
|
|
33
|
+
OPEN = OPEN;
|
|
34
|
+
CLOSING = CLOSING;
|
|
35
|
+
CLOSED = CLOSED;
|
|
36
|
+
url;
|
|
37
|
+
extensions = "";
|
|
38
|
+
onopen = null;
|
|
39
|
+
onmessage = null;
|
|
40
|
+
onerror = null;
|
|
41
|
+
onclose = null;
|
|
42
|
+
protocol = "";
|
|
43
|
+
state = CONNECTING;
|
|
44
|
+
constructor(url) {
|
|
45
|
+
super();
|
|
46
|
+
this.url = String(url);
|
|
47
|
+
for (const type of [
|
|
48
|
+
"open",
|
|
49
|
+
"message",
|
|
50
|
+
"error",
|
|
51
|
+
"close"
|
|
52
|
+
]) this.addEventListener(type, (event) => this[`on${type}`]?.call(this, event));
|
|
53
|
+
}
|
|
54
|
+
get readyState() {
|
|
55
|
+
return this.state;
|
|
56
|
+
}
|
|
57
|
+
opened(protocol) {
|
|
58
|
+
this.state = OPEN;
|
|
59
|
+
this.protocol = protocol;
|
|
60
|
+
this.dispatchEvent(new Event("open"));
|
|
61
|
+
}
|
|
62
|
+
received(data) {
|
|
63
|
+
if (this.state === OPEN) this.dispatchEvent(new MessageEvent("message", { data }));
|
|
64
|
+
}
|
|
65
|
+
ended(code, reason, wasClean) {
|
|
66
|
+
if (this.state === CLOSED) return;
|
|
67
|
+
this.state = CLOSED;
|
|
68
|
+
this.dispatchEvent(closeEvent(code, reason, wasClean));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* A `WebSocket` class that records what its connections do: when each one opened, every message
|
|
73
|
+
* sent and received, errors and closes. Pass it as the `WebSocket` option, and get the session
|
|
74
|
+
* back with `recording()`, or download it with `save()`, to attach to a bug report or replay
|
|
75
|
+
* with {@link createReplayWebSocket}.
|
|
76
|
+
*
|
|
77
|
+
* Recordings hold every message, so they can hold tokens and personal data: record in
|
|
78
|
+
* development, or ask before saving one from a user's session.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { createRecordingWebSocket } from 'react-websocket/replay';
|
|
83
|
+
*
|
|
84
|
+
* export const Recorder = createRecordingWebSocket();
|
|
85
|
+
* export const live = createWebSocket(url, { WebSocket: Recorder });
|
|
86
|
+
*
|
|
87
|
+
* reportButton.onclick = () => Recorder.save('session.json');
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
function createRecordingWebSocket(options = {}) {
|
|
91
|
+
let connections = [];
|
|
92
|
+
class RecordingSocket extends Base {
|
|
93
|
+
#inner;
|
|
94
|
+
#binaryType = "blob";
|
|
95
|
+
static recording() {
|
|
96
|
+
return JSON.parse(JSON.stringify({
|
|
97
|
+
format: "react-websocket-recording",
|
|
98
|
+
version: 1,
|
|
99
|
+
connections
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
static save(filename = "websocket-recording.json") {
|
|
103
|
+
const url = URL.createObjectURL(new Blob([JSON.stringify(RecordingSocket.recording(), null, 2)], { type: "application/json" }));
|
|
104
|
+
Object.assign(document.createElement("a"), {
|
|
105
|
+
href: url,
|
|
106
|
+
download: filename
|
|
107
|
+
}).click();
|
|
108
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
109
|
+
}
|
|
110
|
+
static clear() {
|
|
111
|
+
connections = [];
|
|
112
|
+
}
|
|
113
|
+
constructor(url, protocols) {
|
|
114
|
+
super(url);
|
|
115
|
+
const Impl = options.WebSocket ?? globalThis.WebSocket;
|
|
116
|
+
const inner = this.#inner = new Impl(url, protocols);
|
|
117
|
+
inner.binaryType = "arraybuffer";
|
|
118
|
+
const start = performance.now();
|
|
119
|
+
const events = [];
|
|
120
|
+
connections.push({
|
|
121
|
+
url: this.url,
|
|
122
|
+
at: Date.now(),
|
|
123
|
+
events
|
|
124
|
+
});
|
|
125
|
+
const record = (event) => events.push({
|
|
126
|
+
t: Math.round(performance.now() - start),
|
|
127
|
+
...event
|
|
128
|
+
});
|
|
129
|
+
this.#record = record;
|
|
130
|
+
inner.onopen = () => {
|
|
131
|
+
record({
|
|
132
|
+
type: "open",
|
|
133
|
+
protocol: inner.protocol ?? ""
|
|
134
|
+
});
|
|
135
|
+
this.opened(inner.protocol);
|
|
136
|
+
};
|
|
137
|
+
inner.onmessage = (event) => {
|
|
138
|
+
const data = event.data;
|
|
139
|
+
record({
|
|
140
|
+
type: "message",
|
|
141
|
+
data: typeof data === "string" ? data : encode(data)
|
|
142
|
+
});
|
|
143
|
+
this.received(typeof data === "string" || this.#binaryType !== "blob" ? data : new Blob([data]));
|
|
144
|
+
};
|
|
145
|
+
inner.onerror = () => {
|
|
146
|
+
record({ type: "error" });
|
|
147
|
+
this.dispatchEvent(new Event("error"));
|
|
148
|
+
};
|
|
149
|
+
inner.onclose = (event) => {
|
|
150
|
+
record({
|
|
151
|
+
type: "close",
|
|
152
|
+
code: event.code,
|
|
153
|
+
reason: event.reason,
|
|
154
|
+
wasClean: event.wasClean
|
|
155
|
+
});
|
|
156
|
+
this.ended(event.code, event.reason, event.wasClean);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
#record;
|
|
160
|
+
get binaryType() {
|
|
161
|
+
return this.#binaryType;
|
|
162
|
+
}
|
|
163
|
+
set binaryType(value) {
|
|
164
|
+
this.#binaryType = value;
|
|
165
|
+
}
|
|
166
|
+
get bufferedAmount() {
|
|
167
|
+
return this.#inner.bufferedAmount;
|
|
168
|
+
}
|
|
169
|
+
send(data) {
|
|
170
|
+
this.#inner.send(data);
|
|
171
|
+
if (typeof data === "string") this.#record({
|
|
172
|
+
type: "send",
|
|
173
|
+
data
|
|
174
|
+
});
|
|
175
|
+
else if (data instanceof Blob) {
|
|
176
|
+
const event = {
|
|
177
|
+
type: "send",
|
|
178
|
+
data: { binary: "" }
|
|
179
|
+
};
|
|
180
|
+
this.#record(event);
|
|
181
|
+
data.arrayBuffer().then((buffer) => Object.assign(event.data, encode(buffer)));
|
|
182
|
+
} else this.#record({
|
|
183
|
+
type: "send",
|
|
184
|
+
data: encode(data)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
close(code, reason) {
|
|
188
|
+
if (this.state >= CLOSING) return;
|
|
189
|
+
this.state = CLOSING;
|
|
190
|
+
this.#inner.close(code, reason);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return RecordingSocket;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A `WebSocket` class that plays a recording back, without a server: each connection the client
|
|
197
|
+
* opens replays the next recorded one, with its open, messages, errors and close, at the recorded
|
|
198
|
+
* timing or faster. For reproducing a bug report, offline demos, and UI tests that don't call a
|
|
199
|
+
* live backend, such as an AI model, on every run.
|
|
200
|
+
*
|
|
201
|
+
* By default, what came after a send in the recording waits until the app sends too, so the
|
|
202
|
+
* replay follows the app. Wrap it with `createChaosWebSocket({ WebSocket: Replay })` to add
|
|
203
|
+
* faults.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* import { createReplayWebSocket } from 'react-websocket/replay';
|
|
208
|
+
* import recording from './fixtures/chat.json';
|
|
209
|
+
*
|
|
210
|
+
* const Replay = createReplayWebSocket(recording, { speed: Infinity });
|
|
211
|
+
* render(<Chat client={createWebSocket(url, { WebSocket: Replay })} />);
|
|
212
|
+
* await Replay.done;
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
function createReplayWebSocket(recording, options = {}) {
|
|
216
|
+
const speed = options.speed ?? 1;
|
|
217
|
+
const waitForSends = options.waitForSends ?? true;
|
|
218
|
+
let next = 0;
|
|
219
|
+
let sent = [];
|
|
220
|
+
let finish;
|
|
221
|
+
let done = new Promise((resolve) => finish = resolve);
|
|
222
|
+
const live = /* @__PURE__ */ new Set();
|
|
223
|
+
class ReplaySocket extends Base {
|
|
224
|
+
binaryType = "blob";
|
|
225
|
+
bufferedAmount = 0;
|
|
226
|
+
#events;
|
|
227
|
+
#index = 0;
|
|
228
|
+
#sent = [];
|
|
229
|
+
#recordedAt = 0;
|
|
230
|
+
#realAt = performance.now();
|
|
231
|
+
#timer;
|
|
232
|
+
#sends = 0;
|
|
233
|
+
#paused = false;
|
|
234
|
+
static get sent() {
|
|
235
|
+
return sent;
|
|
236
|
+
}
|
|
237
|
+
static get done() {
|
|
238
|
+
return done;
|
|
239
|
+
}
|
|
240
|
+
static reset() {
|
|
241
|
+
for (const socket of live) socket.#stop();
|
|
242
|
+
next = 0;
|
|
243
|
+
sent = [];
|
|
244
|
+
done = new Promise((resolve) => finish = resolve);
|
|
245
|
+
}
|
|
246
|
+
constructor(url, protocols) {
|
|
247
|
+
super(url);
|
|
248
|
+
const connection = recording.connections[next++];
|
|
249
|
+
sent.push(this.#sent);
|
|
250
|
+
this.#events = connection?.events ?? [{
|
|
251
|
+
t: 0,
|
|
252
|
+
type: "error"
|
|
253
|
+
}, {
|
|
254
|
+
t: 0,
|
|
255
|
+
type: "close",
|
|
256
|
+
code: 1006,
|
|
257
|
+
reason: "",
|
|
258
|
+
wasClean: false
|
|
259
|
+
}];
|
|
260
|
+
live.add(this);
|
|
261
|
+
this.#schedule();
|
|
262
|
+
}
|
|
263
|
+
send(data) {
|
|
264
|
+
if (this.state === CONNECTING) throw new DOMException("WebSocket is still in CONNECTING state", "InvalidStateError");
|
|
265
|
+
if (this.state !== OPEN) return;
|
|
266
|
+
this.#sent.push(typeof data === "string" ? data : data instanceof Blob ? { binary: "" } : encode(data));
|
|
267
|
+
if (this.#paused) {
|
|
268
|
+
this.#paused = false;
|
|
269
|
+
this.#schedule();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
close(code = 1e3, reason = "") {
|
|
273
|
+
if (this.state >= CLOSING) return;
|
|
274
|
+
this.state = CLOSING;
|
|
275
|
+
this.#stop();
|
|
276
|
+
setTimeout(() => this.ended(code, reason, true), 0);
|
|
277
|
+
}
|
|
278
|
+
#schedule() {
|
|
279
|
+
clearTimeout(this.#timer);
|
|
280
|
+
const event = this.#events[this.#index];
|
|
281
|
+
if (!event || this.state === CLOSED) return this.#finished();
|
|
282
|
+
if (event.type === "send") {
|
|
283
|
+
if (waitForSends) {
|
|
284
|
+
if (this.#sent.length <= this.#sends) return void (this.#paused = true);
|
|
285
|
+
this.#recordedAt = event.t;
|
|
286
|
+
this.#realAt = performance.now();
|
|
287
|
+
}
|
|
288
|
+
this.#index++;
|
|
289
|
+
this.#sends++;
|
|
290
|
+
return this.#schedule();
|
|
291
|
+
}
|
|
292
|
+
const wait = (event.t - this.#recordedAt) / speed - (performance.now() - this.#realAt);
|
|
293
|
+
this.#timer = setTimeout(() => this.#play(event), Math.max(0, Number.isFinite(wait) ? wait : 0));
|
|
294
|
+
}
|
|
295
|
+
#play(event) {
|
|
296
|
+
this.#index++;
|
|
297
|
+
this.#recordedAt = event.t;
|
|
298
|
+
this.#realAt = performance.now();
|
|
299
|
+
if (event.type === "open") this.opened(event.protocol);
|
|
300
|
+
else if (event.type === "message") this.received(decode(event.data, this.binaryType));
|
|
301
|
+
else if (event.type === "error") this.dispatchEvent(new Event("error"));
|
|
302
|
+
else if (event.type === "close") {
|
|
303
|
+
this.#stop();
|
|
304
|
+
return this.ended(event.code, event.reason, event.wasClean);
|
|
305
|
+
}
|
|
306
|
+
this.#schedule();
|
|
307
|
+
}
|
|
308
|
+
#stop() {
|
|
309
|
+
clearTimeout(this.#timer);
|
|
310
|
+
this.#index = this.#events.length;
|
|
311
|
+
this.#finished();
|
|
312
|
+
}
|
|
313
|
+
#finished() {
|
|
314
|
+
live.delete(this);
|
|
315
|
+
if (next >= recording.connections.length && !live.size) finish();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return ReplaySocket;
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
exports.createRecordingWebSocket = createRecordingWebSocket;
|
|
322
|
+
exports.createReplayWebSocket = createReplayWebSocket;
|
|
323
|
+
|
|
324
|
+
//# sourceMappingURL=replay.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.cjs","names":[],"sources":["../src/replay.ts"],"sourcesContent":["// An entry with no imports: it only implements the `WebSocket` interface.\n\n/** Data in a recording: text as it was, binary as base64. */\nexport type RecordedData = string | { readonly binary: string };\n\n/** One event of a recorded connection, `t` ms after it was created. */\nexport type RecordedEvent =\n | { readonly t: number; readonly type: 'open'; readonly protocol: string }\n | { readonly t: number; readonly type: 'send' | 'message'; readonly data: RecordedData }\n | { readonly t: number; readonly type: 'error' }\n | {\n readonly t: number;\n readonly type: 'close';\n readonly code: number;\n readonly reason: string;\n readonly wasClean: boolean;\n };\n\n/** A recorded event before its time is known. */\ntype Untimed = RecordedEvent extends infer E\n ? E extends RecordedEvent\n ? Omit<E, 't'>\n : never\n : never;\n\n/** A recorded session: every connection, in the order they were opened. Plain JSON. */\nexport interface WebSocketRecording {\n readonly format: 'react-websocket-recording';\n readonly version: 1;\n readonly connections: readonly {\n readonly url: string;\n /** When it was created, from `Date.now()`. */\n readonly at: number;\n readonly events: readonly RecordedEvent[];\n }[];\n}\n\n/** The class from {@link createRecordingWebSocket}: a `WebSocket` that records what it does. */\nexport interface RecordingWebSocket extends Statics {\n new (url: string | URL, protocols?: string | string[]): WebSocket;\n readonly prototype: WebSocket;\n /** Everything recorded so far, as a new plain object. */\n recording(): WebSocketRecording;\n /** Download the recording as a JSON file, e.g. from a \"Report a problem\" button. */\n save(filename?: string): void;\n /** Forget everything recorded so far. */\n clear(): void;\n}\n\n/** Options for {@link createReplayWebSocket}. */\nexport interface ReplayOptions {\n /**\n * How fast to replay: `1` is the recorded timing, `4` four times faster, `Infinity` as fast as\n * possible, each event in a task of its own.\n * @default 1\n */\n speed?: number;\n /**\n * Wait for the app to send before replaying what came after a send in the recording, so the\n * replay follows the app instead of the clock: a server's answer comes after the question,\n * however long the app takes to ask. `false` follows the recorded timing alone.\n * @default true\n */\n waitForSends?: boolean;\n}\n\n/** The class from {@link createReplayWebSocket}: a `WebSocket` that plays a recording back. */\nexport interface ReplayWebSocket extends Statics {\n new (url: string | URL, protocols?: string | string[]): WebSocket;\n readonly prototype: WebSocket;\n /** What the app sent during the replay, by connection, as recorded data. */\n readonly sent: readonly RecordedData[][];\n /** Resolves once every recorded connection has been played to its end. */\n readonly done: Promise<void>;\n /** Start again from the first recorded connection. */\n reset(): void;\n}\n\ninterface Statics {\n readonly CONNECTING: 0;\n readonly OPEN: 1;\n readonly CLOSING: 2;\n readonly CLOSED: 3;\n}\n\nconst CONNECTING = 0;\nconst OPEN = 1;\nconst CLOSING = 2;\nconst CLOSED = 3;\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\nfunction encode(bytes: ArrayBuffer | ArrayBufferView): RecordedData {\n const view =\n bytes instanceof ArrayBuffer\n ? new Uint8Array(bytes)\n : new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n let text = '';\n for (const byte of view) text += String.fromCharCode(byte);\n return { binary: btoa(text) };\n}\n\nfunction decode(data: RecordedData, binaryType: BinaryType): unknown {\n if (typeof data === 'string') return data;\n const buffer = Uint8Array.from(atob(data.binary), char => char.charCodeAt(0)).buffer;\n return binaryType === 'blob' ? new Blob([buffer]) : buffer;\n}\n\n/** Shared by both classes: the `WebSocket` surface over a state and some events. */\nclass Base extends EventTarget {\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 readonly url: string;\n readonly extensions = '';\n onopen: WebSocket['onopen'] = null;\n onmessage: WebSocket['onmessage'] = null;\n onerror: WebSocket['onerror'] = null;\n onclose: WebSocket['onclose'] = null;\n protocol = '';\n state: WebSocket['readyState'] = CONNECTING;\n\n constructor(url: string | URL) {\n super();\n this.url = String(url);\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 }\n\n get readyState() {\n return this.state;\n }\n\n opened(protocol: string) {\n this.state = OPEN;\n this.protocol = protocol;\n this.dispatchEvent(new Event('open'));\n }\n\n received(data: unknown) {\n if (this.state === OPEN) this.dispatchEvent(new MessageEvent('message', { data }));\n }\n\n ended(code: number, reason: string, wasClean: boolean) {\n if (this.state === CLOSED) return;\n this.state = CLOSED;\n this.dispatchEvent(closeEvent(code, reason, wasClean));\n }\n}\n\n/**\n * A `WebSocket` class that records what its connections do: when each one opened, every message\n * sent and received, errors and closes. Pass it as the `WebSocket` option, and get the session\n * back with `recording()`, or download it with `save()`, to attach to a bug report or replay\n * with {@link createReplayWebSocket}.\n *\n * Recordings hold every message, so they can hold tokens and personal data: record in\n * development, or ask before saving one from a user's session.\n *\n * @example\n * ```ts\n * import { createRecordingWebSocket } from 'react-websocket/replay';\n *\n * export const Recorder = createRecordingWebSocket();\n * export const live = createWebSocket(url, { WebSocket: Recorder });\n *\n * reportButton.onclick = () => Recorder.save('session.json');\n * ```\n */\nexport function createRecordingWebSocket(\n options: { WebSocket?: typeof WebSocket } = {}\n): RecordingWebSocket {\n let connections: { url: string; at: number; events: RecordedEvent[] }[] = [];\n\n class RecordingSocket extends Base implements WebSocket {\n #inner: WebSocket;\n #binaryType: BinaryType = 'blob';\n\n static recording(): WebSocketRecording {\n return JSON.parse(\n JSON.stringify({ format: 'react-websocket-recording', version: 1, connections })\n );\n }\n\n static save(filename = 'websocket-recording.json') {\n const url = URL.createObjectURL(\n new Blob([JSON.stringify(RecordingSocket.recording(), null, 2)], {\n type: 'application/json'\n })\n );\n const link = Object.assign(document.createElement('a'), { href: url, download: filename });\n link.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n }\n\n static clear() {\n connections = [];\n }\n\n constructor(url: string | URL, protocols?: string | string[]) {\n super(url);\n const Impl = options.WebSocket ?? globalThis.WebSocket;\n const inner = (this.#inner = new Impl(url, protocols));\n // Binary data arrives as an ArrayBuffer, to record it without waiting on a Blob.\n inner.binaryType = 'arraybuffer';\n const start = performance.now();\n const events: RecordedEvent[] = [];\n connections.push({ url: this.url, at: Date.now(), events });\n const record = (event: Untimed) =>\n events.push({ t: Math.round(performance.now() - start), ...event } as RecordedEvent);\n this.#record = record;\n inner.onopen = () => {\n record({ type: 'open', protocol: inner.protocol ?? '' });\n this.opened(inner.protocol);\n };\n inner.onmessage = event => {\n const data = event.data as string | ArrayBuffer;\n record({ type: 'message', data: typeof data === 'string' ? data : encode(data) });\n this.received(\n typeof data === 'string' || this.#binaryType !== 'blob' ? data : new Blob([data])\n );\n };\n inner.onerror = () => {\n record({ type: 'error' });\n this.dispatchEvent(new Event('error'));\n };\n inner.onclose = event => {\n record({ type: 'close', code: event.code, reason: event.reason, wasClean: event.wasClean });\n this.ended(event.code, event.reason, event.wasClean);\n };\n }\n\n #record: (event: Untimed) => void;\n\n get binaryType() {\n return this.#binaryType;\n }\n\n set binaryType(value) {\n this.#binaryType = value;\n }\n\n get bufferedAmount() {\n return this.#inner.bufferedAmount;\n }\n\n send(data: Parameters<WebSocket['send']>[0]) {\n this.#inner.send(data);\n if (typeof data === 'string') this.#record({ type: 'send', data });\n else if (data instanceof Blob) {\n // Recorded where it was sent; its bytes are filled in once read.\n const event = { type: 'send', data: { binary: '' } } as const;\n this.#record(event);\n void data.arrayBuffer().then(buffer => Object.assign(event.data, encode(buffer)));\n } else this.#record({ type: 'send', data: encode(data) });\n }\n\n close(code?: number, reason?: string) {\n if (this.state >= CLOSING) return;\n this.state = CLOSING;\n this.#inner.close(code, reason);\n }\n }\n\n return RecordingSocket as unknown as RecordingWebSocket;\n}\n\n/**\n * A `WebSocket` class that plays a recording back, without a server: each connection the client\n * opens replays the next recorded one, with its open, messages, errors and close, at the recorded\n * timing or faster. For reproducing a bug report, offline demos, and UI tests that don't call a\n * live backend, such as an AI model, on every run.\n *\n * By default, what came after a send in the recording waits until the app sends too, so the\n * replay follows the app. Wrap it with `createChaosWebSocket({ WebSocket: Replay })` to add\n * faults.\n *\n * @example\n * ```ts\n * import { createReplayWebSocket } from 'react-websocket/replay';\n * import recording from './fixtures/chat.json';\n *\n * const Replay = createReplayWebSocket(recording, { speed: Infinity });\n * render(<Chat client={createWebSocket(url, { WebSocket: Replay })} />);\n * await Replay.done;\n * ```\n */\nexport function createReplayWebSocket(\n recording: WebSocketRecording,\n options: ReplayOptions = {}\n): ReplayWebSocket {\n const speed = options.speed ?? 1;\n const waitForSends = options.waitForSends ?? true;\n let next = 0;\n let sent: RecordedData[][] = [];\n let finish: () => void;\n let done = new Promise<void>(resolve => (finish = resolve));\n const live = new Set<ReplaySocket>();\n\n class ReplaySocket extends Base implements WebSocket {\n binaryType: BinaryType = 'blob';\n readonly bufferedAmount = 0;\n #events: readonly RecordedEvent[];\n #index = 0;\n #sent: RecordedData[] = [];\n // The recorded time the timeline is at, and the real time it was there.\n #recordedAt = 0;\n #realAt = performance.now();\n #timer: ReturnType<typeof setTimeout> | undefined;\n // Recorded sends passed so far, and whether the timeline waits for the app to send.\n #sends = 0;\n #paused = false;\n\n static get sent() {\n return sent;\n }\n\n static get done() {\n return done;\n }\n\n static reset() {\n for (const socket of live) socket.#stop();\n next = 0;\n sent = [];\n done = new Promise<void>(resolve => (finish = resolve));\n }\n\n constructor(url: string | URL, protocols?: string | string[]) {\n super(url);\n void protocols;\n const connection = recording.connections[next++];\n sent.push(this.#sent);\n // More connections than were recorded: refused, like a server that's gone.\n this.#events = connection?.events ?? [\n { t: 0, type: 'error' },\n { t: 0, type: 'close', code: 1006, reason: '', wasClean: false }\n ];\n live.add(this);\n this.#schedule();\n }\n\n send(data: Parameters<WebSocket['send']>[0]) {\n if (this.state === CONNECTING) {\n throw new DOMException('WebSocket is still in CONNECTING state', 'InvalidStateError');\n }\n if (this.state !== OPEN) return;\n this.#sent.push(\n typeof data === 'string' ? data : data instanceof Blob ? { binary: '' } : encode(data)\n );\n if (this.#paused) {\n this.#paused = false;\n this.#schedule();\n }\n }\n\n close(code = 1000, reason = '') {\n if (this.state >= CLOSING) return;\n this.state = CLOSING;\n this.#stop();\n setTimeout(() => this.ended(code, reason, true), 0);\n }\n\n #schedule(): void {\n clearTimeout(this.#timer);\n const event = this.#events[this.#index];\n if (!event || this.state === CLOSED) return this.#finished();\n if (event.type === 'send') {\n if (waitForSends) {\n // Wait until the app has sent as many messages, then go on from that moment.\n if (this.#sent.length <= this.#sends) return void (this.#paused = true);\n this.#recordedAt = event.t;\n this.#realAt = performance.now();\n }\n this.#index++;\n this.#sends++;\n return this.#schedule();\n }\n const wait = (event.t - this.#recordedAt) / speed - (performance.now() - this.#realAt);\n this.#timer = setTimeout(\n () => this.#play(event),\n Math.max(0, Number.isFinite(wait) ? wait : 0)\n );\n }\n\n #play(event: RecordedEvent) {\n this.#index++;\n this.#recordedAt = event.t;\n this.#realAt = performance.now();\n if (event.type === 'open') this.opened(event.protocol);\n else if (event.type === 'message') this.received(decode(event.data, this.binaryType));\n else if (event.type === 'error') this.dispatchEvent(new Event('error'));\n else if (event.type === 'close') {\n this.#stop();\n return this.ended(event.code, event.reason, event.wasClean);\n }\n this.#schedule();\n }\n\n #stop() {\n clearTimeout(this.#timer);\n this.#index = this.#events.length;\n this.#finished();\n }\n\n #finished() {\n live.delete(this);\n if (next >= recording.connections.length && !live.size) finish();\n }\n }\n\n return ReplaySocket as unknown as ReplayWebSocket;\n}\n"],"mappings":";;AAqFA,MAAM,aAAa;AACnB,MAAM,OAAO;AACb,MAAM,UAAU;AAChB,MAAM,SAAS;AAEf,SAAS,WAAW,MAAc,QAAgB,UAA0B;CAC1E,MAAM,OAAO;EAAE;EAAM;EAAQ;CAAS;CAEtC,OAAO,OAAO,eAAe,aACzB,IAAI,WAAW,SAAS,IAAI,IAC5B,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,IAAI;AAC5C;AAEA,SAAS,OAAO,OAAoD;CAClE,MAAM,OACJ,iBAAiB,cACb,IAAI,WAAW,KAAK,IACpB,IAAI,WAAW,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;CACrE,IAAI,OAAO;CACX,KAAK,MAAM,QAAQ,MAAM,QAAQ,OAAO,aAAa,IAAI;CACzD,OAAO,EAAE,QAAQ,KAAK,IAAI,EAAE;AAC9B;AAEA,SAAS,OAAO,MAAoB,YAAiC;CACnE,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,MAAM,SAAS,WAAW,KAAK,KAAK,KAAK,MAAM,IAAG,SAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC;CAC9E,OAAO,eAAe,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI;AACtD;;AAGA,IAAM,OAAN,cAAmB,YAAY;CAC7B,OAAgB,aAAa;CAC7B,OAAgB,OAAO;CACvB,OAAgB,UAAU;CAC1B,OAAgB,SAAS;CACzB,aAAsB;CACtB,OAAgB;CAChB,UAAmB;CACnB,SAAkB;CAClB;CACA,aAAsB;CACtB,SAA8B;CAC9B,YAAoC;CACpC,UAAgC;CAChC,UAAgC;CAChC,WAAW;CACX,QAAiC;CAEjC,YAAY,KAAmB;EAC7B,MAAM;EACN,KAAK,MAAM,OAAO,GAAG;EACrB,KAAK,MAAM,QAAQ;GAAC;GAAQ;GAAW;GAAS;EAAO,GACrD,KAAK,iBAAiB,OAAM,UACzB,KAAK,KAAK,OAAO,EAAsC,KAAK,MAAM,KAAK,CAC1E;CAEJ;CAEA,IAAI,aAAa;EACf,OAAO,KAAK;CACd;CAEA,OAAO,UAAkB;EACvB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,KAAK,cAAc,IAAI,MAAM,MAAM,CAAC;CACtC;CAEA,SAAS,MAAe;EACtB,IAAI,KAAK,UAAU,MAAM,KAAK,cAAc,IAAI,aAAa,WAAW,EAAE,KAAK,CAAC,CAAC;CACnF;CAEA,MAAM,MAAc,QAAgB,UAAmB;EACrD,IAAI,KAAK,UAAU,QAAQ;EAC3B,KAAK,QAAQ;EACb,KAAK,cAAc,WAAW,MAAM,QAAQ,QAAQ,CAAC;CACvD;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,yBACd,UAA4C,CAAC,GACzB;CACpB,IAAI,cAAsE,CAAC;CAE3E,MAAM,wBAAwB,KAA0B;EACtD;EACA,cAA0B;EAE1B,OAAO,YAAgC;GACrC,OAAO,KAAK,MACV,KAAK,UAAU;IAAE,QAAQ;IAA6B,SAAS;IAAG;GAAY,CAAC,CACjF;EACF;EAEA,OAAO,KAAK,WAAW,4BAA4B;GACjD,MAAM,MAAM,IAAI,gBACd,IAAI,KAAK,CAAC,KAAK,UAAU,gBAAgB,UAAU,GAAG,MAAM,CAAC,CAAC,GAAG,EAC/D,MAAM,mBACR,CAAC,CACH;GAEA,OADoB,OAAO,SAAS,cAAc,GAAG,GAAG;IAAE,MAAM;IAAK,UAAU;GAAS,CACrF,CAAC,CAAC,MAAM;GACX,iBAAiB,IAAI,gBAAgB,GAAG,GAAG,CAAC;EAC9C;EAEA,OAAO,QAAQ;GACb,cAAc,CAAC;EACjB;EAEA,YAAY,KAAmB,WAA+B;GAC5D,MAAM,GAAG;GACT,MAAM,OAAO,QAAQ,aAAa,WAAW;GAC7C,MAAM,QAAS,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;GAEpD,MAAM,aAAa;GACnB,MAAM,QAAQ,YAAY,IAAI;GAC9B,MAAM,SAA0B,CAAC;GACjC,YAAY,KAAK;IAAE,KAAK,KAAK;IAAK,IAAI,KAAK,IAAI;IAAG;GAAO,CAAC;GAC1D,MAAM,UAAU,UACd,OAAO,KAAK;IAAE,GAAG,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;IAAG,GAAG;GAAM,CAAkB;GACrF,KAAK,UAAU;GACf,MAAM,eAAe;IACnB,OAAO;KAAE,MAAM;KAAQ,UAAU,MAAM,YAAY;IAAG,CAAC;IACvD,KAAK,OAAO,MAAM,QAAQ;GAC5B;GACA,MAAM,aAAY,UAAS;IACzB,MAAM,OAAO,MAAM;IACnB,OAAO;KAAE,MAAM;KAAW,MAAM,OAAO,SAAS,WAAW,OAAO,OAAO,IAAI;IAAE,CAAC;IAChF,KAAK,SACH,OAAO,SAAS,YAAY,KAAK,gBAAgB,SAAS,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,CAClF;GACF;GACA,MAAM,gBAAgB;IACpB,OAAO,EAAE,MAAM,QAAQ,CAAC;IACxB,KAAK,cAAc,IAAI,MAAM,OAAO,CAAC;GACvC;GACA,MAAM,WAAU,UAAS;IACvB,OAAO;KAAE,MAAM;KAAS,MAAM,MAAM;KAAM,QAAQ,MAAM;KAAQ,UAAU,MAAM;IAAS,CAAC;IAC1F,KAAK,MAAM,MAAM,MAAM,MAAM,QAAQ,MAAM,QAAQ;GACrD;EACF;EAEA;EAEA,IAAI,aAAa;GACf,OAAO,KAAK;EACd;EAEA,IAAI,WAAW,OAAO;GACpB,KAAK,cAAc;EACrB;EAEA,IAAI,iBAAiB;GACnB,OAAO,KAAK,OAAO;EACrB;EAEA,KAAK,MAAwC;GAC3C,KAAK,OAAO,KAAK,IAAI;GACrB,IAAI,OAAO,SAAS,UAAU,KAAK,QAAQ;IAAE,MAAM;IAAQ;GAAK,CAAC;QAC5D,IAAI,gBAAgB,MAAM;IAE7B,MAAM,QAAQ;KAAE,MAAM;KAAQ,MAAM,EAAE,QAAQ,GAAG;IAAE;IACnD,KAAK,QAAQ,KAAK;IAClB,KAAU,YAAY,CAAC,CAAC,MAAK,WAAU,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM,CAAC,CAAC;GAClF,OAAO,KAAK,QAAQ;IAAE,MAAM;IAAQ,MAAM,OAAO,IAAI;GAAE,CAAC;EAC1D;EAEA,MAAM,MAAe,QAAiB;GACpC,IAAI,KAAK,SAAS,SAAS;GAC3B,KAAK,QAAQ;GACb,KAAK,OAAO,MAAM,MAAM,MAAM;EAChC;CACF;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,sBACd,WACA,UAAyB,CAAC,GACT;CACjB,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,IAAI,OAAO;CACX,IAAI,OAAyB,CAAC;CAC9B,IAAI;CACJ,IAAI,OAAO,IAAI,SAAc,YAAY,SAAS,OAAQ;CAC1D,MAAM,uBAAO,IAAI,IAAkB;CAEnC,MAAM,qBAAqB,KAA0B;EACnD,aAAyB;EACzB,iBAA0B;EAC1B;EACA,SAAS;EACT,QAAwB,CAAC;EAEzB,cAAc;EACd,UAAU,YAAY,IAAI;EAC1B;EAEA,SAAS;EACT,UAAU;EAEV,WAAW,OAAO;GAChB,OAAO;EACT;EAEA,WAAW,OAAO;GAChB,OAAO;EACT;EAEA,OAAO,QAAQ;GACb,KAAK,MAAM,UAAU,MAAM,OAAO,MAAM;GACxC,OAAO;GACP,OAAO,CAAC;GACR,OAAO,IAAI,SAAc,YAAY,SAAS,OAAQ;EACxD;EAEA,YAAY,KAAmB,WAA+B;GAC5D,MAAM,GAAG;GAET,MAAM,aAAa,UAAU,YAAY;GACzC,KAAK,KAAK,KAAK,KAAK;GAEpB,KAAK,UAAU,YAAY,UAAU,CACnC;IAAE,GAAG;IAAG,MAAM;GAAQ,GACtB;IAAE,GAAG;IAAG,MAAM;IAAS,MAAM;IAAM,QAAQ;IAAI,UAAU;GAAM,CACjE;GACA,KAAK,IAAI,IAAI;GACb,KAAK,UAAU;EACjB;EAEA,KAAK,MAAwC;GAC3C,IAAI,KAAK,UAAU,YACjB,MAAM,IAAI,aAAa,0CAA0C,mBAAmB;GAEtF,IAAI,KAAK,UAAU,MAAM;GACzB,KAAK,MAAM,KACT,OAAO,SAAS,WAAW,OAAO,gBAAgB,OAAO,EAAE,QAAQ,GAAG,IAAI,OAAO,IAAI,CACvF;GACA,IAAI,KAAK,SAAS;IAChB,KAAK,UAAU;IACf,KAAK,UAAU;GACjB;EACF;EAEA,MAAM,OAAO,KAAM,SAAS,IAAI;GAC9B,IAAI,KAAK,SAAS,SAAS;GAC3B,KAAK,QAAQ;GACb,KAAK,MAAM;GACX,iBAAiB,KAAK,MAAM,MAAM,QAAQ,IAAI,GAAG,CAAC;EACpD;EAEA,YAAkB;GAChB,aAAa,KAAK,MAAM;GACxB,MAAM,QAAQ,KAAK,QAAQ,KAAK;GAChC,IAAI,CAAC,SAAS,KAAK,UAAU,QAAQ,OAAO,KAAK,UAAU;GAC3D,IAAI,MAAM,SAAS,QAAQ;IACzB,IAAI,cAAc;KAEhB,IAAI,KAAK,MAAM,UAAU,KAAK,QAAQ,OAAO,MAAM,KAAK,UAAU;KAClE,KAAK,cAAc,MAAM;KACzB,KAAK,UAAU,YAAY,IAAI;IACjC;IACA,KAAK;IACL,KAAK;IACL,OAAO,KAAK,UAAU;GACxB;GACA,MAAM,QAAQ,MAAM,IAAI,KAAK,eAAe,SAAS,YAAY,IAAI,IAAI,KAAK;GAC9E,KAAK,SAAS,iBACN,KAAK,MAAM,KAAK,GACtB,KAAK,IAAI,GAAG,OAAO,SAAS,IAAI,IAAI,OAAO,CAAC,CAC9C;EACF;EAEA,MAAM,OAAsB;GAC1B,KAAK;GACL,KAAK,cAAc,MAAM;GACzB,KAAK,UAAU,YAAY,IAAI;GAC/B,IAAI,MAAM,SAAS,QAAQ,KAAK,OAAO,MAAM,QAAQ;QAChD,IAAI,MAAM,SAAS,WAAW,KAAK,SAAS,OAAO,MAAM,MAAM,KAAK,UAAU,CAAC;QAC/E,IAAI,MAAM,SAAS,SAAS,KAAK,cAAc,IAAI,MAAM,OAAO,CAAC;QACjE,IAAI,MAAM,SAAS,SAAS;IAC/B,KAAK,MAAM;IACX,OAAO,KAAK,MAAM,MAAM,MAAM,MAAM,QAAQ,MAAM,QAAQ;GAC5D;GACA,KAAK,UAAU;EACjB;EAEA,QAAQ;GACN,aAAa,KAAK,MAAM;GACxB,KAAK,SAAS,KAAK,QAAQ;GAC3B,KAAK,UAAU;EACjB;EAEA,YAAY;GACV,KAAK,OAAO,IAAI;GAChB,IAAI,QAAQ,UAAU,YAAY,UAAU,CAAC,KAAK,MAAM,OAAO;EACjE;CACF;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//#region src/replay.d.ts
|
|
2
|
+
/** Data in a recording: text as it was, binary as base64. */
|
|
3
|
+
export type RecordedData = string | {
|
|
4
|
+
readonly binary: string;
|
|
5
|
+
};
|
|
6
|
+
/** One event of a recorded connection, `t` ms after it was created. */
|
|
7
|
+
export type RecordedEvent = {
|
|
8
|
+
readonly t: number;
|
|
9
|
+
readonly type: 'open';
|
|
10
|
+
readonly protocol: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly t: number;
|
|
13
|
+
readonly type: 'send' | 'message';
|
|
14
|
+
readonly data: RecordedData;
|
|
15
|
+
} | {
|
|
16
|
+
readonly t: number;
|
|
17
|
+
readonly type: 'error';
|
|
18
|
+
} | {
|
|
19
|
+
readonly t: number;
|
|
20
|
+
readonly type: 'close';
|
|
21
|
+
readonly code: number;
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
readonly wasClean: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** A recorded session: every connection, in the order they were opened. Plain JSON. */
|
|
26
|
+
export interface WebSocketRecording {
|
|
27
|
+
readonly format: 'react-websocket-recording';
|
|
28
|
+
readonly version: 1;
|
|
29
|
+
readonly connections: readonly {
|
|
30
|
+
readonly url: string;
|
|
31
|
+
/** When it was created, from `Date.now()`. */
|
|
32
|
+
readonly at: number;
|
|
33
|
+
readonly events: readonly RecordedEvent[];
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
/** The class from {@link createRecordingWebSocket}: a `WebSocket` that records what it does. */
|
|
37
|
+
export interface RecordingWebSocket extends Statics {
|
|
38
|
+
new (url: string | URL, protocols?: string | string[]): WebSocket;
|
|
39
|
+
readonly prototype: WebSocket;
|
|
40
|
+
/** Everything recorded so far, as a new plain object. */
|
|
41
|
+
recording(): WebSocketRecording;
|
|
42
|
+
/** Download the recording as a JSON file, e.g. from a "Report a problem" button. */
|
|
43
|
+
save(filename?: string): void;
|
|
44
|
+
/** Forget everything recorded so far. */
|
|
45
|
+
clear(): void;
|
|
46
|
+
}
|
|
47
|
+
/** Options for {@link createReplayWebSocket}. */
|
|
48
|
+
export interface ReplayOptions {
|
|
49
|
+
/**
|
|
50
|
+
* How fast to replay: `1` is the recorded timing, `4` four times faster, `Infinity` as fast as
|
|
51
|
+
* possible, each event in a task of its own.
|
|
52
|
+
* @default 1
|
|
53
|
+
*/
|
|
54
|
+
speed?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Wait for the app to send before replaying what came after a send in the recording, so the
|
|
57
|
+
* replay follows the app instead of the clock: a server's answer comes after the question,
|
|
58
|
+
* however long the app takes to ask. `false` follows the recorded timing alone.
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
waitForSends?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** The class from {@link createReplayWebSocket}: a `WebSocket` that plays a recording back. */
|
|
64
|
+
export interface ReplayWebSocket extends Statics {
|
|
65
|
+
new (url: string | URL, protocols?: string | string[]): WebSocket;
|
|
66
|
+
readonly prototype: WebSocket;
|
|
67
|
+
/** What the app sent during the replay, by connection, as recorded data. */
|
|
68
|
+
readonly sent: readonly RecordedData[][];
|
|
69
|
+
/** Resolves once every recorded connection has been played to its end. */
|
|
70
|
+
readonly done: Promise<void>;
|
|
71
|
+
/** Start again from the first recorded connection. */
|
|
72
|
+
reset(): void;
|
|
73
|
+
}
|
|
74
|
+
interface Statics {
|
|
75
|
+
readonly CONNECTING: 0;
|
|
76
|
+
readonly OPEN: 1;
|
|
77
|
+
readonly CLOSING: 2;
|
|
78
|
+
readonly CLOSED: 3;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A `WebSocket` class that records what its connections do: when each one opened, every message
|
|
82
|
+
* sent and received, errors and closes. Pass it as the `WebSocket` option, and get the session
|
|
83
|
+
* back with `recording()`, or download it with `save()`, to attach to a bug report or replay
|
|
84
|
+
* with {@link createReplayWebSocket}.
|
|
85
|
+
*
|
|
86
|
+
* Recordings hold every message, so they can hold tokens and personal data: record in
|
|
87
|
+
* development, or ask before saving one from a user's session.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { createRecordingWebSocket } from 'react-websocket/replay';
|
|
92
|
+
*
|
|
93
|
+
* export const Recorder = createRecordingWebSocket();
|
|
94
|
+
* export const live = createWebSocket(url, { WebSocket: Recorder });
|
|
95
|
+
*
|
|
96
|
+
* reportButton.onclick = () => Recorder.save('session.json');
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function createRecordingWebSocket(options?: {
|
|
100
|
+
WebSocket?: typeof WebSocket;
|
|
101
|
+
}): RecordingWebSocket;
|
|
102
|
+
/**
|
|
103
|
+
* A `WebSocket` class that plays a recording back, without a server: each connection the client
|
|
104
|
+
* opens replays the next recorded one, with its open, messages, errors and close, at the recorded
|
|
105
|
+
* timing or faster. For reproducing a bug report, offline demos, and UI tests that don't call a
|
|
106
|
+
* live backend, such as an AI model, on every run.
|
|
107
|
+
*
|
|
108
|
+
* By default, what came after a send in the recording waits until the app sends too, so the
|
|
109
|
+
* replay follows the app. Wrap it with `createChaosWebSocket({ WebSocket: Replay })` to add
|
|
110
|
+
* faults.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { createReplayWebSocket } from 'react-websocket/replay';
|
|
115
|
+
* import recording from './fixtures/chat.json';
|
|
116
|
+
*
|
|
117
|
+
* const Replay = createReplayWebSocket(recording, { speed: Infinity });
|
|
118
|
+
* render(<Chat client={createWebSocket(url, { WebSocket: Replay })} />);
|
|
119
|
+
* await Replay.done;
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function createReplayWebSocket(recording: WebSocketRecording, options?: ReplayOptions): ReplayWebSocket;
|
|
123
|
+
//#endregion
|
|
124
|
+
//# sourceMappingURL=replay.d.cts.map
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//#region src/replay.d.ts
|
|
2
|
+
/** Data in a recording: text as it was, binary as base64. */
|
|
3
|
+
export type RecordedData = string | {
|
|
4
|
+
readonly binary: string;
|
|
5
|
+
};
|
|
6
|
+
/** One event of a recorded connection, `t` ms after it was created. */
|
|
7
|
+
export type RecordedEvent = {
|
|
8
|
+
readonly t: number;
|
|
9
|
+
readonly type: 'open';
|
|
10
|
+
readonly protocol: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly t: number;
|
|
13
|
+
readonly type: 'send' | 'message';
|
|
14
|
+
readonly data: RecordedData;
|
|
15
|
+
} | {
|
|
16
|
+
readonly t: number;
|
|
17
|
+
readonly type: 'error';
|
|
18
|
+
} | {
|
|
19
|
+
readonly t: number;
|
|
20
|
+
readonly type: 'close';
|
|
21
|
+
readonly code: number;
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
readonly wasClean: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** A recorded session: every connection, in the order they were opened. Plain JSON. */
|
|
26
|
+
export interface WebSocketRecording {
|
|
27
|
+
readonly format: 'react-websocket-recording';
|
|
28
|
+
readonly version: 1;
|
|
29
|
+
readonly connections: readonly {
|
|
30
|
+
readonly url: string;
|
|
31
|
+
/** When it was created, from `Date.now()`. */
|
|
32
|
+
readonly at: number;
|
|
33
|
+
readonly events: readonly RecordedEvent[];
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
/** The class from {@link createRecordingWebSocket}: a `WebSocket` that records what it does. */
|
|
37
|
+
export interface RecordingWebSocket extends Statics {
|
|
38
|
+
new (url: string | URL, protocols?: string | string[]): WebSocket;
|
|
39
|
+
readonly prototype: WebSocket;
|
|
40
|
+
/** Everything recorded so far, as a new plain object. */
|
|
41
|
+
recording(): WebSocketRecording;
|
|
42
|
+
/** Download the recording as a JSON file, e.g. from a "Report a problem" button. */
|
|
43
|
+
save(filename?: string): void;
|
|
44
|
+
/** Forget everything recorded so far. */
|
|
45
|
+
clear(): void;
|
|
46
|
+
}
|
|
47
|
+
/** Options for {@link createReplayWebSocket}. */
|
|
48
|
+
export interface ReplayOptions {
|
|
49
|
+
/**
|
|
50
|
+
* How fast to replay: `1` is the recorded timing, `4` four times faster, `Infinity` as fast as
|
|
51
|
+
* possible, each event in a task of its own.
|
|
52
|
+
* @default 1
|
|
53
|
+
*/
|
|
54
|
+
speed?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Wait for the app to send before replaying what came after a send in the recording, so the
|
|
57
|
+
* replay follows the app instead of the clock: a server's answer comes after the question,
|
|
58
|
+
* however long the app takes to ask. `false` follows the recorded timing alone.
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
waitForSends?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** The class from {@link createReplayWebSocket}: a `WebSocket` that plays a recording back. */
|
|
64
|
+
export interface ReplayWebSocket extends Statics {
|
|
65
|
+
new (url: string | URL, protocols?: string | string[]): WebSocket;
|
|
66
|
+
readonly prototype: WebSocket;
|
|
67
|
+
/** What the app sent during the replay, by connection, as recorded data. */
|
|
68
|
+
readonly sent: readonly RecordedData[][];
|
|
69
|
+
/** Resolves once every recorded connection has been played to its end. */
|
|
70
|
+
readonly done: Promise<void>;
|
|
71
|
+
/** Start again from the first recorded connection. */
|
|
72
|
+
reset(): void;
|
|
73
|
+
}
|
|
74
|
+
interface Statics {
|
|
75
|
+
readonly CONNECTING: 0;
|
|
76
|
+
readonly OPEN: 1;
|
|
77
|
+
readonly CLOSING: 2;
|
|
78
|
+
readonly CLOSED: 3;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A `WebSocket` class that records what its connections do: when each one opened, every message
|
|
82
|
+
* sent and received, errors and closes. Pass it as the `WebSocket` option, and get the session
|
|
83
|
+
* back with `recording()`, or download it with `save()`, to attach to a bug report or replay
|
|
84
|
+
* with {@link createReplayWebSocket}.
|
|
85
|
+
*
|
|
86
|
+
* Recordings hold every message, so they can hold tokens and personal data: record in
|
|
87
|
+
* development, or ask before saving one from a user's session.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { createRecordingWebSocket } from 'react-websocket/replay';
|
|
92
|
+
*
|
|
93
|
+
* export const Recorder = createRecordingWebSocket();
|
|
94
|
+
* export const live = createWebSocket(url, { WebSocket: Recorder });
|
|
95
|
+
*
|
|
96
|
+
* reportButton.onclick = () => Recorder.save('session.json');
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function createRecordingWebSocket(options?: {
|
|
100
|
+
WebSocket?: typeof WebSocket;
|
|
101
|
+
}): RecordingWebSocket;
|
|
102
|
+
/**
|
|
103
|
+
* A `WebSocket` class that plays a recording back, without a server: each connection the client
|
|
104
|
+
* opens replays the next recorded one, with its open, messages, errors and close, at the recorded
|
|
105
|
+
* timing or faster. For reproducing a bug report, offline demos, and UI tests that don't call a
|
|
106
|
+
* live backend, such as an AI model, on every run.
|
|
107
|
+
*
|
|
108
|
+
* By default, what came after a send in the recording waits until the app sends too, so the
|
|
109
|
+
* replay follows the app. Wrap it with `createChaosWebSocket({ WebSocket: Replay })` to add
|
|
110
|
+
* faults.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { createReplayWebSocket } from 'react-websocket/replay';
|
|
115
|
+
* import recording from './fixtures/chat.json';
|
|
116
|
+
*
|
|
117
|
+
* const Replay = createReplayWebSocket(recording, { speed: Infinity });
|
|
118
|
+
* render(<Chat client={createWebSocket(url, { WebSocket: Replay })} />);
|
|
119
|
+
* await Replay.done;
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function createReplayWebSocket(recording: WebSocketRecording, options?: ReplayOptions): ReplayWebSocket;
|
|
123
|
+
//#endregion
|
|
124
|
+
//# sourceMappingURL=replay.d.ts.map
|