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/core.cjs
CHANGED
|
@@ -35,6 +35,7 @@ const RECONNECT_DEFAULTS = {
|
|
|
35
35
|
minUptime: 5e3
|
|
36
36
|
};
|
|
37
37
|
const QUEUE_LIMIT = 100;
|
|
38
|
+
const MAX_TIMEOUT = 2 ** 31 - 1;
|
|
38
39
|
/**
|
|
39
40
|
* The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
|
|
40
41
|
* `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
|
|
@@ -51,6 +52,22 @@ const QUEUE_LIMIT = 100;
|
|
|
51
52
|
function retryableClose(close) {
|
|
52
53
|
return close.code !== 1002 && close.code !== 1003 && close.code !== 1008;
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The default `reconnect.retryAfter`: for a `1012` (Service Restart) or `1013` (Try Again Later)
|
|
57
|
+
* close whose reason is a number of seconds, like an HTTP `Retry-After` header, that delay in ms.
|
|
58
|
+
* Call it from your own `retryAfter` to keep reading it.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* createWebSocket(url, {
|
|
63
|
+
* // Our server also closes with 4429 and the delay in ms as the reason.
|
|
64
|
+
* reconnect: { retryAfter: close => (close.code === 4429 ? Number(close.reason) : retryAfter(close)) }
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
function retryAfter(close) {
|
|
69
|
+
return (close.code === 1012 || close.code === 1013) && /^\s*\d+(\.\d+)?\s*$/.test(close.reason) ? Number(close.reason) * 1e3 : void 0;
|
|
70
|
+
}
|
|
54
71
|
const READY_GRACE = 5e3;
|
|
55
72
|
function createDeferred() {
|
|
56
73
|
let resolve;
|
|
@@ -95,6 +112,10 @@ function isThenable(value) {
|
|
|
95
112
|
* because the hooks entry imports the client from the `core` entry at runtime.
|
|
96
113
|
*/
|
|
97
114
|
const ON_TEARDOWN = Symbol.for("react-websocket.onTeardown");
|
|
115
|
+
/** @internal Method key on a socket that sends `volatile` messages as datagrams (WebTransport). */
|
|
116
|
+
const DATAGRAM = Symbol.for("react-websocket.datagram");
|
|
117
|
+
/** @internal Method key for what `react-websocket/devtools` shows that the snapshot doesn't. */
|
|
118
|
+
const INSPECT = Symbol.for("react-websocket.inspect");
|
|
98
119
|
function createClient(url, options = {}) {
|
|
99
120
|
const onTeardown = options[ON_TEARDOWN];
|
|
100
121
|
const log = options.logger ?? (() => {});
|
|
@@ -102,7 +123,9 @@ function createClient(url, options = {}) {
|
|
|
102
123
|
...RECONNECT_DEFAULTS,
|
|
103
124
|
...typeof options.reconnect === "object" ? options.reconnect : {}
|
|
104
125
|
};
|
|
105
|
-
const
|
|
126
|
+
const queue = typeof options.queue === "object" ? options.queue : {};
|
|
127
|
+
const queueLimit = options.queue === false ? 0 : Math.max(0, queue.limit ?? QUEUE_LIMIT);
|
|
128
|
+
const queueMaxAge = queue.maxAge ?? Infinity;
|
|
106
129
|
let snapshot = {
|
|
107
130
|
status: "idle",
|
|
108
131
|
lastMessage: void 0,
|
|
@@ -116,9 +139,13 @@ function createClient(url, options = {}) {
|
|
|
116
139
|
error: /* @__PURE__ */ new Set()
|
|
117
140
|
};
|
|
118
141
|
let ws = null;
|
|
142
|
+
let incoming = null;
|
|
143
|
+
let replaced = null;
|
|
144
|
+
let moved;
|
|
119
145
|
let wanted = false;
|
|
120
146
|
let refs = 0;
|
|
121
147
|
let pinned = false;
|
|
148
|
+
let unheldStop = false;
|
|
122
149
|
let pending = [];
|
|
123
150
|
let ready = null;
|
|
124
151
|
let opened = false;
|
|
@@ -128,7 +155,9 @@ function createClient(url, options = {}) {
|
|
|
128
155
|
let generation = 0;
|
|
129
156
|
let windowBound = false;
|
|
130
157
|
let failures = 0;
|
|
158
|
+
let serverWait = false;
|
|
131
159
|
let retryTimer;
|
|
160
|
+
let migrateTimer;
|
|
132
161
|
let teardownTimer;
|
|
133
162
|
let stableTimer;
|
|
134
163
|
let heartbeatTimer;
|
|
@@ -153,49 +182,55 @@ function createClient(url, options = {}) {
|
|
|
153
182
|
}
|
|
154
183
|
function getImpl() {
|
|
155
184
|
if (options.WebSocket) return options.WebSocket;
|
|
156
|
-
return typeof window === "undefined" ? void 0 : globalThis.WebSocket;
|
|
185
|
+
return typeof window === "undefined" && typeof WorkerGlobalScope === "undefined" ? void 0 : globalThis.WebSocket;
|
|
157
186
|
}
|
|
158
187
|
function rejectReady(reason) {
|
|
159
188
|
if (!ready || ready.settled) ready = createDeferred();
|
|
160
189
|
ready.reject(reason);
|
|
161
190
|
}
|
|
162
|
-
|
|
191
|
+
/** The network or the tab came back: retry now, or check that an open connection still works. */
|
|
192
|
+
function wake() {
|
|
163
193
|
if (retryTimer !== void 0) {
|
|
194
|
+
if (serverWait) return;
|
|
164
195
|
log("network or visibility change, retrying now");
|
|
165
196
|
open();
|
|
197
|
+
} else if (heartbeatTimer !== void 0 && pongTimer === void 0) {
|
|
198
|
+
log("network or visibility change, checking the connection");
|
|
199
|
+
ping(ws);
|
|
166
200
|
}
|
|
167
201
|
}
|
|
168
202
|
function onVisibilityChange() {
|
|
169
|
-
if (document.visibilityState === "visible")
|
|
203
|
+
if (document.visibilityState === "visible") wake();
|
|
170
204
|
}
|
|
171
205
|
function bindWindow() {
|
|
172
|
-
if (windowBound || typeof
|
|
206
|
+
if (windowBound || typeof addEventListener !== "function") return;
|
|
173
207
|
windowBound = true;
|
|
174
|
-
|
|
175
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
208
|
+
addEventListener("online", wake);
|
|
209
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibilityChange);
|
|
176
210
|
}
|
|
177
211
|
function unbindWindow() {
|
|
178
212
|
if (!windowBound) return;
|
|
179
213
|
windowBound = false;
|
|
180
|
-
|
|
181
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
214
|
+
removeEventListener("online", wake);
|
|
215
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
182
216
|
}
|
|
183
|
-
|
|
217
|
+
/** Send a heartbeat ping and, with a `timeout`, close the socket if nothing comes back. */
|
|
218
|
+
function ping(socket) {
|
|
184
219
|
const heartbeat = options.heartbeat;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
socket.send(heartbeat.message ?? "ping");
|
|
221
|
+
if (heartbeat.timeout && pongTimer === void 0) pongTimer = setTimeout(() => {
|
|
222
|
+
log("heartbeat timeout");
|
|
223
|
+
detach(socket, false);
|
|
224
|
+
socket.close(4e3, "heartbeat timeout");
|
|
225
|
+
handleClose({
|
|
226
|
+
code: 4e3,
|
|
227
|
+
reason: "heartbeat timeout",
|
|
228
|
+
wasClean: false
|
|
229
|
+
});
|
|
230
|
+
}, heartbeat.timeout);
|
|
231
|
+
}
|
|
232
|
+
function startHeartbeat(socket) {
|
|
233
|
+
if (options.heartbeat) heartbeatTimer = setInterval(ping, options.heartbeat.interval, socket);
|
|
199
234
|
}
|
|
200
235
|
/** Stop the timers that belong to the current open socket. */
|
|
201
236
|
function stopSocketTimers() {
|
|
@@ -214,59 +249,93 @@ function createClient(url, options = {}) {
|
|
|
214
249
|
wasClean: event.wasClean
|
|
215
250
|
}) : null;
|
|
216
251
|
}
|
|
217
|
-
|
|
252
|
+
/** The delay before attempt `n`; `slow` starts at `maxDelay`, for a server shedding load. */
|
|
253
|
+
function backoff(attempt, slow) {
|
|
218
254
|
const { minDelay, maxDelay, factor, jitter } = retry;
|
|
219
|
-
const base = Math.min(maxDelay, minDelay * factor ** (attempt - 1));
|
|
255
|
+
const base = slow ? maxDelay : Math.min(maxDelay, minDelay * factor ** (attempt - 1));
|
|
220
256
|
return Math.round(base * (1 - jitter * Math.random()));
|
|
221
257
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
258
|
+
/** Stop a `migrate()` that hasn't switched yet. */
|
|
259
|
+
function dropIncoming() {
|
|
260
|
+
if (incoming) {
|
|
261
|
+
detach(incoming, false);
|
|
262
|
+
incoming.close();
|
|
263
|
+
incoming = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Start a connection attempt. A `handoff` (from `migrate()`) opens beside the current
|
|
268
|
+
* connection, which keeps working until the new one opens.
|
|
269
|
+
*/
|
|
270
|
+
function open(handoff) {
|
|
271
|
+
if (!handoff) {
|
|
272
|
+
clearTimeout(retryTimer);
|
|
273
|
+
retryTimer = void 0;
|
|
274
|
+
serverWait = false;
|
|
275
|
+
dropIncoming();
|
|
276
|
+
}
|
|
225
277
|
const Impl = getImpl();
|
|
226
278
|
if (!Impl) return;
|
|
227
279
|
const id = ++generation;
|
|
228
|
-
set({ status: snapshot.attempt === 0 ? "connecting" : "reconnecting" });
|
|
280
|
+
if (!handoff) set({ status: snapshot.attempt === 0 ? "connecting" : "reconnecting" });
|
|
281
|
+
const source = moved ?? url;
|
|
229
282
|
let target;
|
|
230
283
|
try {
|
|
231
|
-
target = typeof
|
|
284
|
+
target = typeof source === "string" ? source : source();
|
|
232
285
|
} catch (error) {
|
|
233
|
-
urlFailed(error);
|
|
286
|
+
urlFailed(error, handoff);
|
|
234
287
|
return;
|
|
235
288
|
}
|
|
236
289
|
if (typeof target === "string") {
|
|
237
|
-
start(Impl, target);
|
|
290
|
+
start(Impl, target, handoff);
|
|
238
291
|
return;
|
|
239
292
|
}
|
|
240
293
|
Promise.resolve(target).then((resolved) => {
|
|
241
|
-
if (id === generation && wanted) start(Impl, resolved);
|
|
294
|
+
if (id === generation && wanted) start(Impl, resolved, handoff);
|
|
242
295
|
}, (error) => {
|
|
243
|
-
if (id === generation && wanted) urlFailed(error);
|
|
296
|
+
if (id === generation && wanted) urlFailed(error, handoff);
|
|
244
297
|
});
|
|
245
298
|
}
|
|
246
|
-
/**
|
|
247
|
-
|
|
299
|
+
/**
|
|
300
|
+
* `url()` threw or rejected (e.g. a token refresh failed): count it as a failed attempt,
|
|
301
|
+
* unless it was for a handoff, which leaves the current connection in place.
|
|
302
|
+
*/
|
|
303
|
+
function urlFailed(error, handoff) {
|
|
248
304
|
const reason = error instanceof Error ? error : new Error(String(error));
|
|
249
305
|
log("url() failed", reason);
|
|
250
306
|
emit("error", reason);
|
|
251
|
-
retryOrFail({
|
|
307
|
+
if (!handoff) retryOrFail({
|
|
252
308
|
code: 1006,
|
|
253
309
|
reason: "url() failed",
|
|
254
310
|
wasClean: false
|
|
255
311
|
}, reason);
|
|
256
312
|
}
|
|
257
|
-
function start(Impl, target) {
|
|
313
|
+
function start(Impl, target, handoff) {
|
|
258
314
|
let socket;
|
|
259
315
|
try {
|
|
260
316
|
socket = new Impl(target, options.protocols);
|
|
261
317
|
} catch (error) {
|
|
262
318
|
emit("error", error);
|
|
263
|
-
fail(error);
|
|
319
|
+
if (!handoff) fail(error);
|
|
264
320
|
return;
|
|
265
321
|
}
|
|
266
322
|
if (options.binaryType) socket.binaryType = options.binaryType;
|
|
267
|
-
|
|
323
|
+
if (handoff) incoming = socket;
|
|
324
|
+
else ws = socket;
|
|
268
325
|
log("connecting", target);
|
|
269
326
|
socket.onopen = (event) => {
|
|
327
|
+
if (handoff) {
|
|
328
|
+
incoming = null;
|
|
329
|
+
clearTimeout(retryTimer);
|
|
330
|
+
retryTimer = void 0;
|
|
331
|
+
stopSocketTimers();
|
|
332
|
+
if (ws) {
|
|
333
|
+
const old = replaced = ws;
|
|
334
|
+
detach(old, true);
|
|
335
|
+
old.close(1e3, "migrated");
|
|
336
|
+
}
|
|
337
|
+
ws = socket;
|
|
338
|
+
}
|
|
270
339
|
log("open");
|
|
271
340
|
opened = true;
|
|
272
341
|
set({
|
|
@@ -284,11 +353,20 @@ function createClient(url, options = {}) {
|
|
|
284
353
|
if (ws !== socket) return;
|
|
285
354
|
const queued = pending;
|
|
286
355
|
pending = [];
|
|
287
|
-
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
for (const entry of queued) if (entry.expires >= now) socket.send(entry.data);
|
|
358
|
+
for (const entry of queued) if (entry.expires < now) expired(entry);
|
|
288
359
|
};
|
|
289
360
|
socket.onmessage = (event) => receive(event, socket);
|
|
290
|
-
socket.onerror = (event) =>
|
|
361
|
+
socket.onerror = (event) => {
|
|
362
|
+
if (ws === socket) emit("error", event);
|
|
363
|
+
};
|
|
291
364
|
socket.onclose = (event) => {
|
|
365
|
+
if (ws !== socket) {
|
|
366
|
+
incoming = null;
|
|
367
|
+
log("migrate() failed", event.code);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
292
370
|
detach(socket, false);
|
|
293
371
|
handleClose({
|
|
294
372
|
code: event.code,
|
|
@@ -298,6 +376,7 @@ function createClient(url, options = {}) {
|
|
|
298
376
|
};
|
|
299
377
|
}
|
|
300
378
|
function handleClose(info) {
|
|
379
|
+
replaced = null;
|
|
301
380
|
stopSocketTimers();
|
|
302
381
|
log("closed", info);
|
|
303
382
|
retryOrFail(info);
|
|
@@ -306,7 +385,10 @@ function createClient(url, options = {}) {
|
|
|
306
385
|
function retryOrFail(info, error) {
|
|
307
386
|
if (wanted && retry && failures < retry.maxAttempts && (retry.shouldReconnect ?? retryableClose)(info)) {
|
|
308
387
|
const attempt = ++failures;
|
|
309
|
-
const
|
|
388
|
+
const asked = (retry.retryAfter ?? retryAfter)(info);
|
|
389
|
+
const named = asked >= 0;
|
|
390
|
+
const delay = Math.min(MAX_TIMEOUT, named ? asked + backoff(attempt, false) : backoff(attempt, info.code === 1013));
|
|
391
|
+
serverWait = named || info.code === 1013;
|
|
310
392
|
set({
|
|
311
393
|
status: "reconnecting",
|
|
312
394
|
attempt
|
|
@@ -315,10 +397,29 @@ function createClient(url, options = {}) {
|
|
|
315
397
|
retryTimer = setTimeout(open, delay);
|
|
316
398
|
} else fail(error ?? /* @__PURE__ */ new Error(`WebSocket closed (code ${info.code}${info.reason ? `: ${info.reason}` : ""})`));
|
|
317
399
|
}
|
|
400
|
+
/** Forget `migrate()`: its timer, its connection and the URL it moved to. */
|
|
401
|
+
function stopMigrating() {
|
|
402
|
+
clearTimeout(migrateTimer);
|
|
403
|
+
migrateTimer = void 0;
|
|
404
|
+
dropIncoming();
|
|
405
|
+
moved = void 0;
|
|
406
|
+
}
|
|
407
|
+
function migrate({ url: to, within } = {}) {
|
|
408
|
+
clearTimeout(migrateTimer);
|
|
409
|
+
dropIncoming();
|
|
410
|
+
migrateTimer = setTimeout(() => {
|
|
411
|
+
migrateTimer = void 0;
|
|
412
|
+
if (!wanted) return;
|
|
413
|
+
if (to !== void 0) moved = to;
|
|
414
|
+
if (snapshot.status === "open") open(true);
|
|
415
|
+
}, Math.min(MAX_TIMEOUT, Math.random() * (within ?? 0)));
|
|
416
|
+
}
|
|
318
417
|
function fail(reason) {
|
|
319
418
|
wanted = false;
|
|
320
419
|
opened = false;
|
|
420
|
+
unheldStop = refs === 0;
|
|
321
421
|
unbindWindow();
|
|
422
|
+
stopMigrating();
|
|
322
423
|
pending = [];
|
|
323
424
|
set({
|
|
324
425
|
status: "closed",
|
|
@@ -371,7 +472,7 @@ function createClient(url, options = {}) {
|
|
|
371
472
|
else deliver(result.value, event, socket);
|
|
372
473
|
}
|
|
373
474
|
function deliver(message, event, socket) {
|
|
374
|
-
if (ws !== socket) return;
|
|
475
|
+
if (ws !== socket && replaced !== socket) return;
|
|
375
476
|
set({ lastMessage: message });
|
|
376
477
|
emit("message", message, event);
|
|
377
478
|
}
|
|
@@ -383,7 +484,9 @@ function createClient(url, options = {}) {
|
|
|
383
484
|
log("dropped message: send buffer above maxBuffered");
|
|
384
485
|
return false;
|
|
385
486
|
}
|
|
386
|
-
ws
|
|
487
|
+
const datagram = sendOptions.volatile ? ws[DATAGRAM] : void 0;
|
|
488
|
+
if (datagram) datagram.call(ws, data);
|
|
489
|
+
else ws.send(data);
|
|
387
490
|
return true;
|
|
388
491
|
}
|
|
389
492
|
if (queueLimit === 0 || sendOptions.volatile) {
|
|
@@ -394,18 +497,18 @@ function createClient(url, options = {}) {
|
|
|
394
497
|
log("dropped message: no WebSocket implementation available");
|
|
395
498
|
return false;
|
|
396
499
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
});
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const before = pending;
|
|
502
|
+
pending = before.filter((entry) => entry.expires >= now && (key === void 0 || entry.key !== key));
|
|
503
|
+
for (const entry of before) if (!pending.includes(entry)) (entry.expires < now ? expired : dropped)(entry);
|
|
402
504
|
let head = pending.findIndex((entry) => !entry.priority);
|
|
403
505
|
if (head < 0) head = pending.length;
|
|
404
506
|
const entry = {
|
|
405
507
|
data,
|
|
406
508
|
key,
|
|
407
509
|
priority,
|
|
408
|
-
onDrop: sendOptions.onDrop
|
|
510
|
+
onDrop: sendOptions.onDrop,
|
|
511
|
+
expires: now + (sendOptions.maxAge ?? queueMaxAge)
|
|
409
512
|
};
|
|
410
513
|
if (priority) pending.splice(head++, 0, entry);
|
|
411
514
|
else pending.push(entry);
|
|
@@ -415,6 +518,10 @@ function createClient(url, options = {}) {
|
|
|
415
518
|
}
|
|
416
519
|
return false;
|
|
417
520
|
}
|
|
521
|
+
function expired(entry) {
|
|
522
|
+
log("dropped queued message: older than maxAge");
|
|
523
|
+
dropped(entry);
|
|
524
|
+
}
|
|
418
525
|
function dropped(entry) {
|
|
419
526
|
try {
|
|
420
527
|
entry.onDrop?.();
|
|
@@ -424,6 +531,7 @@ function createClient(url, options = {}) {
|
|
|
424
531
|
}
|
|
425
532
|
function begin() {
|
|
426
533
|
wanted = true;
|
|
534
|
+
unheldStop = false;
|
|
427
535
|
if (snapshot.status === "idle" || snapshot.status === "closed") {
|
|
428
536
|
opened = false;
|
|
429
537
|
if (ready?.settled) ready = null;
|
|
@@ -446,6 +554,8 @@ function createClient(url, options = {}) {
|
|
|
446
554
|
ensureConnecting();
|
|
447
555
|
}
|
|
448
556
|
function stop(code, reason) {
|
|
557
|
+
const socket = ws;
|
|
558
|
+
socket?.close(code, reason);
|
|
449
559
|
wanted = false;
|
|
450
560
|
generation++;
|
|
451
561
|
clearTimeout(retryTimer);
|
|
@@ -453,17 +563,16 @@ function createClient(url, options = {}) {
|
|
|
453
563
|
failures = 0;
|
|
454
564
|
stopSocketTimers();
|
|
455
565
|
unbindWindow();
|
|
566
|
+
stopMigrating();
|
|
456
567
|
pending = [];
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
detach(socket, true);
|
|
460
|
-
socket.close(code, reason);
|
|
461
|
-
}
|
|
568
|
+
replaced = null;
|
|
569
|
+
if (socket) detach(socket, true);
|
|
462
570
|
}
|
|
463
571
|
function close(code = 1e3, reason) {
|
|
572
|
+
stop(code, reason);
|
|
464
573
|
pinned = false;
|
|
465
574
|
opened = false;
|
|
466
|
-
|
|
575
|
+
unheldStop = refs === 0;
|
|
467
576
|
set({
|
|
468
577
|
status: "closed",
|
|
469
578
|
attempt: 0
|
|
@@ -497,7 +606,7 @@ function createClient(url, options = {}) {
|
|
|
497
606
|
function acquire() {
|
|
498
607
|
refs++;
|
|
499
608
|
clearTimeout(teardownTimer);
|
|
500
|
-
if (snapshot.status === "idle") ensureConnecting();
|
|
609
|
+
if (snapshot.status === "idle" || snapshot.status === "closed" && unheldStop) ensureConnecting();
|
|
501
610
|
let released = false;
|
|
502
611
|
return () => {
|
|
503
612
|
if (released) return;
|
|
@@ -551,7 +660,9 @@ function createClient(url, options = {}) {
|
|
|
551
660
|
connect,
|
|
552
661
|
close,
|
|
553
662
|
reconnect,
|
|
554
|
-
|
|
663
|
+
migrate,
|
|
664
|
+
acquire,
|
|
665
|
+
[INSPECT]: () => ({ queued: pending.length })
|
|
555
666
|
};
|
|
556
667
|
}
|
|
557
668
|
/**
|
|
@@ -577,6 +688,175 @@ function createWebSocket(url, options) {
|
|
|
577
688
|
return createClient(url, options);
|
|
578
689
|
}
|
|
579
690
|
//#endregion
|
|
691
|
+
//#region src/topics.ts
|
|
692
|
+
/** @internal Option key for a callback run when nothing holds a topic anymore. Used by feeds. */
|
|
693
|
+
const ON_RELEASE = Symbol("react-websocket.onRelease");
|
|
694
|
+
/**
|
|
695
|
+
* Manage topic subscriptions that survive reconnects.
|
|
696
|
+
*
|
|
697
|
+
* - The first holder of a topic sends `subscribe(topic)`. Later holders share it.
|
|
698
|
+
* - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode
|
|
699
|
+
* and route transitions don't churn subscriptions.
|
|
700
|
+
* - After every reconnect, every held topic is subscribed again on the new socket, before
|
|
701
|
+
* messages queued while disconnected are sent.
|
|
702
|
+
*
|
|
703
|
+
* Subscriptions are only sent while the socket is open; nothing is queued for them.
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```ts
|
|
707
|
+
* export const topics = createWebSocketTopics(live, {
|
|
708
|
+
* subscribe: topic => ({ type: 'subscribe', topic }),
|
|
709
|
+
* unsubscribe: topic => ({ type: 'unsubscribe', topic })
|
|
710
|
+
* });
|
|
711
|
+
*
|
|
712
|
+
* // In a component:
|
|
713
|
+
* useWebSocketTopic(topics, `prices:${symbol}`);
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
function createWebSocketTopics(client, options) {
|
|
717
|
+
const counts = /* @__PURE__ */ new Map();
|
|
718
|
+
const pendingRemoval = /* @__PURE__ */ new Map();
|
|
719
|
+
let sentOn = null;
|
|
720
|
+
let sent = /* @__PURE__ */ new Set();
|
|
721
|
+
function isOpen() {
|
|
722
|
+
return client.socket?.readyState === 1;
|
|
723
|
+
}
|
|
724
|
+
function subscribe(topic) {
|
|
725
|
+
if (!isOpen()) return;
|
|
726
|
+
if (client.socket !== sentOn) {
|
|
727
|
+
sentOn = client.socket;
|
|
728
|
+
sent = /* @__PURE__ */ new Set();
|
|
729
|
+
}
|
|
730
|
+
if (sent.has(topic)) return;
|
|
731
|
+
sent.add(topic);
|
|
732
|
+
client.send(options.subscribe(topic));
|
|
733
|
+
}
|
|
734
|
+
function unsubscribe(topic) {
|
|
735
|
+
if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;
|
|
736
|
+
if (options.unsubscribe) client.send(options.unsubscribe(topic));
|
|
737
|
+
}
|
|
738
|
+
const off = client.on("open", () => {
|
|
739
|
+
sentOn = client.socket;
|
|
740
|
+
sent = /* @__PURE__ */ new Set();
|
|
741
|
+
for (const topic of counts.keys()) subscribe(topic);
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
client,
|
|
745
|
+
acquire(topic) {
|
|
746
|
+
const releaseConnection = client.acquire();
|
|
747
|
+
clearTimeout(pendingRemoval.get(topic));
|
|
748
|
+
pendingRemoval.delete(topic);
|
|
749
|
+
counts.set(topic, (counts.get(topic) ?? 0) + 1);
|
|
750
|
+
subscribe(topic);
|
|
751
|
+
let released = false;
|
|
752
|
+
return () => {
|
|
753
|
+
if (released) return;
|
|
754
|
+
released = true;
|
|
755
|
+
const remaining = (counts.get(topic) ?? 1) - 1;
|
|
756
|
+
if (remaining > 0) counts.set(topic, remaining);
|
|
757
|
+
else {
|
|
758
|
+
counts.delete(topic);
|
|
759
|
+
pendingRemoval.set(topic, setTimeout(() => {
|
|
760
|
+
pendingRemoval.delete(topic);
|
|
761
|
+
if (!counts.has(topic)) {
|
|
762
|
+
unsubscribe(topic);
|
|
763
|
+
options[ON_RELEASE]?.(topic);
|
|
764
|
+
}
|
|
765
|
+
}, 0));
|
|
766
|
+
}
|
|
767
|
+
releaseConnection();
|
|
768
|
+
};
|
|
769
|
+
},
|
|
770
|
+
active: () => [...counts.keys()],
|
|
771
|
+
dispose() {
|
|
772
|
+
off();
|
|
773
|
+
for (const timer of pendingRemoval.values()) clearTimeout(timer);
|
|
774
|
+
pendingRemoval.clear();
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/feeds.ts
|
|
780
|
+
/**
|
|
781
|
+
* Subscribe to topics whose messages carry a sequence number, and get each message once, in
|
|
782
|
+
* order, across reconnects:
|
|
783
|
+
*
|
|
784
|
+
* - On every new connection, each held topic is subscribed again with `after`, the last
|
|
785
|
+
* number received, so the server sends what was missed instead of everything.
|
|
786
|
+
* - A message numbered at or below the last one is a duplicate and is dropped.
|
|
787
|
+
* - A message that skips numbers is held back, and the topic asks for what's missing (`resume`)
|
|
788
|
+
* once, so nothing is delivered out of order.
|
|
789
|
+
* - A server that can't replay sends a `snapshot`, and the numbering goes on from it.
|
|
790
|
+
*
|
|
791
|
+
* Listen with `feeds.on('message', …)`, or pass `feeds` to `createWebSocketState` in place of
|
|
792
|
+
* the client. Hold topics with `useWebSocketTopic(feeds, topic)` or `feeds.acquire(topic)`.
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* ```ts
|
|
796
|
+
* export const feeds = createWebSocketFeeds(live, {
|
|
797
|
+
* subscribe: (topic, after) => ({ type: 'subscribe', topic, after }),
|
|
798
|
+
* unsubscribe: topic => ({ type: 'unsubscribe', topic }),
|
|
799
|
+
* sequence: message => (message.type === 'event' ? message : undefined),
|
|
800
|
+
* snapshot: message => (message.type === 'snapshot' ? message : undefined)
|
|
801
|
+
* });
|
|
802
|
+
*
|
|
803
|
+
* export const chat = createWebSocketState(feeds, { initialState: [], reduce: addMessage });
|
|
804
|
+
* ```
|
|
805
|
+
*/
|
|
806
|
+
function createWebSocketFeeds(client, options) {
|
|
807
|
+
const last = /* @__PURE__ */ new Map();
|
|
808
|
+
const resuming = /* @__PURE__ */ new Set();
|
|
809
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
810
|
+
const topics = createWebSocketTopics(client, {
|
|
811
|
+
subscribe: (topic) => options.subscribe(topic, last.get(topic)),
|
|
812
|
+
unsubscribe: options.unsubscribe,
|
|
813
|
+
[ON_RELEASE]: (topic) => {
|
|
814
|
+
last.delete(topic);
|
|
815
|
+
resuming.delete(topic);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
const offs = [client.on("open", () => resuming.clear()), client.on("message", (message, event) => {
|
|
819
|
+
const snapshot = options.snapshot?.(message);
|
|
820
|
+
const position = snapshot ?? options.sequence(message);
|
|
821
|
+
if (position) {
|
|
822
|
+
const { topic, seq } = position;
|
|
823
|
+
const prev = last.get(topic);
|
|
824
|
+
if (prev !== void 0 && !snapshot && seq !== prev + 1) {
|
|
825
|
+
if (seq > prev && !resuming.has(topic)) {
|
|
826
|
+
resuming.add(topic);
|
|
827
|
+
options.onGap?.(topic, prev, seq);
|
|
828
|
+
if (client.socket?.readyState === 1) client.send((options.resume ?? options.subscribe)(topic, prev));
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (prev !== void 0 || topics.active().includes(topic)) last.set(topic, seq);
|
|
833
|
+
resuming.delete(topic);
|
|
834
|
+
}
|
|
835
|
+
for (const listener of [...listeners]) try {
|
|
836
|
+
listener(message, event);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
queueMicrotask(() => {
|
|
839
|
+
throw error;
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
})];
|
|
843
|
+
return {
|
|
844
|
+
...topics,
|
|
845
|
+
on(_type, listener) {
|
|
846
|
+
const entry = (message, event) => listener(message, event);
|
|
847
|
+
listeners.add(entry);
|
|
848
|
+
return () => {
|
|
849
|
+
listeners.delete(entry);
|
|
850
|
+
};
|
|
851
|
+
},
|
|
852
|
+
position: (topic) => last.get(topic),
|
|
853
|
+
dispose() {
|
|
854
|
+
topics.dispose();
|
|
855
|
+
for (const off of offs) off();
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
580
860
|
//#region src/latency.ts
|
|
581
861
|
const EMPTY = Object.freeze({
|
|
582
862
|
rtt: void 0,
|
|
@@ -686,9 +966,118 @@ function createWebSocketLatency(client, options) {
|
|
|
686
966
|
};
|
|
687
967
|
}
|
|
688
968
|
//#endregion
|
|
969
|
+
//#region src/presence.ts
|
|
970
|
+
const VOLATILE$1 = { volatile: true };
|
|
971
|
+
/**
|
|
972
|
+
* Share per-peer state that expires, for presence, live cursors and selections.
|
|
973
|
+
*
|
|
974
|
+
* - `set(state)` shares this client's state, throttled to `maxRate` sends per second with the
|
|
975
|
+
* newest winning, and sent again on every new connection and every `refresh`, so it's never
|
|
976
|
+
* queued stale while disconnected.
|
|
977
|
+
* - Other peers' states come from `receive`. A peer is forgotten when it leaves (`state: null`)
|
|
978
|
+
* or when nothing has arrived from it for `timeout`.
|
|
979
|
+
*
|
|
980
|
+
* The server relays each client's state to the others, tagged with a peer id, and says
|
|
981
|
+
* `state: null` when a client disconnects.
|
|
982
|
+
*
|
|
983
|
+
* @example
|
|
984
|
+
* ```ts
|
|
985
|
+
* export const cursors = createWebSocketPresence(room, {
|
|
986
|
+
* send: (cursor: { x: number; y: number }) => ({ type: 'cursor', cursor }),
|
|
987
|
+
* leave: () => ({ type: 'leave' }),
|
|
988
|
+
* receive: message => (message.type === 'cursor' ? { peer: message.peer, state: message.cursor } : undefined)
|
|
989
|
+
* });
|
|
990
|
+
*
|
|
991
|
+
* window.addEventListener('pointermove', event => cursors.set({ x: event.clientX, y: event.clientY }));
|
|
992
|
+
* ```
|
|
993
|
+
*/
|
|
994
|
+
function createWebSocketPresence(client, options) {
|
|
995
|
+
const timeout = options.timeout ?? 3e4;
|
|
996
|
+
const interval = 1e3 / (options.maxRate ?? 20);
|
|
997
|
+
const peers = /* @__PURE__ */ new Map();
|
|
998
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
999
|
+
let snapshot = [];
|
|
1000
|
+
let own;
|
|
1001
|
+
let lastSent = -Infinity;
|
|
1002
|
+
let sendTimer;
|
|
1003
|
+
let refreshTimer;
|
|
1004
|
+
let expiryTimer;
|
|
1005
|
+
function changed() {
|
|
1006
|
+
snapshot = [...peers.values()];
|
|
1007
|
+
for (const listener of [...listeners]) listener();
|
|
1008
|
+
}
|
|
1009
|
+
function sendNow() {
|
|
1010
|
+
clearTimeout(sendTimer);
|
|
1011
|
+
sendTimer = void 0;
|
|
1012
|
+
if (!own) return;
|
|
1013
|
+
lastSent = Date.now();
|
|
1014
|
+
client.send(options.send(own.state), VOLATILE$1);
|
|
1015
|
+
}
|
|
1016
|
+
function expire() {
|
|
1017
|
+
expiryTimer = void 0;
|
|
1018
|
+
const now = Date.now();
|
|
1019
|
+
let next = Infinity;
|
|
1020
|
+
let removed = false;
|
|
1021
|
+
for (const [peer, entry] of peers) {
|
|
1022
|
+
const at = entry.updatedAt + timeout;
|
|
1023
|
+
if (at <= now) removed = peers.delete(peer);
|
|
1024
|
+
else next = Math.min(next, at);
|
|
1025
|
+
}
|
|
1026
|
+
if (next < Infinity) expiryTimer = setTimeout(expire, next - now);
|
|
1027
|
+
if (removed) changed();
|
|
1028
|
+
}
|
|
1029
|
+
const offs = [client.on("open", sendNow), client.on("message", (message) => {
|
|
1030
|
+
const update = options.receive(message);
|
|
1031
|
+
if (!update) return;
|
|
1032
|
+
if (update.state === null) {
|
|
1033
|
+
if (!peers.delete(update.peer)) return;
|
|
1034
|
+
} else {
|
|
1035
|
+
peers.set(update.peer, {
|
|
1036
|
+
peer: update.peer,
|
|
1037
|
+
state: update.state,
|
|
1038
|
+
updatedAt: Date.now()
|
|
1039
|
+
});
|
|
1040
|
+
expiryTimer ??= setTimeout(expire, timeout);
|
|
1041
|
+
}
|
|
1042
|
+
changed();
|
|
1043
|
+
})];
|
|
1044
|
+
return {
|
|
1045
|
+
client,
|
|
1046
|
+
set(state) {
|
|
1047
|
+
own = { state };
|
|
1048
|
+
refreshTimer ??= setInterval(sendNow, options.refresh ?? timeout / 3);
|
|
1049
|
+
if (sendTimer !== void 0) return;
|
|
1050
|
+
const wait = lastSent + interval - Date.now();
|
|
1051
|
+
if (wait <= 0) sendNow();
|
|
1052
|
+
else sendTimer = setTimeout(sendNow, wait);
|
|
1053
|
+
},
|
|
1054
|
+
leave() {
|
|
1055
|
+
own = void 0;
|
|
1056
|
+
clearTimeout(sendTimer);
|
|
1057
|
+
sendTimer = void 0;
|
|
1058
|
+
clearInterval(refreshTimer);
|
|
1059
|
+
refreshTimer = void 0;
|
|
1060
|
+
if (options.leave) client.send(options.leave(), VOLATILE$1);
|
|
1061
|
+
},
|
|
1062
|
+
getSnapshot: () => snapshot,
|
|
1063
|
+
subscribe(listener) {
|
|
1064
|
+
listeners.add(listener);
|
|
1065
|
+
return () => {
|
|
1066
|
+
listeners.delete(listener);
|
|
1067
|
+
};
|
|
1068
|
+
},
|
|
1069
|
+
dispose() {
|
|
1070
|
+
own = void 0;
|
|
1071
|
+
clearTimeout(sendTimer);
|
|
1072
|
+
clearInterval(refreshTimer);
|
|
1073
|
+
clearTimeout(expiryTimer);
|
|
1074
|
+
for (const off of offs) off();
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
689
1079
|
//#region src/requests.ts
|
|
690
1080
|
const lastIds = /* @__PURE__ */ new WeakMap();
|
|
691
|
-
const VOLATILE$2 = { volatile: true };
|
|
692
1081
|
const noop = () => {};
|
|
693
1082
|
/**
|
|
694
1083
|
* Send requests and await their replies, matched by an id the library generates.
|
|
@@ -742,6 +1131,10 @@ function createWebSocketRequests(client, options) {
|
|
|
742
1131
|
done(true, signal.reason);
|
|
743
1132
|
return noop;
|
|
744
1133
|
}
|
|
1134
|
+
if (client.getSnapshot().status === "closed") {
|
|
1135
|
+
done(true, /* @__PURE__ */ new Error("WebSocket is closed: request not sent"));
|
|
1136
|
+
return noop;
|
|
1137
|
+
}
|
|
745
1138
|
const id = (lastIds.get(client) ?? 0) + 1;
|
|
746
1139
|
lastIds.set(client, id);
|
|
747
1140
|
const key = String(id);
|
|
@@ -766,7 +1159,8 @@ function createWebSocketRequests(client, options) {
|
|
|
766
1159
|
release();
|
|
767
1160
|
done(failed, reason);
|
|
768
1161
|
if (cancel && options.cancel) try {
|
|
769
|
-
client.send(options.cancel(id),
|
|
1162
|
+
if (!entry.socket) client.send(options.cancel(id), { priority });
|
|
1163
|
+
else if (entry.socket === client.socket && entry.socket.readyState === 1) client.send(options.cancel(id));
|
|
770
1164
|
} catch (error) {
|
|
771
1165
|
queueMicrotask(() => {
|
|
772
1166
|
throw error;
|
|
@@ -777,8 +1171,7 @@ function createWebSocketRequests(client, options) {
|
|
|
777
1171
|
waiting.set(key, entry);
|
|
778
1172
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
779
1173
|
arm();
|
|
780
|
-
|
|
781
|
-
if (status === "closed" || status === "idle") entry.end(true, /* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
|
|
1174
|
+
if (client.getSnapshot().status === "idle") entry.end(true, /* @__PURE__ */ new Error("WebSocket is idle: request not sent"));
|
|
782
1175
|
else try {
|
|
783
1176
|
if (client.send(options.send(message, id), {
|
|
784
1177
|
...requestOptions,
|
|
@@ -849,7 +1242,6 @@ function createWebSocketRequests(client, options) {
|
|
|
849
1242
|
}
|
|
850
1243
|
//#endregion
|
|
851
1244
|
//#region src/responder.ts
|
|
852
|
-
const VOLATILE$1 = { volatile: true };
|
|
853
1245
|
const abortError = (message) => new DOMException(message, "AbortError");
|
|
854
1246
|
/**
|
|
855
1247
|
* Answer requests the server sends: run a handler for tools that only exist in the browser
|
|
@@ -887,12 +1279,16 @@ function createWebSocketResponder(client, options) {
|
|
|
887
1279
|
waiting = next;
|
|
888
1280
|
for (const listener of [...listeners]) listener();
|
|
889
1281
|
}
|
|
890
|
-
/**
|
|
1282
|
+
/**
|
|
1283
|
+
* Send a reply, or an `error` reply if building or encoding it throws. Only while open: never
|
|
1284
|
+
* queued, and not `volatile`, which can be a datagram that never arrives.
|
|
1285
|
+
*/
|
|
891
1286
|
function reply(id, build) {
|
|
1287
|
+
if (client.socket?.readyState !== 1) return false;
|
|
892
1288
|
try {
|
|
893
|
-
return client.send(build()
|
|
1289
|
+
return client.send(build());
|
|
894
1290
|
} catch (error) {
|
|
895
|
-
return client.send(options.error(id, error)
|
|
1291
|
+
return client.send(options.error(id, error));
|
|
896
1292
|
}
|
|
897
1293
|
}
|
|
898
1294
|
function receive(incoming) {
|
|
@@ -991,6 +1387,221 @@ function createWebSocketResponder(client, options) {
|
|
|
991
1387
|
};
|
|
992
1388
|
}
|
|
993
1389
|
//#endregion
|
|
1390
|
+
//#region src/runs.ts
|
|
1391
|
+
function randomId() {
|
|
1392
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1393
|
+
}
|
|
1394
|
+
const CLAIM_WAIT = 200;
|
|
1395
|
+
function defaultStorage() {
|
|
1396
|
+
try {
|
|
1397
|
+
return typeof sessionStorage === "undefined" ? null : sessionStorage;
|
|
1398
|
+
} catch {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Stream runs, such as an AI agent's answer, that survive reconnects and page reloads.
|
|
1404
|
+
*
|
|
1405
|
+
* - `start(request)` sends `start(request, runId)` and follows the run's events, folding them
|
|
1406
|
+
* into its value with `reduce`.
|
|
1407
|
+
* - Each event carries an `offset`. On a new connection, a streaming run sends
|
|
1408
|
+
* `resume(runId, after)` with the last offset received, so the server sends what was missed,
|
|
1409
|
+
* and duplicates are dropped.
|
|
1410
|
+
* - Streaming runs are remembered in `sessionStorage`. After a reload they're followed again from
|
|
1411
|
+
* their first event, rebuilding the value, so `list()` or `useAgentRun` can show them.
|
|
1412
|
+
* - A run whose policy is `cancel` is aborted when its connection drops, and `cancel` is sent
|
|
1413
|
+
* first thing on the next one.
|
|
1414
|
+
*
|
|
1415
|
+
* Built on `createWebSocketFeeds`, with each run a topic. While runs are streaming, it holds
|
|
1416
|
+
* the client's connection.
|
|
1417
|
+
*
|
|
1418
|
+
* @example
|
|
1419
|
+
* ```ts
|
|
1420
|
+
* export const runs = createWebSocketRuns(agent, {
|
|
1421
|
+
* name: 'chat',
|
|
1422
|
+
* start: (prompt: string, runId) => ({ type: 'run', runId, prompt }),
|
|
1423
|
+
* resume: (runId, after) => ({ type: 'resume', runId, after }),
|
|
1424
|
+
* cancel: runId => ({ type: 'cancel', runId }),
|
|
1425
|
+
* event: message => (message.type === 'token' || message.type === 'done' ? message : undefined),
|
|
1426
|
+
* done: message => message.type === 'done',
|
|
1427
|
+
* initial: '',
|
|
1428
|
+
* reduce: (text, message) => text + message.text
|
|
1429
|
+
* });
|
|
1430
|
+
*
|
|
1431
|
+
* const runId = runs.start('Summarize this thread');
|
|
1432
|
+
* ```
|
|
1433
|
+
*/
|
|
1434
|
+
function createWebSocketRuns(client, options) {
|
|
1435
|
+
const storage = options.storage === void 0 ? defaultStorage() : options.storage;
|
|
1436
|
+
const key = `react-websocket-runs:${options.name}`;
|
|
1437
|
+
const runs = /* @__PURE__ */ new Map();
|
|
1438
|
+
const starting = /* @__PURE__ */ new Map();
|
|
1439
|
+
const policies = /* @__PURE__ */ new Map();
|
|
1440
|
+
const releases = /* @__PURE__ */ new Map();
|
|
1441
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1442
|
+
let list = [];
|
|
1443
|
+
const feeds = createWebSocketFeeds(client, {
|
|
1444
|
+
subscribe: (runId, after) => after === void 0 && starting.has(runId) ? options.start(starting.get(runId), runId) : options.resume(runId, after),
|
|
1445
|
+
sequence: (message) => {
|
|
1446
|
+
const event = options.event(message);
|
|
1447
|
+
return event && {
|
|
1448
|
+
topic: event.runId,
|
|
1449
|
+
seq: event.offset
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
function read() {
|
|
1454
|
+
try {
|
|
1455
|
+
return JSON.parse(storage?.getItem(key) ?? "{}");
|
|
1456
|
+
} catch {
|
|
1457
|
+
return {};
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function remember(runId, policy) {
|
|
1461
|
+
if (!storage) return;
|
|
1462
|
+
const stored = read();
|
|
1463
|
+
if (policy) stored[runId] = policy;
|
|
1464
|
+
else delete stored[runId];
|
|
1465
|
+
try {
|
|
1466
|
+
if (Object.keys(stored).length) storage.setItem(key, JSON.stringify(stored));
|
|
1467
|
+
else storage.removeItem(key);
|
|
1468
|
+
} catch {}
|
|
1469
|
+
}
|
|
1470
|
+
function set(runId, patch) {
|
|
1471
|
+
const run = runs.get(runId);
|
|
1472
|
+
runs.set(runId, {
|
|
1473
|
+
...run ?? {
|
|
1474
|
+
runId,
|
|
1475
|
+
status: "streaming",
|
|
1476
|
+
value: options.initial,
|
|
1477
|
+
error: void 0
|
|
1478
|
+
},
|
|
1479
|
+
...patch
|
|
1480
|
+
});
|
|
1481
|
+
list = [...runs.values()];
|
|
1482
|
+
for (const listener of [...listeners]) listener();
|
|
1483
|
+
}
|
|
1484
|
+
/** Stop following a run that ended. */
|
|
1485
|
+
function end(runId, patch) {
|
|
1486
|
+
releases.get(runId)?.();
|
|
1487
|
+
releases.delete(runId);
|
|
1488
|
+
starting.delete(runId);
|
|
1489
|
+
policies.delete(runId);
|
|
1490
|
+
remember(runId, void 0);
|
|
1491
|
+
set(runId, patch);
|
|
1492
|
+
}
|
|
1493
|
+
function follow(runId, policy, value = options.initial) {
|
|
1494
|
+
policies.set(runId, policy);
|
|
1495
|
+
remember(runId, policy);
|
|
1496
|
+
set(runId, { value });
|
|
1497
|
+
releases.set(runId, feeds.acquire(runId));
|
|
1498
|
+
}
|
|
1499
|
+
function cancel(runId) {
|
|
1500
|
+
const message = options.cancel;
|
|
1501
|
+
if (!message) return;
|
|
1502
|
+
if (client.socket?.readyState === 1) return void client.send(message(runId));
|
|
1503
|
+
const release = client.acquire();
|
|
1504
|
+
const off = client.on("open", () => {
|
|
1505
|
+
off();
|
|
1506
|
+
client.send(message(runId));
|
|
1507
|
+
release();
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
function abort(runId) {
|
|
1511
|
+
if (runs.get(runId)?.status !== "streaming") return;
|
|
1512
|
+
cancel(runId);
|
|
1513
|
+
end(runId, { status: "aborted" });
|
|
1514
|
+
}
|
|
1515
|
+
let status = client.getSnapshot().status;
|
|
1516
|
+
const offs = [
|
|
1517
|
+
feeds.on("message", (message) => {
|
|
1518
|
+
const event = options.event(message);
|
|
1519
|
+
if (!event || runs.get(event.runId)?.status !== "streaming") return;
|
|
1520
|
+
const { runId } = event;
|
|
1521
|
+
starting.delete(runId);
|
|
1522
|
+
const error = options.error?.(message);
|
|
1523
|
+
if (error !== void 0) return end(runId, {
|
|
1524
|
+
status: "error",
|
|
1525
|
+
error
|
|
1526
|
+
});
|
|
1527
|
+
const value = options.reduce(runs.get(runId).value, message);
|
|
1528
|
+
if (options.done?.(message)) end(runId, {
|
|
1529
|
+
status: "done",
|
|
1530
|
+
value
|
|
1531
|
+
});
|
|
1532
|
+
else set(runId, { value });
|
|
1533
|
+
}),
|
|
1534
|
+
client.subscribe(() => {
|
|
1535
|
+
const previous = status;
|
|
1536
|
+
status = client.getSnapshot().status;
|
|
1537
|
+
if (previous !== "open" || status === "open") return;
|
|
1538
|
+
for (const [runId, policy] of policies) if (policy === "cancel") abort(runId);
|
|
1539
|
+
}),
|
|
1540
|
+
feeds.dispose
|
|
1541
|
+
];
|
|
1542
|
+
const channel = storage && typeof window !== "undefined" && typeof BroadcastChannel === "function" ? new BroadcastChannel(key) : void 0;
|
|
1543
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1544
|
+
if (channel) {
|
|
1545
|
+
channel.onmessage = ({ data }) => {
|
|
1546
|
+
if (data?.mine) claimed.add(data.mine);
|
|
1547
|
+
else if (data?.who && policies.has(data.who)) channel.postMessage({ mine: data.who });
|
|
1548
|
+
};
|
|
1549
|
+
offs.push(() => channel.close());
|
|
1550
|
+
}
|
|
1551
|
+
const stale = [];
|
|
1552
|
+
for (const [runId, policy] of Object.entries(read())) if (policy !== "cancel") follow(runId, policy);
|
|
1553
|
+
else if (channel) {
|
|
1554
|
+
stale.push(runId);
|
|
1555
|
+
channel.postMessage({ who: runId });
|
|
1556
|
+
} else {
|
|
1557
|
+
cancel(runId);
|
|
1558
|
+
remember(runId, void 0);
|
|
1559
|
+
}
|
|
1560
|
+
if (stale.length) {
|
|
1561
|
+
const timer = setTimeout(() => {
|
|
1562
|
+
for (const runId of stale) {
|
|
1563
|
+
if (claimed.has(runId) || policies.has(runId)) continue;
|
|
1564
|
+
cancel(runId);
|
|
1565
|
+
remember(runId, void 0);
|
|
1566
|
+
}
|
|
1567
|
+
}, CLAIM_WAIT);
|
|
1568
|
+
offs.push(() => clearTimeout(timer));
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
client,
|
|
1572
|
+
start(request, { runId = randomId(), policy = options.policy ?? "keep", initial } = {}) {
|
|
1573
|
+
if (runs.has(runId)) return runId;
|
|
1574
|
+
starting.set(runId, request);
|
|
1575
|
+
follow(runId, policy, initial);
|
|
1576
|
+
return runId;
|
|
1577
|
+
},
|
|
1578
|
+
attach(runId, { policy = options.policy ?? "keep", initial } = {}) {
|
|
1579
|
+
if (!runs.has(runId)) follow(runId, policy, initial);
|
|
1580
|
+
},
|
|
1581
|
+
abort,
|
|
1582
|
+
get: (runId) => runs.get(runId),
|
|
1583
|
+
list: () => list,
|
|
1584
|
+
subscribe(listener) {
|
|
1585
|
+
listeners.add(listener);
|
|
1586
|
+
return () => {
|
|
1587
|
+
listeners.delete(listener);
|
|
1588
|
+
};
|
|
1589
|
+
},
|
|
1590
|
+
delete(runId) {
|
|
1591
|
+
abort(runId);
|
|
1592
|
+
if (runs.delete(runId)) {
|
|
1593
|
+
list = [...runs.values()];
|
|
1594
|
+
for (const listener of [...listeners]) listener();
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
dispose() {
|
|
1598
|
+
for (const release of releases.values()) release();
|
|
1599
|
+
releases.clear();
|
|
1600
|
+
for (const off of offs) off();
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
//#endregion
|
|
994
1605
|
//#region src/sendrate.ts
|
|
995
1606
|
const VOLATILE = { volatile: true };
|
|
996
1607
|
/**
|
|
@@ -1114,6 +1725,9 @@ function createWebSocketSendRate(client, options = {}) {
|
|
|
1114
1725
|
* It starts listening immediately and keeps the state across reconnects; it doesn't open
|
|
1115
1726
|
* the connection by itself.
|
|
1116
1727
|
*
|
|
1728
|
+
* Pass feeds from `createWebSocketFeeds` in place of the client to reduce their messages
|
|
1729
|
+
* instead: numbered ones once each and in order.
|
|
1730
|
+
*
|
|
1117
1731
|
* @example
|
|
1118
1732
|
* ```ts
|
|
1119
1733
|
* const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });
|
|
@@ -1127,7 +1741,8 @@ function createWebSocketSendRate(client, options = {}) {
|
|
|
1127
1741
|
* const acme = useWebSocketState(priceState, state => state.ACME);
|
|
1128
1742
|
* ```
|
|
1129
1743
|
*/
|
|
1130
|
-
function createWebSocketState(
|
|
1744
|
+
function createWebSocketState(source, options) {
|
|
1745
|
+
const client = "client" in source ? source.client : source;
|
|
1131
1746
|
const { initialState, reduce, batch } = options;
|
|
1132
1747
|
let state = initialState;
|
|
1133
1748
|
let notified = state;
|
|
@@ -1165,101 +1780,22 @@ function createWebSocketState(client, options) {
|
|
|
1165
1780
|
},
|
|
1166
1781
|
setState: (change) => update(change),
|
|
1167
1782
|
reset: () => update(initialState),
|
|
1168
|
-
dispose:
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
//#endregion
|
|
1172
|
-
//#region src/topics.ts
|
|
1173
|
-
/**
|
|
1174
|
-
* Manage topic subscriptions that survive reconnects.
|
|
1175
|
-
*
|
|
1176
|
-
* - The first holder of a topic sends `subscribe(topic)`. Later holders share it.
|
|
1177
|
-
* - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode
|
|
1178
|
-
* and route transitions don't churn subscriptions.
|
|
1179
|
-
* - After every reconnect, every held topic is subscribed again on the new socket, before
|
|
1180
|
-
* messages queued while disconnected are sent.
|
|
1181
|
-
*
|
|
1182
|
-
* Subscriptions are only sent while the socket is open; nothing is queued for them.
|
|
1183
|
-
*
|
|
1184
|
-
* @example
|
|
1185
|
-
* ```ts
|
|
1186
|
-
* export const topics = createWebSocketTopics(live, {
|
|
1187
|
-
* subscribe: topic => ({ type: 'subscribe', topic }),
|
|
1188
|
-
* unsubscribe: topic => ({ type: 'unsubscribe', topic })
|
|
1189
|
-
* });
|
|
1190
|
-
*
|
|
1191
|
-
* // In a component:
|
|
1192
|
-
* useWebSocketTopic(topics, `prices:${symbol}`);
|
|
1193
|
-
* ```
|
|
1194
|
-
*/
|
|
1195
|
-
function createWebSocketTopics(client, options) {
|
|
1196
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1197
|
-
const pendingRemoval = /* @__PURE__ */ new Map();
|
|
1198
|
-
let sentOn = null;
|
|
1199
|
-
let sent = /* @__PURE__ */ new Set();
|
|
1200
|
-
function isOpen() {
|
|
1201
|
-
return client.getSnapshot().status === "open" && client.socket !== null;
|
|
1202
|
-
}
|
|
1203
|
-
function subscribe(topic) {
|
|
1204
|
-
if (!isOpen()) return;
|
|
1205
|
-
if (client.socket !== sentOn) {
|
|
1206
|
-
sentOn = client.socket;
|
|
1207
|
-
sent = /* @__PURE__ */ new Set();
|
|
1208
|
-
}
|
|
1209
|
-
if (sent.has(topic)) return;
|
|
1210
|
-
sent.add(topic);
|
|
1211
|
-
client.send(options.subscribe(topic));
|
|
1212
|
-
}
|
|
1213
|
-
function unsubscribe(topic) {
|
|
1214
|
-
if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;
|
|
1215
|
-
if (options.unsubscribe) client.send(options.unsubscribe(topic));
|
|
1216
|
-
}
|
|
1217
|
-
const off = client.on("open", () => {
|
|
1218
|
-
sentOn = client.socket;
|
|
1219
|
-
sent = /* @__PURE__ */ new Set();
|
|
1220
|
-
for (const topic of counts.keys()) subscribe(topic);
|
|
1221
|
-
});
|
|
1222
|
-
return {
|
|
1223
|
-
client,
|
|
1224
|
-
acquire(topic) {
|
|
1225
|
-
const releaseConnection = client.acquire();
|
|
1226
|
-
clearTimeout(pendingRemoval.get(topic));
|
|
1227
|
-
pendingRemoval.delete(topic);
|
|
1228
|
-
counts.set(topic, (counts.get(topic) ?? 0) + 1);
|
|
1229
|
-
subscribe(topic);
|
|
1230
|
-
let released = false;
|
|
1231
|
-
return () => {
|
|
1232
|
-
if (released) return;
|
|
1233
|
-
released = true;
|
|
1234
|
-
const remaining = (counts.get(topic) ?? 1) - 1;
|
|
1235
|
-
if (remaining > 0) counts.set(topic, remaining);
|
|
1236
|
-
else {
|
|
1237
|
-
counts.delete(topic);
|
|
1238
|
-
pendingRemoval.set(topic, setTimeout(() => {
|
|
1239
|
-
pendingRemoval.delete(topic);
|
|
1240
|
-
if (!counts.has(topic)) unsubscribe(topic);
|
|
1241
|
-
}, 0));
|
|
1242
|
-
}
|
|
1243
|
-
releaseConnection();
|
|
1244
|
-
};
|
|
1245
|
-
},
|
|
1246
|
-
active: () => [...counts.keys()],
|
|
1247
|
-
dispose() {
|
|
1248
|
-
off();
|
|
1249
|
-
for (const timer of pendingRemoval.values()) clearTimeout(timer);
|
|
1250
|
-
pendingRemoval.clear();
|
|
1251
|
-
}
|
|
1783
|
+
dispose: source.on("message", (message) => update((current) => reduce(current, message), batch === "frame"))
|
|
1252
1784
|
};
|
|
1253
1785
|
}
|
|
1254
1786
|
//#endregion
|
|
1255
1787
|
exports.MessageError = MessageError;
|
|
1256
1788
|
exports.createWebSocket = createWebSocket;
|
|
1789
|
+
exports.createWebSocketFeeds = createWebSocketFeeds;
|
|
1257
1790
|
exports.createWebSocketLatency = createWebSocketLatency;
|
|
1791
|
+
exports.createWebSocketPresence = createWebSocketPresence;
|
|
1258
1792
|
exports.createWebSocketRequests = createWebSocketRequests;
|
|
1259
1793
|
exports.createWebSocketResponder = createWebSocketResponder;
|
|
1794
|
+
exports.createWebSocketRuns = createWebSocketRuns;
|
|
1260
1795
|
exports.createWebSocketSendRate = createWebSocketSendRate;
|
|
1261
1796
|
exports.createWebSocketState = createWebSocketState;
|
|
1262
1797
|
exports.createWebSocketTopics = createWebSocketTopics;
|
|
1798
|
+
exports.retryAfter = retryAfter;
|
|
1263
1799
|
exports.retryableClose = retryableClose;
|
|
1264
1800
|
|
|
1265
1801
|
//# sourceMappingURL=core.cjs.map
|