react-websocket 3.2.0 → 3.3.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/LICENSE +1 -0
- package/README.md +8 -4
- package/dist/core.cjs +1013 -759
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +525 -374
- package/dist/core.d.ts +525 -374
- package/dist/core.js +1006 -727
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +217 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +585 -410
- package/dist/index.d.ts +423 -524
- package/dist/index.js +204 -121
- package/dist/index.js.map +1 -1
- package/package.json +43 -28
package/dist/core.cjs
CHANGED
|
@@ -1,783 +1,1037 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/core.ts
|
|
21
|
-
var core_exports = {};
|
|
22
|
-
__export(core_exports, {
|
|
23
|
-
MessageError: () => MessageError,
|
|
24
|
-
createWebSocket: () => createWebSocket,
|
|
25
|
-
createWebSocketLatency: () => createWebSocketLatency,
|
|
26
|
-
createWebSocketRequests: () => createWebSocketRequests,
|
|
27
|
-
createWebSocketState: () => createWebSocketState,
|
|
28
|
-
createWebSocketTopics: () => createWebSocketTopics
|
|
29
|
-
});
|
|
30
|
-
module.exports = __toCommonJS(core_exports);
|
|
31
|
-
|
|
32
|
-
// src/types.ts
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/types.ts
|
|
3
|
+
/**
|
|
4
|
+
* Reported via the `error` event (and `onError`) when an incoming message can't be
|
|
5
|
+
* decoded by `parse`/`json` or fails `schema` validation. The message is not delivered.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* onError(error) {
|
|
10
|
+
* if (error instanceof MessageError) report(error.data, error.issues);
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
33
14
|
var MessageError = class extends Error {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
15
|
+
/** The raw `event.data` that failed. */
|
|
16
|
+
data;
|
|
17
|
+
/** Schema validation issues, when the message failed `schema`. */
|
|
18
|
+
issues;
|
|
19
|
+
constructor(message, data, options) {
|
|
20
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
21
|
+
this.name = "MessageError";
|
|
22
|
+
this.data = data;
|
|
23
|
+
this.issues = options?.issues;
|
|
24
|
+
}
|
|
44
25
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/client.ts
|
|
28
|
+
const OPEN = 1;
|
|
29
|
+
const RECONNECT_DEFAULTS = {
|
|
30
|
+
maxAttempts: Infinity,
|
|
31
|
+
minDelay: 1e3,
|
|
32
|
+
maxDelay: 3e4,
|
|
33
|
+
factor: 2,
|
|
34
|
+
jitter: .5
|
|
54
35
|
};
|
|
36
|
+
const READY_GRACE = 5e3;
|
|
55
37
|
function createDeferred() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return deferred;
|
|
38
|
+
let resolve;
|
|
39
|
+
let reject;
|
|
40
|
+
const promise = new Promise((res, rej) => {
|
|
41
|
+
resolve = res;
|
|
42
|
+
reject = rej;
|
|
43
|
+
});
|
|
44
|
+
promise.status = "pending";
|
|
45
|
+
promise.catch(() => {});
|
|
46
|
+
const deferred = {
|
|
47
|
+
promise,
|
|
48
|
+
settled: false,
|
|
49
|
+
resolve() {
|
|
50
|
+
if (deferred.settled) return;
|
|
51
|
+
deferred.settled = true;
|
|
52
|
+
promise.status = "fulfilled";
|
|
53
|
+
promise.value = void 0;
|
|
54
|
+
resolve();
|
|
55
|
+
},
|
|
56
|
+
reject(reason) {
|
|
57
|
+
if (deferred.settled) return;
|
|
58
|
+
deferred.settled = true;
|
|
59
|
+
promise.status = "rejected";
|
|
60
|
+
promise.reason = reason;
|
|
61
|
+
reject(reason);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return deferred;
|
|
84
65
|
}
|
|
85
66
|
function rethrowAsync(error) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
throw error;
|
|
69
|
+
});
|
|
89
70
|
}
|
|
90
71
|
function isThenable(value) {
|
|
91
|
-
|
|
72
|
+
return typeof value?.then === "function";
|
|
92
73
|
}
|
|
93
|
-
|
|
74
|
+
/**
|
|
75
|
+
* @internal Option key for a callback run when the client tears down, which lets the hooks'
|
|
76
|
+
* URL registry forget clients nobody holds anymore. A registered symbol rather than an export,
|
|
77
|
+
* because the hooks entry imports the client from the `core` entry at runtime.
|
|
78
|
+
*/
|
|
79
|
+
const ON_TEARDOWN = Symbol.for("react-websocket.onTeardown");
|
|
94
80
|
function createClient(url, options = {}) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
81
|
+
const onTeardown = options[ON_TEARDOWN];
|
|
82
|
+
const log = options.logger ?? (() => {});
|
|
83
|
+
const retry = options.reconnect === false ? null : {
|
|
84
|
+
...RECONNECT_DEFAULTS,
|
|
85
|
+
...typeof options.reconnect === "object" ? options.reconnect : {}
|
|
86
|
+
};
|
|
87
|
+
const queueLimit = options.queue === false ? 0 : typeof options.queue === "object" ? Math.max(0, options.queue.limit ?? Infinity) : Infinity;
|
|
88
|
+
let snapshot = {
|
|
89
|
+
status: "idle",
|
|
90
|
+
lastMessage: void 0,
|
|
91
|
+
attempt: 0
|
|
92
|
+
};
|
|
93
|
+
const storeListeners = /* @__PURE__ */ new Set();
|
|
94
|
+
const listeners = {
|
|
95
|
+
open: /* @__PURE__ */ new Set(),
|
|
96
|
+
message: /* @__PURE__ */ new Set(),
|
|
97
|
+
close: /* @__PURE__ */ new Set(),
|
|
98
|
+
error: /* @__PURE__ */ new Set()
|
|
99
|
+
};
|
|
100
|
+
let ws = null;
|
|
101
|
+
let wanted = false;
|
|
102
|
+
let refs = 0;
|
|
103
|
+
let pinned = false;
|
|
104
|
+
let pending = [];
|
|
105
|
+
let ready = null;
|
|
106
|
+
let opened = false;
|
|
107
|
+
let unavailable = null;
|
|
108
|
+
let startScheduled = false;
|
|
109
|
+
let inbox = null;
|
|
110
|
+
let generation = 0;
|
|
111
|
+
let windowBound = false;
|
|
112
|
+
let retryTimer;
|
|
113
|
+
let teardownTimer;
|
|
114
|
+
let heartbeatTimer;
|
|
115
|
+
let pongTimer;
|
|
116
|
+
function set(patch) {
|
|
117
|
+
snapshot = {
|
|
118
|
+
...snapshot,
|
|
119
|
+
...patch
|
|
120
|
+
};
|
|
121
|
+
for (const listener of [...storeListeners]) try {
|
|
122
|
+
listener();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
rethrowAsync(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function emit(type, ...args) {
|
|
128
|
+
for (const listener of [...listeners[type]]) try {
|
|
129
|
+
listener(...args);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
rethrowAsync(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function getImpl() {
|
|
135
|
+
if (options.WebSocket) return options.WebSocket;
|
|
136
|
+
return typeof window === "undefined" ? void 0 : globalThis.WebSocket;
|
|
137
|
+
}
|
|
138
|
+
function rejectReady(reason) {
|
|
139
|
+
if (!ready || ready.settled) ready = createDeferred();
|
|
140
|
+
ready.reject(reason);
|
|
141
|
+
}
|
|
142
|
+
function retryNow() {
|
|
143
|
+
if (retryTimer !== void 0) {
|
|
144
|
+
log("network or visibility change, retrying now");
|
|
145
|
+
open();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function onVisibilityChange() {
|
|
149
|
+
if (document.visibilityState === "visible") retryNow();
|
|
150
|
+
}
|
|
151
|
+
function bindWindow() {
|
|
152
|
+
if (windowBound || typeof window === "undefined") return;
|
|
153
|
+
windowBound = true;
|
|
154
|
+
window.addEventListener("online", retryNow);
|
|
155
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
156
|
+
}
|
|
157
|
+
function unbindWindow() {
|
|
158
|
+
if (!windowBound) return;
|
|
159
|
+
windowBound = false;
|
|
160
|
+
window.removeEventListener("online", retryNow);
|
|
161
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
162
|
+
}
|
|
163
|
+
function startHeartbeat(socket) {
|
|
164
|
+
const heartbeat = options.heartbeat;
|
|
165
|
+
if (!heartbeat) return;
|
|
166
|
+
heartbeatTimer = setInterval(() => {
|
|
167
|
+
socket.send(heartbeat.message ?? "ping");
|
|
168
|
+
if (heartbeat.timeout && pongTimer === void 0) pongTimer = setTimeout(() => {
|
|
169
|
+
log("heartbeat timeout");
|
|
170
|
+
detach(socket, false);
|
|
171
|
+
socket.close(4e3, "heartbeat timeout");
|
|
172
|
+
handleClose({
|
|
173
|
+
code: 4e3,
|
|
174
|
+
reason: "heartbeat timeout",
|
|
175
|
+
wasClean: false
|
|
176
|
+
});
|
|
177
|
+
}, heartbeat.timeout);
|
|
178
|
+
}, heartbeat.interval);
|
|
179
|
+
}
|
|
180
|
+
function stopHeartbeat() {
|
|
181
|
+
clearInterval(heartbeatTimer);
|
|
182
|
+
clearTimeout(pongTimer);
|
|
183
|
+
heartbeatTimer = pongTimer = void 0;
|
|
184
|
+
}
|
|
185
|
+
/** Stop tracking `socket`. Its eventual close event is still reported unless `reportClose` is false. */
|
|
186
|
+
function detach(socket, reportClose) {
|
|
187
|
+
if (ws === socket) ws = null;
|
|
188
|
+
socket.onopen = socket.onmessage = socket.onerror = null;
|
|
189
|
+
socket.onclose = reportClose ? (event) => emit("close", {
|
|
190
|
+
code: event.code,
|
|
191
|
+
reason: event.reason,
|
|
192
|
+
wasClean: event.wasClean
|
|
193
|
+
}) : null;
|
|
194
|
+
}
|
|
195
|
+
function backoff(attempt) {
|
|
196
|
+
const { minDelay, maxDelay, factor, jitter } = retry;
|
|
197
|
+
const base = Math.min(maxDelay, minDelay * factor ** (attempt - 1));
|
|
198
|
+
return Math.round(base * (1 - jitter * Math.random()));
|
|
199
|
+
}
|
|
200
|
+
function open() {
|
|
201
|
+
clearTimeout(retryTimer);
|
|
202
|
+
retryTimer = void 0;
|
|
203
|
+
const Impl = getImpl();
|
|
204
|
+
if (!Impl) return;
|
|
205
|
+
const id = ++generation;
|
|
206
|
+
set({ status: snapshot.attempt === 0 ? "connecting" : "reconnecting" });
|
|
207
|
+
let target;
|
|
208
|
+
try {
|
|
209
|
+
target = typeof url === "string" ? url : url();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
urlFailed(error);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (typeof target === "string") {
|
|
215
|
+
start(Impl, target);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
Promise.resolve(target).then((resolved) => {
|
|
219
|
+
if (id === generation && wanted) start(Impl, resolved);
|
|
220
|
+
}, (error) => {
|
|
221
|
+
if (id === generation && wanted) urlFailed(error);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** `url()` threw or rejected (e.g. a token refresh failed): count it as a failed attempt. */
|
|
225
|
+
function urlFailed(error) {
|
|
226
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
227
|
+
log("url() failed", reason);
|
|
228
|
+
emit("error", reason);
|
|
229
|
+
retryOrFail({
|
|
230
|
+
code: 1006,
|
|
231
|
+
reason: "url() failed",
|
|
232
|
+
wasClean: false
|
|
233
|
+
}, reason);
|
|
234
|
+
}
|
|
235
|
+
function start(Impl, target) {
|
|
236
|
+
let socket;
|
|
237
|
+
try {
|
|
238
|
+
socket = new Impl(target, options.protocols);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
emit("error", error);
|
|
241
|
+
fail(error);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (options.binaryType) socket.binaryType = options.binaryType;
|
|
245
|
+
ws = socket;
|
|
246
|
+
log("connecting", target);
|
|
247
|
+
socket.onopen = (event) => {
|
|
248
|
+
log("open");
|
|
249
|
+
opened = true;
|
|
250
|
+
set({
|
|
251
|
+
status: "open",
|
|
252
|
+
attempt: 0
|
|
253
|
+
});
|
|
254
|
+
startHeartbeat(socket);
|
|
255
|
+
ready?.resolve();
|
|
256
|
+
if (refs === 0 && !pinned) {
|
|
257
|
+
clearTimeout(teardownTimer);
|
|
258
|
+
teardownTimer = setTimeout(teardown, READY_GRACE);
|
|
259
|
+
}
|
|
260
|
+
emit("open", event);
|
|
261
|
+
if (ws !== socket) return;
|
|
262
|
+
const queued = pending;
|
|
263
|
+
pending = [];
|
|
264
|
+
for (const { data } of queued) socket.send(data);
|
|
265
|
+
};
|
|
266
|
+
socket.onmessage = (event) => receive(event, socket);
|
|
267
|
+
socket.onerror = (event) => emit("error", event);
|
|
268
|
+
socket.onclose = (event) => {
|
|
269
|
+
detach(socket, false);
|
|
270
|
+
handleClose({
|
|
271
|
+
code: event.code,
|
|
272
|
+
reason: event.reason,
|
|
273
|
+
wasClean: event.wasClean
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function handleClose(info) {
|
|
278
|
+
stopHeartbeat();
|
|
279
|
+
log("closed", info);
|
|
280
|
+
retryOrFail(info);
|
|
281
|
+
emit("close", info);
|
|
282
|
+
}
|
|
283
|
+
function retryOrFail(info, error) {
|
|
284
|
+
if (wanted && retry && snapshot.attempt < retry.maxAttempts && (retry.shouldReconnect?.(info) ?? true)) {
|
|
285
|
+
const attempt = snapshot.attempt + 1;
|
|
286
|
+
const delay = backoff(attempt);
|
|
287
|
+
set({
|
|
288
|
+
status: "reconnecting",
|
|
289
|
+
attempt
|
|
290
|
+
});
|
|
291
|
+
log(`reconnecting in ${delay}ms (attempt ${attempt})`);
|
|
292
|
+
retryTimer = setTimeout(open, delay);
|
|
293
|
+
} else fail(error ?? /* @__PURE__ */ new Error(`WebSocket closed (code ${info.code}${info.reason ? `: ${info.reason}` : ""})`));
|
|
294
|
+
}
|
|
295
|
+
function fail(reason) {
|
|
296
|
+
wanted = false;
|
|
297
|
+
opened = false;
|
|
298
|
+
unbindWindow();
|
|
299
|
+
pending = [];
|
|
300
|
+
set({ status: "closed" });
|
|
301
|
+
rejectReady(reason);
|
|
302
|
+
}
|
|
303
|
+
function receive(event, socket) {
|
|
304
|
+
clearTimeout(pongTimer);
|
|
305
|
+
pongTimer = void 0;
|
|
306
|
+
if (options.heartbeat?.pong !== void 0 && event.data === options.heartbeat.pong) return;
|
|
307
|
+
const run = () => decode(event, socket);
|
|
308
|
+
if (inbox) {
|
|
309
|
+
const next = inbox.then(run).catch(rethrowAsync).then(() => {
|
|
310
|
+
if (inbox === next) inbox = null;
|
|
311
|
+
});
|
|
312
|
+
inbox = next;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const result = run();
|
|
316
|
+
if (result) {
|
|
317
|
+
const next = result.catch(rethrowAsync).then(() => {
|
|
318
|
+
if (inbox === next) inbox = null;
|
|
319
|
+
});
|
|
320
|
+
inbox = next;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function decode(event, socket) {
|
|
324
|
+
let value;
|
|
325
|
+
try {
|
|
326
|
+
value = options.parse ? options.parse(event.data) : options.json ? JSON.parse(event.data) : event.data;
|
|
327
|
+
} catch (cause) {
|
|
328
|
+
emit("error", new MessageError("Failed to parse message", event.data, { cause }));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const schema = options.schema;
|
|
332
|
+
if (!schema) return deliver(value, event, socket);
|
|
333
|
+
let result;
|
|
334
|
+
try {
|
|
335
|
+
result = schema["~standard"].validate(value);
|
|
336
|
+
} catch (cause) {
|
|
337
|
+
emit("error", new MessageError("Schema validation threw", event.data, { cause }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (isThenable(result)) return Promise.resolve(result).then((r) => validated(r, event, socket), (cause) => emit("error", new MessageError("Schema validation threw", event.data, { cause })));
|
|
341
|
+
validated(result, event, socket);
|
|
342
|
+
}
|
|
343
|
+
function validated(result, event, socket) {
|
|
344
|
+
if (result.issues) emit("error", new MessageError("Message failed schema validation", event.data, { issues: result.issues }));
|
|
345
|
+
else deliver(result.value, event, socket);
|
|
346
|
+
}
|
|
347
|
+
function deliver(message, event, socket) {
|
|
348
|
+
if (ws !== socket) return;
|
|
349
|
+
set({ lastMessage: message });
|
|
350
|
+
emit("message", message, event);
|
|
351
|
+
}
|
|
352
|
+
function send(message, sendOptions) {
|
|
353
|
+
const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
|
|
354
|
+
if (ws && ws.readyState === OPEN) {
|
|
355
|
+
ws.send(data);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
if (queueLimit === 0 || sendOptions?.volatile) {
|
|
359
|
+
log("dropped message: socket is not open");
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (!getImpl()) {
|
|
363
|
+
log("dropped message: no WebSocket implementation available");
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const key = sendOptions?.key;
|
|
367
|
+
if (key !== void 0) pending = pending.filter((entry) => {
|
|
368
|
+
if (entry.key !== key) return true;
|
|
369
|
+
dropped(entry);
|
|
370
|
+
return false;
|
|
371
|
+
});
|
|
372
|
+
pending.push({
|
|
373
|
+
data,
|
|
374
|
+
key,
|
|
375
|
+
onDrop: sendOptions?.onDrop
|
|
376
|
+
});
|
|
377
|
+
if (pending.length > queueLimit) {
|
|
378
|
+
dropped(pending.shift());
|
|
379
|
+
log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
function dropped(entry) {
|
|
384
|
+
try {
|
|
385
|
+
entry.onDrop?.();
|
|
386
|
+
} catch (error) {
|
|
387
|
+
rethrowAsync(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function begin() {
|
|
391
|
+
wanted = true;
|
|
392
|
+
if (snapshot.status === "idle" || snapshot.status === "closed") {
|
|
393
|
+
opened = false;
|
|
394
|
+
if (ready?.settled) ready = null;
|
|
395
|
+
}
|
|
396
|
+
snapshot = {
|
|
397
|
+
...snapshot,
|
|
398
|
+
attempt: 0
|
|
399
|
+
};
|
|
400
|
+
bindWindow();
|
|
401
|
+
open();
|
|
402
|
+
}
|
|
403
|
+
/** Start connecting unless already wanted. Used by holders (`acquire`, `ready`). */
|
|
404
|
+
function ensureConnecting() {
|
|
405
|
+
if (wanted || !getImpl()) return;
|
|
406
|
+
begin();
|
|
407
|
+
}
|
|
408
|
+
function connect() {
|
|
409
|
+
pinned = true;
|
|
410
|
+
ensureConnecting();
|
|
411
|
+
}
|
|
412
|
+
function stop(code, reason) {
|
|
413
|
+
wanted = false;
|
|
414
|
+
generation++;
|
|
415
|
+
clearTimeout(retryTimer);
|
|
416
|
+
retryTimer = void 0;
|
|
417
|
+
stopHeartbeat();
|
|
418
|
+
unbindWindow();
|
|
419
|
+
pending = [];
|
|
420
|
+
if (ws) {
|
|
421
|
+
const socket = ws;
|
|
422
|
+
detach(socket, true);
|
|
423
|
+
socket.close(code, reason);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function close(code = 1e3, reason) {
|
|
427
|
+
pinned = false;
|
|
428
|
+
opened = false;
|
|
429
|
+
stop(code, reason);
|
|
430
|
+
set({
|
|
431
|
+
status: "closed",
|
|
432
|
+
attempt: 0
|
|
433
|
+
});
|
|
434
|
+
rejectReady(/* @__PURE__ */ new Error("WebSocket was closed"));
|
|
435
|
+
}
|
|
436
|
+
function reconnect() {
|
|
437
|
+
if (!getImpl()) return;
|
|
438
|
+
clearTimeout(retryTimer);
|
|
439
|
+
retryTimer = void 0;
|
|
440
|
+
stopHeartbeat();
|
|
441
|
+
if (ws) {
|
|
442
|
+
const socket = ws;
|
|
443
|
+
detach(socket, true);
|
|
444
|
+
socket.close(1e3, "reconnect");
|
|
445
|
+
}
|
|
446
|
+
begin();
|
|
447
|
+
}
|
|
448
|
+
function teardown() {
|
|
449
|
+
if (refs > 0 || pinned) return;
|
|
450
|
+
if (ready && !ready.settled) return;
|
|
451
|
+
stop();
|
|
452
|
+
ready = null;
|
|
453
|
+
opened = false;
|
|
454
|
+
set({
|
|
455
|
+
status: "idle",
|
|
456
|
+
attempt: 0
|
|
457
|
+
});
|
|
458
|
+
onTeardown?.();
|
|
459
|
+
}
|
|
460
|
+
function acquire() {
|
|
461
|
+
refs++;
|
|
462
|
+
clearTimeout(teardownTimer);
|
|
463
|
+
if (snapshot.status === "idle") ensureConnecting();
|
|
464
|
+
let released = false;
|
|
465
|
+
return () => {
|
|
466
|
+
if (released) return;
|
|
467
|
+
released = true;
|
|
468
|
+
if (--refs > 0) return;
|
|
469
|
+
clearTimeout(teardownTimer);
|
|
470
|
+
teardownTimer = setTimeout(teardown, 0);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
url,
|
|
475
|
+
get ready() {
|
|
476
|
+
if (!getImpl()) {
|
|
477
|
+
if (!unavailable) {
|
|
478
|
+
unavailable = createDeferred();
|
|
479
|
+
unavailable.reject(/* @__PURE__ */ new Error("react-websocket: no WebSocket available here (server render?)"));
|
|
480
|
+
}
|
|
481
|
+
return unavailable.promise;
|
|
482
|
+
}
|
|
483
|
+
if (snapshot.status === "idle" && !startScheduled) {
|
|
484
|
+
startScheduled = true;
|
|
485
|
+
queueMicrotask(() => {
|
|
486
|
+
startScheduled = false;
|
|
487
|
+
if (snapshot.status === "idle") ensureConnecting();
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (!ready) {
|
|
491
|
+
ready = createDeferred();
|
|
492
|
+
if (opened) ready.resolve();
|
|
493
|
+
}
|
|
494
|
+
return ready.promise;
|
|
495
|
+
},
|
|
496
|
+
get socket() {
|
|
497
|
+
return ws;
|
|
498
|
+
},
|
|
499
|
+
subscribe(listener) {
|
|
500
|
+
storeListeners.add(listener);
|
|
501
|
+
return () => {
|
|
502
|
+
storeListeners.delete(listener);
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
getSnapshot: () => snapshot,
|
|
506
|
+
on(type, listener) {
|
|
507
|
+
const entry = ((...args) => listener(...args));
|
|
508
|
+
listeners[type].add(entry);
|
|
509
|
+
return () => {
|
|
510
|
+
listeners[type].delete(entry);
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
send,
|
|
514
|
+
connect,
|
|
515
|
+
close,
|
|
516
|
+
reconnect,
|
|
517
|
+
acquire
|
|
518
|
+
};
|
|
494
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Create a WebSocket client, usually at module level, to share across components.
|
|
522
|
+
*
|
|
523
|
+
* Nothing connects until a hook uses it, `ready` is read, or `connect()` is called.
|
|
524
|
+
* It closes one tick after the last hook unmounts, unless `connect()` was called.
|
|
525
|
+
*
|
|
526
|
+
* @param url - A URL, or a function (sync or async) called before every connection
|
|
527
|
+
* attempt, e.g. to put a fresh auth token in the URL on each reconnect.
|
|
528
|
+
* @typeParam In - Type of incoming messages. Inferred from `options.schema` if given.
|
|
529
|
+
* @typeParam Out - Type accepted by `send()`.
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* ```ts
|
|
533
|
+
* type ChatIn = { user: string; text: string };
|
|
534
|
+
* type ChatOut = { text: string };
|
|
535
|
+
*
|
|
536
|
+
* export const chat = createWebSocket<ChatIn, ChatOut>('wss://api.example.com/chat', { json: true });
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
495
539
|
function createWebSocket(url, options) {
|
|
496
|
-
|
|
540
|
+
return createClient(url, options);
|
|
497
541
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/latency.ts
|
|
544
|
+
const EMPTY = Object.freeze({
|
|
545
|
+
rtt: void 0,
|
|
546
|
+
offset: void 0
|
|
547
|
+
});
|
|
501
548
|
function median(values) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
549
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
550
|
+
const middle = sorted.length >> 1;
|
|
551
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
505
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Measure round-trip time and estimate the server's clock, for ping displays, countdowns
|
|
555
|
+
* and timestamps that must agree across players.
|
|
556
|
+
*
|
|
557
|
+
* Pings are sent on an interval while the socket is open, and paused otherwise. Measurements
|
|
558
|
+
* from a previous connection are discarded on reconnect, but the last values stay readable
|
|
559
|
+
* until new ones arrive. It doesn't open the connection by itself.
|
|
560
|
+
*
|
|
561
|
+
* Pongs are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* ```ts
|
|
565
|
+
* export const latency = createWebSocketLatency(game, {
|
|
566
|
+
* ping: id => ({ type: 'ping', id }),
|
|
567
|
+
* pong: message => (message.type === 'pong' ? { id: message.id, time: message.time } : undefined)
|
|
568
|
+
* });
|
|
569
|
+
*
|
|
570
|
+
* // In a component:
|
|
571
|
+
* const { rtt } = useWebSocketLatency(latency);
|
|
572
|
+
*
|
|
573
|
+
* // Anywhere: seconds left in a round that ends at the server's `endsAt`.
|
|
574
|
+
* const left = (endsAt - latency.serverNow()) / 1000;
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
506
577
|
function createWebSocketLatency(client, options) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
+
const interval = options.interval ?? 2e3;
|
|
579
|
+
const keep = Math.max(1, options.samples ?? 5);
|
|
580
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
581
|
+
let snapshot = EMPTY;
|
|
582
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
583
|
+
let samples = [];
|
|
584
|
+
let nextId = 0;
|
|
585
|
+
let timer;
|
|
586
|
+
function ping() {
|
|
587
|
+
if (client.getSnapshot().status !== "open") return;
|
|
588
|
+
const id = ++nextId;
|
|
589
|
+
inFlight.set(String(id), performance.now());
|
|
590
|
+
if (inFlight.size > keep * 2) inFlight.delete(inFlight.keys().next().value);
|
|
591
|
+
client.send(options.ping(id), { volatile: true });
|
|
592
|
+
}
|
|
593
|
+
function start() {
|
|
594
|
+
stop();
|
|
595
|
+
inFlight.clear();
|
|
596
|
+
samples = [];
|
|
597
|
+
ping();
|
|
598
|
+
timer = setInterval(ping, interval);
|
|
599
|
+
}
|
|
600
|
+
function stop() {
|
|
601
|
+
clearInterval(timer);
|
|
602
|
+
timer = void 0;
|
|
603
|
+
}
|
|
604
|
+
function measure(id, serverTime) {
|
|
605
|
+
const sentAt = inFlight.get(id);
|
|
606
|
+
if (sentAt === void 0) return;
|
|
607
|
+
inFlight.delete(id);
|
|
608
|
+
const rtt = performance.now() - sentAt;
|
|
609
|
+
const offset = serverTime === void 0 ? void 0 : serverTime + rtt / 2 - Date.now();
|
|
610
|
+
samples.push({
|
|
611
|
+
rtt,
|
|
612
|
+
offset
|
|
613
|
+
});
|
|
614
|
+
if (samples.length > keep) samples.shift();
|
|
615
|
+
let best;
|
|
616
|
+
for (const sample of samples) if (sample.offset !== void 0 && (!best || sample.rtt < best.rtt)) best = sample;
|
|
617
|
+
snapshot = {
|
|
618
|
+
rtt: median(samples.map((sample) => sample.rtt)),
|
|
619
|
+
offset: best ? best.offset : snapshot.offset
|
|
620
|
+
};
|
|
621
|
+
for (const listener of [...listeners]) listener();
|
|
622
|
+
}
|
|
623
|
+
const offs = [
|
|
624
|
+
client.on("open", start),
|
|
625
|
+
client.on("message", (message) => {
|
|
626
|
+
const pong = options.pong(message);
|
|
627
|
+
if (pong != null) measure(String(pong.id), pong.time);
|
|
628
|
+
}),
|
|
629
|
+
client.subscribe(() => {
|
|
630
|
+
if (client.getSnapshot().status !== "open") stop();
|
|
631
|
+
})
|
|
632
|
+
];
|
|
633
|
+
if (client.getSnapshot().status === "open") start();
|
|
634
|
+
return {
|
|
635
|
+
client,
|
|
636
|
+
getSnapshot: () => snapshot,
|
|
637
|
+
subscribe(listener) {
|
|
638
|
+
listeners.add(listener);
|
|
639
|
+
return () => {
|
|
640
|
+
listeners.delete(listener);
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
serverNow: () => Date.now() + (snapshot.offset ?? 0),
|
|
644
|
+
ping,
|
|
645
|
+
dispose() {
|
|
646
|
+
stop();
|
|
647
|
+
offs.forEach((off) => off());
|
|
648
|
+
}
|
|
649
|
+
};
|
|
578
650
|
}
|
|
579
|
-
|
|
580
|
-
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/requests.ts
|
|
653
|
+
const lastIds = /* @__PURE__ */ new WeakMap();
|
|
654
|
+
/**
|
|
655
|
+
* Send requests and await their replies, matched by an id the library generates.
|
|
656
|
+
*
|
|
657
|
+
* Replies are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* ```ts
|
|
661
|
+
* type Request = { type: 'join'; room: string } | { type: 'leave' };
|
|
662
|
+
*
|
|
663
|
+
* export const rpc = createWebSocketRequests(game, {
|
|
664
|
+
* send: (message: Request, id) => ({ ...message, id }),
|
|
665
|
+
* reply: message => (message.type === 'reply' ? message.id : undefined),
|
|
666
|
+
* error: message => (message.type === 'reply' && message.error ? new Error(message.error) : undefined)
|
|
667
|
+
* });
|
|
668
|
+
*
|
|
669
|
+
* const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
581
672
|
function createWebSocketRequests(client, options) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
if (sent) entry.socket = client.socket;
|
|
667
|
-
else if (requestOptions.volatile)
|
|
668
|
-
entry.reject(new Error("WebSocket is not open: request dropped"));
|
|
669
|
-
});
|
|
670
|
-
},
|
|
671
|
-
pending: () => waiting.size,
|
|
672
|
-
dispose() {
|
|
673
|
-
offs.forEach((off) => off());
|
|
674
|
-
rejectWhere(
|
|
675
|
-
() => true,
|
|
676
|
-
() => new Error("Requests were disposed")
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
};
|
|
673
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
674
|
+
function rejectWhere(test, reason) {
|
|
675
|
+
for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
|
|
676
|
+
}
|
|
677
|
+
const offs = [
|
|
678
|
+
client.on("message", (message) => {
|
|
679
|
+
const id = options.reply(message);
|
|
680
|
+
const entry = id == null ? void 0 : waiting.get(String(id));
|
|
681
|
+
if (!entry) return;
|
|
682
|
+
const error = options.error?.(message);
|
|
683
|
+
if (error === void 0) entry.resolve(message);
|
|
684
|
+
else entry.reject(error);
|
|
685
|
+
}),
|
|
686
|
+
client.on("open", () => {
|
|
687
|
+
for (const entry of waiting.values()) entry.socket ??= client.socket;
|
|
688
|
+
}),
|
|
689
|
+
client.subscribe(() => {
|
|
690
|
+
const { status } = client.getSnapshot();
|
|
691
|
+
if (status === "closed" || status === "idle") rejectWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
|
|
692
|
+
else rejectWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
|
|
693
|
+
})
|
|
694
|
+
];
|
|
695
|
+
return {
|
|
696
|
+
client,
|
|
697
|
+
request(message, requestOptions = {}) {
|
|
698
|
+
const { signal } = requestOptions;
|
|
699
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
700
|
+
const id = (lastIds.get(client) ?? 0) + 1;
|
|
701
|
+
lastIds.set(client, id);
|
|
702
|
+
const key = String(id);
|
|
703
|
+
const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
|
|
704
|
+
return new Promise((resolve, reject) => {
|
|
705
|
+
let timer;
|
|
706
|
+
const release = client.acquire();
|
|
707
|
+
const onAbort = () => entry.reject(signal.reason);
|
|
708
|
+
function settle() {
|
|
709
|
+
waiting.delete(key);
|
|
710
|
+
clearTimeout(timer);
|
|
711
|
+
signal?.removeEventListener("abort", onAbort);
|
|
712
|
+
release();
|
|
713
|
+
}
|
|
714
|
+
const entry = {
|
|
715
|
+
socket: null,
|
|
716
|
+
resolve(reply) {
|
|
717
|
+
settle();
|
|
718
|
+
resolve(reply);
|
|
719
|
+
},
|
|
720
|
+
reject(reason) {
|
|
721
|
+
settle();
|
|
722
|
+
reject(reason);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
waiting.set(key, entry);
|
|
726
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
727
|
+
if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.reject(new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")), timeout);
|
|
728
|
+
const { status } = client.getSnapshot();
|
|
729
|
+
if (status === "closed" || status === "idle") {
|
|
730
|
+
entry.reject(/* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
let sent;
|
|
734
|
+
try {
|
|
735
|
+
sent = client.send(options.send(message, id), {
|
|
736
|
+
...requestOptions,
|
|
737
|
+
onDrop() {
|
|
738
|
+
entry.reject(/* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
|
|
739
|
+
requestOptions.onDrop?.();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
} catch (error) {
|
|
743
|
+
entry.reject(error);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (sent) entry.socket = client.socket;
|
|
747
|
+
else if (requestOptions.volatile) entry.reject(/* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
|
|
748
|
+
});
|
|
749
|
+
},
|
|
750
|
+
pending: () => waiting.size,
|
|
751
|
+
dispose() {
|
|
752
|
+
offs.forEach((off) => off());
|
|
753
|
+
rejectWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"));
|
|
754
|
+
}
|
|
755
|
+
};
|
|
680
756
|
}
|
|
681
|
-
|
|
682
|
-
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/responder.ts
|
|
759
|
+
const VOLATILE = { volatile: true };
|
|
760
|
+
const abortError = (message) => new DOMException(message, "AbortError");
|
|
761
|
+
/**
|
|
762
|
+
* Answer requests the server sends: run a handler for tools that only exist in the browser
|
|
763
|
+
* (selection, clipboard, geolocation), or let a person answer, e.g. to approve an agent's action.
|
|
764
|
+
* The reverse of `createWebSocketRequests`.
|
|
765
|
+
*
|
|
766
|
+
* Every request gets exactly one reply while its connection lives: the handler's result, an
|
|
767
|
+
* `error` reply when it throws, has no registration or times out. When the connection a request
|
|
768
|
+
* arrived on closes, the request is dropped and nothing is sent, since the server can't match a
|
|
769
|
+
* reply on a new connection. Replies are never queued.
|
|
770
|
+
*
|
|
771
|
+
* Requests are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
|
|
772
|
+
*
|
|
773
|
+
* @example
|
|
774
|
+
* ```ts
|
|
775
|
+
* export const tools = createWebSocketResponder(agent, {
|
|
776
|
+
* request: message => (message.type === 'call' ? message : undefined),
|
|
777
|
+
* respond: (id, result: unknown) => ({ type: 'result', id, result }),
|
|
778
|
+
* error: (id, error) => ({ type: 'result', id, error: String(error) })
|
|
779
|
+
* });
|
|
780
|
+
*
|
|
781
|
+
* tools.onRequest('get_selection', () => window.getSelection()?.toString() ?? '');
|
|
782
|
+
*
|
|
783
|
+
* // In a component: requests a person answers.
|
|
784
|
+
* const approvals = useAgentApproval(tools, 'approve');
|
|
785
|
+
* approvals[0]?.respond({ approved: true });
|
|
786
|
+
* ```
|
|
787
|
+
*/
|
|
788
|
+
function createWebSocketResponder(client, options) {
|
|
789
|
+
const entries = /* @__PURE__ */ new Map();
|
|
790
|
+
const registrations = /* @__PURE__ */ new Map();
|
|
791
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
792
|
+
let waiting = [];
|
|
793
|
+
function setWaiting(next) {
|
|
794
|
+
waiting = next;
|
|
795
|
+
for (const listener of [...listeners]) listener();
|
|
796
|
+
}
|
|
797
|
+
/** Send a reply, or an `error` reply if building or encoding it throws. */
|
|
798
|
+
function reply(id, build) {
|
|
799
|
+
try {
|
|
800
|
+
return client.send(build(), VOLATILE);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
return client.send(options.error(id, error), VOLATILE);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function receive(incoming) {
|
|
806
|
+
const { id, method } = incoming;
|
|
807
|
+
const key = String(id);
|
|
808
|
+
if (entries.has(key)) return;
|
|
809
|
+
const registration = registrations.get(method)?.at(-1);
|
|
810
|
+
if (!registration) {
|
|
811
|
+
const error = new DOMException(`No handler for request method "${method}"`, "NotFoundError");
|
|
812
|
+
reply(id, () => options.error(id, error));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const controller = new AbortController();
|
|
816
|
+
let timer;
|
|
817
|
+
let done = false;
|
|
818
|
+
let running = false;
|
|
819
|
+
function settle(build, reason) {
|
|
820
|
+
if (done) return false;
|
|
821
|
+
done = true;
|
|
822
|
+
entries.delete(key);
|
|
823
|
+
clearTimeout(timer);
|
|
824
|
+
if (waiting.includes(request)) setWaiting(waiting.filter((entry) => entry !== request));
|
|
825
|
+
if (reason !== void 0) controller.abort(reason);
|
|
826
|
+
return build ? reply(id, build) : false;
|
|
827
|
+
}
|
|
828
|
+
const answeredElsewhere = () => running ? abortError(`Request ${id} was answered elsewhere`) : void 0;
|
|
829
|
+
const request = {
|
|
830
|
+
id,
|
|
831
|
+
method,
|
|
832
|
+
params: incoming.params,
|
|
833
|
+
signal: controller.signal,
|
|
834
|
+
respond: (result) => settle(() => options.respond(id, result), answeredElsewhere()),
|
|
835
|
+
reject: (error) => settle(() => options.error(id, error), answeredElsewhere())
|
|
836
|
+
};
|
|
837
|
+
const fail = (error) => {
|
|
838
|
+
settle(() => options.error(id, error), error);
|
|
839
|
+
};
|
|
840
|
+
entries.set(key, {
|
|
841
|
+
socket: client.socket,
|
|
842
|
+
drop: (reason) => {
|
|
843
|
+
settle(null, reason);
|
|
844
|
+
},
|
|
845
|
+
fail
|
|
846
|
+
});
|
|
847
|
+
const timeout = incoming.timeout ?? options.timeout ?? Infinity;
|
|
848
|
+
if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => fail(new DOMException(`Request ${id} was not answered within ${timeout}ms`, "TimeoutError")), timeout);
|
|
849
|
+
const { handler } = registration;
|
|
850
|
+
if (!handler) {
|
|
851
|
+
setWaiting([...waiting, request]);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
running = true;
|
|
855
|
+
new Promise((resolve) => resolve(handler(request.params, request))).then((result) => {
|
|
856
|
+
running = false;
|
|
857
|
+
request.respond(result);
|
|
858
|
+
}, (error) => {
|
|
859
|
+
running = false;
|
|
860
|
+
request.reject(error);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
const offs = [client.on("message", (message) => {
|
|
864
|
+
const incoming = options.request(message);
|
|
865
|
+
if (incoming != null) {
|
|
866
|
+
receive(incoming);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const id = options.cancel?.(message);
|
|
870
|
+
if (id != null) entries.get(String(id))?.drop(abortError(`Request ${id} was cancelled`));
|
|
871
|
+
}), client.subscribe(() => {
|
|
872
|
+
for (const entry of [...entries.values()]) if (entry.socket !== client.socket) entry.drop(abortError("WebSocket connection was lost before the request was answered"));
|
|
873
|
+
})];
|
|
874
|
+
return {
|
|
875
|
+
client,
|
|
876
|
+
onRequest(method, handler) {
|
|
877
|
+
let stack = registrations.get(method);
|
|
878
|
+
if (!stack) registrations.set(method, stack = []);
|
|
879
|
+
const registration = { handler };
|
|
880
|
+
stack.push(registration);
|
|
881
|
+
return () => {
|
|
882
|
+
const index = stack.indexOf(registration);
|
|
883
|
+
if (index !== -1) stack.splice(index, 1);
|
|
884
|
+
};
|
|
885
|
+
},
|
|
886
|
+
getSnapshot: () => waiting,
|
|
887
|
+
subscribe(listener) {
|
|
888
|
+
listeners.add(listener);
|
|
889
|
+
return () => {
|
|
890
|
+
listeners.delete(listener);
|
|
891
|
+
};
|
|
892
|
+
},
|
|
893
|
+
dispose() {
|
|
894
|
+
offs.forEach((off) => off());
|
|
895
|
+
registrations.clear();
|
|
896
|
+
for (const entry of [...entries.values()]) entry.fail(abortError("Responder was disposed"));
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/state.ts
|
|
902
|
+
/**
|
|
903
|
+
* Keep state derived from a client's messages, like a reducer that runs on every message.
|
|
904
|
+
* It starts listening immediately and keeps the state across reconnects; it doesn't open
|
|
905
|
+
* the connection by itself.
|
|
906
|
+
*
|
|
907
|
+
* @example
|
|
908
|
+
* ```ts
|
|
909
|
+
* const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });
|
|
910
|
+
*
|
|
911
|
+
* export const priceState = createWebSocketState(prices, {
|
|
912
|
+
* initialState: {} as Record<string, number>,
|
|
913
|
+
* reduce: (state, tick) => ({ ...state, [tick.symbol]: tick.price })
|
|
914
|
+
* });
|
|
915
|
+
*
|
|
916
|
+
* // In a component: re-renders only when ACME's price changes.
|
|
917
|
+
* const acme = useWebSocketState(priceState, state => state.ACME);
|
|
918
|
+
* ```
|
|
919
|
+
*/
|
|
683
920
|
function createWebSocketState(client, options) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
};
|
|
921
|
+
const { initialState, reduce } = options;
|
|
922
|
+
let state = initialState;
|
|
923
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
924
|
+
function setState(update) {
|
|
925
|
+
const next = typeof update === "function" ? update(state) : update;
|
|
926
|
+
if (Object.is(next, state)) return;
|
|
927
|
+
state = next;
|
|
928
|
+
for (const listener of [...listeners]) listener();
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
client,
|
|
932
|
+
initialState,
|
|
933
|
+
getState: () => state,
|
|
934
|
+
subscribe(listener) {
|
|
935
|
+
listeners.add(listener);
|
|
936
|
+
return () => {
|
|
937
|
+
listeners.delete(listener);
|
|
938
|
+
};
|
|
939
|
+
},
|
|
940
|
+
setState,
|
|
941
|
+
reset: () => setState(initialState),
|
|
942
|
+
dispose: client.on("message", (message) => setState((current) => reduce(current, message)))
|
|
943
|
+
};
|
|
708
944
|
}
|
|
709
|
-
|
|
710
|
-
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/topics.ts
|
|
947
|
+
/**
|
|
948
|
+
* Manage topic subscriptions that survive reconnects.
|
|
949
|
+
*
|
|
950
|
+
* - The first holder of a topic sends `subscribe(topic)`. Later holders share it.
|
|
951
|
+
* - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode
|
|
952
|
+
* and route transitions don't churn subscriptions.
|
|
953
|
+
* - After every reconnect, every held topic is subscribed again on the new socket, before
|
|
954
|
+
* messages queued while disconnected are sent.
|
|
955
|
+
*
|
|
956
|
+
* Subscriptions are only sent while the socket is open; nothing is queued for them.
|
|
957
|
+
*
|
|
958
|
+
* @example
|
|
959
|
+
* ```ts
|
|
960
|
+
* export const topics = createWebSocketTopics(live, {
|
|
961
|
+
* subscribe: topic => ({ type: 'subscribe', topic }),
|
|
962
|
+
* unsubscribe: topic => ({ type: 'unsubscribe', topic })
|
|
963
|
+
* });
|
|
964
|
+
*
|
|
965
|
+
* // In a component:
|
|
966
|
+
* useWebSocketTopic(topics, `prices:${symbol}`);
|
|
967
|
+
* ```
|
|
968
|
+
*/
|
|
711
969
|
function createWebSocketTopics(client, options) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
for (const timer of pendingRemoval.values()) clearTimeout(timer);
|
|
770
|
-
pendingRemoval.clear();
|
|
771
|
-
}
|
|
772
|
-
};
|
|
970
|
+
const counts = /* @__PURE__ */ new Map();
|
|
971
|
+
const pendingRemoval = /* @__PURE__ */ new Map();
|
|
972
|
+
let sentOn = null;
|
|
973
|
+
let sent = /* @__PURE__ */ new Set();
|
|
974
|
+
function isOpen() {
|
|
975
|
+
return client.getSnapshot().status === "open" && client.socket !== null;
|
|
976
|
+
}
|
|
977
|
+
function subscribe(topic) {
|
|
978
|
+
if (!isOpen()) return;
|
|
979
|
+
if (client.socket !== sentOn) {
|
|
980
|
+
sentOn = client.socket;
|
|
981
|
+
sent = /* @__PURE__ */ new Set();
|
|
982
|
+
}
|
|
983
|
+
if (sent.has(topic)) return;
|
|
984
|
+
sent.add(topic);
|
|
985
|
+
client.send(options.subscribe(topic));
|
|
986
|
+
}
|
|
987
|
+
function unsubscribe(topic) {
|
|
988
|
+
if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;
|
|
989
|
+
if (options.unsubscribe) client.send(options.unsubscribe(topic));
|
|
990
|
+
}
|
|
991
|
+
const off = client.on("open", () => {
|
|
992
|
+
sentOn = client.socket;
|
|
993
|
+
sent = /* @__PURE__ */ new Set();
|
|
994
|
+
for (const topic of counts.keys()) subscribe(topic);
|
|
995
|
+
});
|
|
996
|
+
return {
|
|
997
|
+
client,
|
|
998
|
+
acquire(topic) {
|
|
999
|
+
const releaseConnection = client.acquire();
|
|
1000
|
+
clearTimeout(pendingRemoval.get(topic));
|
|
1001
|
+
pendingRemoval.delete(topic);
|
|
1002
|
+
counts.set(topic, (counts.get(topic) ?? 0) + 1);
|
|
1003
|
+
subscribe(topic);
|
|
1004
|
+
let released = false;
|
|
1005
|
+
return () => {
|
|
1006
|
+
if (released) return;
|
|
1007
|
+
released = true;
|
|
1008
|
+
const remaining = (counts.get(topic) ?? 1) - 1;
|
|
1009
|
+
if (remaining > 0) counts.set(topic, remaining);
|
|
1010
|
+
else {
|
|
1011
|
+
counts.delete(topic);
|
|
1012
|
+
pendingRemoval.set(topic, setTimeout(() => {
|
|
1013
|
+
pendingRemoval.delete(topic);
|
|
1014
|
+
if (!counts.has(topic)) unsubscribe(topic);
|
|
1015
|
+
}, 0));
|
|
1016
|
+
}
|
|
1017
|
+
releaseConnection();
|
|
1018
|
+
};
|
|
1019
|
+
},
|
|
1020
|
+
active: () => [...counts.keys()],
|
|
1021
|
+
dispose() {
|
|
1022
|
+
off();
|
|
1023
|
+
for (const timer of pendingRemoval.values()) clearTimeout(timer);
|
|
1024
|
+
pendingRemoval.clear();
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
773
1027
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1028
|
+
//#endregion
|
|
1029
|
+
exports.MessageError = MessageError;
|
|
1030
|
+
exports.createWebSocket = createWebSocket;
|
|
1031
|
+
exports.createWebSocketLatency = createWebSocketLatency;
|
|
1032
|
+
exports.createWebSocketRequests = createWebSocketRequests;
|
|
1033
|
+
exports.createWebSocketResponder = createWebSocketResponder;
|
|
1034
|
+
exports.createWebSocketState = createWebSocketState;
|
|
1035
|
+
exports.createWebSocketTopics = createWebSocketTopics;
|
|
1036
|
+
|
|
783
1037
|
//# sourceMappingURL=core.cjs.map
|