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