react-websocket 3.6.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.
Files changed (52) hide show
  1. package/README.md +34 -5
  2. package/dist/ag-ui.cjs +421 -0
  3. package/dist/ag-ui.cjs.map +1 -0
  4. package/dist/ag-ui.d.cts +493 -0
  5. package/dist/ag-ui.d.ts +171 -0
  6. package/dist/ag-ui.js +418 -0
  7. package/dist/ag-ui.js.map +1 -0
  8. package/dist/core.cjs +727 -162
  9. package/dist/core.cjs.map +1 -1
  10. package/dist/core.d.cts +501 -62
  11. package/dist/core.d.ts +501 -62
  12. package/dist/core.js +723 -163
  13. package/dist/core.js.map +1 -1
  14. package/dist/devtools.cjs +186 -0
  15. package/dist/devtools.cjs.map +1 -0
  16. package/dist/devtools.d.cts +32 -0
  17. package/dist/devtools.d.ts +32 -0
  18. package/dist/devtools.js +185 -0
  19. package/dist/devtools.js.map +1 -0
  20. package/dist/index.cjs +214 -19
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +705 -56
  23. package/dist/index.d.ts +421 -30
  24. package/dist/index.js +211 -21
  25. package/dist/index.js.map +1 -1
  26. package/dist/outbox.cjs +222 -0
  27. package/dist/outbox.cjs.map +1 -0
  28. package/dist/outbox.d.cts +127 -0
  29. package/dist/outbox.d.ts +127 -0
  30. package/dist/outbox.js +219 -0
  31. package/dist/outbox.js.map +1 -0
  32. package/dist/replay.cjs +324 -0
  33. package/dist/replay.cjs.map +1 -0
  34. package/dist/replay.d.cts +124 -0
  35. package/dist/replay.d.ts +124 -0
  36. package/dist/replay.js +322 -0
  37. package/dist/replay.js.map +1 -0
  38. package/dist/tabs.cjs +336 -0
  39. package/dist/tabs.cjs.map +1 -0
  40. package/dist/tabs.d.cts +81 -0
  41. package/dist/tabs.d.ts +81 -0
  42. package/dist/tabs.js +335 -0
  43. package/dist/tabs.js.map +1 -0
  44. package/dist/timeline.d.cts +1 -213
  45. package/dist/types-BEecFqIB.d.cts +268 -0
  46. package/dist/webtransport.cjs +243 -0
  47. package/dist/webtransport.cjs.map +1 -0
  48. package/dist/webtransport.d.cts +67 -0
  49. package/dist/webtransport.d.ts +67 -0
  50. package/dist/webtransport.js +242 -0
  51. package/dist/webtransport.js.map +1 -0
  52. package/package.json +139 -3
package/dist/core.js CHANGED
@@ -30,8 +30,43 @@ const RECONNECT_DEFAULTS = {
30
30
  minDelay: 1e3,
31
31
  maxDelay: 3e4,
32
32
  factor: 2,
33
- jitter: .5
33
+ jitter: 1,
34
+ minUptime: 5e3
34
35
  };
36
+ const QUEUE_LIMIT = 100;
37
+ const MAX_TIMEOUT = 2 ** 31 - 1;
38
+ /**
39
+ * The default `reconnect.shouldReconnect`: retry every close except `1002` (protocol error),
40
+ * `1003` (unsupported data) and `1008` (policy violation, often a rejected login), which
41
+ * retrying the same way can't fix. Call it from your own `shouldReconnect` to keep that rule.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * createWebSocket(url, {
46
+ * // Also stop on our server's "invalid token" code.
47
+ * reconnect: { shouldReconnect: close => close.code !== 4001 && retryableClose(close) }
48
+ * });
49
+ * ```
50
+ */
51
+ function retryableClose(close) {
52
+ return close.code !== 1002 && close.code !== 1003 && close.code !== 1008;
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
+ }
35
70
  const READY_GRACE = 5e3;
36
71
  function createDeferred() {
37
72
  let resolve;
@@ -76,6 +111,10 @@ function isThenable(value) {
76
111
  * because the hooks entry imports the client from the `core` entry at runtime.
77
112
  */
78
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");
79
118
  function createClient(url, options = {}) {
80
119
  const onTeardown = options[ON_TEARDOWN];
81
120
  const log = options.logger ?? (() => {});
@@ -83,7 +122,9 @@ function createClient(url, options = {}) {
83
122
  ...RECONNECT_DEFAULTS,
84
123
  ...typeof options.reconnect === "object" ? options.reconnect : {}
85
124
  };
86
- const queueLimit = options.queue === false ? 0 : typeof options.queue === "object" ? Math.max(0, options.queue.limit ?? Infinity) : Infinity;
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;
87
128
  let snapshot = {
88
129
  status: "idle",
89
130
  lastMessage: void 0,
@@ -97,9 +138,13 @@ function createClient(url, options = {}) {
97
138
  error: /* @__PURE__ */ new Set()
98
139
  };
99
140
  let ws = null;
141
+ let incoming = null;
142
+ let replaced = null;
143
+ let moved;
100
144
  let wanted = false;
101
145
  let refs = 0;
102
146
  let pinned = false;
147
+ let unheldStop = false;
103
148
  let pending = [];
104
149
  let ready = null;
105
150
  let opened = false;
@@ -108,8 +153,12 @@ function createClient(url, options = {}) {
108
153
  let inbox = null;
109
154
  let generation = 0;
110
155
  let windowBound = false;
156
+ let failures = 0;
157
+ let serverWait = false;
111
158
  let retryTimer;
159
+ let migrateTimer;
112
160
  let teardownTimer;
161
+ let stableTimer;
113
162
  let heartbeatTimer;
114
163
  let pongTimer;
115
164
  function set(patch) {
@@ -132,54 +181,62 @@ function createClient(url, options = {}) {
132
181
  }
133
182
  function getImpl() {
134
183
  if (options.WebSocket) return options.WebSocket;
135
- return typeof window === "undefined" ? void 0 : globalThis.WebSocket;
184
+ return typeof window === "undefined" && typeof WorkerGlobalScope === "undefined" ? void 0 : globalThis.WebSocket;
136
185
  }
137
186
  function rejectReady(reason) {
138
187
  if (!ready || ready.settled) ready = createDeferred();
139
188
  ready.reject(reason);
140
189
  }
141
- function retryNow() {
190
+ /** The network or the tab came back: retry now, or check that an open connection still works. */
191
+ function wake() {
142
192
  if (retryTimer !== void 0) {
193
+ if (serverWait) return;
143
194
  log("network or visibility change, retrying now");
144
195
  open();
196
+ } else if (heartbeatTimer !== void 0 && pongTimer === void 0) {
197
+ log("network or visibility change, checking the connection");
198
+ ping(ws);
145
199
  }
146
200
  }
147
201
  function onVisibilityChange() {
148
- if (document.visibilityState === "visible") retryNow();
202
+ if (document.visibilityState === "visible") wake();
149
203
  }
150
204
  function bindWindow() {
151
- if (windowBound || typeof window === "undefined") return;
205
+ if (windowBound || typeof addEventListener !== "function") return;
152
206
  windowBound = true;
153
- window.addEventListener("online", retryNow);
154
- document.addEventListener("visibilitychange", onVisibilityChange);
207
+ addEventListener("online", wake);
208
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibilityChange);
155
209
  }
156
210
  function unbindWindow() {
157
211
  if (!windowBound) return;
158
212
  windowBound = false;
159
- window.removeEventListener("online", retryNow);
160
- document.removeEventListener("visibilitychange", onVisibilityChange);
213
+ removeEventListener("online", wake);
214
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
161
215
  }
162
- function startHeartbeat(socket) {
216
+ /** Send a heartbeat ping and, with a `timeout`, close the socket if nothing comes back. */
217
+ function ping(socket) {
163
218
  const heartbeat = options.heartbeat;
164
- if (!heartbeat) return;
165
- heartbeatTimer = setInterval(() => {
166
- socket.send(heartbeat.message ?? "ping");
167
- if (heartbeat.timeout && pongTimer === void 0) pongTimer = setTimeout(() => {
168
- log("heartbeat timeout");
169
- detach(socket, false);
170
- socket.close(4e3, "heartbeat timeout");
171
- handleClose({
172
- code: 4e3,
173
- reason: "heartbeat timeout",
174
- wasClean: false
175
- });
176
- }, heartbeat.timeout);
177
- }, heartbeat.interval);
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);
178
233
  }
179
- function stopHeartbeat() {
234
+ /** Stop the timers that belong to the current open socket. */
235
+ function stopSocketTimers() {
180
236
  clearInterval(heartbeatTimer);
181
237
  clearTimeout(pongTimer);
182
- heartbeatTimer = pongTimer = void 0;
238
+ clearTimeout(stableTimer);
239
+ heartbeatTimer = pongTimer = stableTimer = void 0;
183
240
  }
184
241
  /** Stop tracking `socket`. Its eventual close event is still reported unless `reportClose` is false. */
185
242
  function detach(socket, reportClose) {
@@ -191,65 +248,100 @@ function createClient(url, options = {}) {
191
248
  wasClean: event.wasClean
192
249
  }) : null;
193
250
  }
194
- function backoff(attempt) {
251
+ /** The delay before attempt `n`; `slow` starts at `maxDelay`, for a server shedding load. */
252
+ function backoff(attempt, slow) {
195
253
  const { minDelay, maxDelay, factor, jitter } = retry;
196
- const base = Math.min(maxDelay, minDelay * factor ** (attempt - 1));
254
+ const base = slow ? maxDelay : Math.min(maxDelay, minDelay * factor ** (attempt - 1));
197
255
  return Math.round(base * (1 - jitter * Math.random()));
198
256
  }
199
- function open() {
200
- clearTimeout(retryTimer);
201
- retryTimer = void 0;
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
+ }
202
276
  const Impl = getImpl();
203
277
  if (!Impl) return;
204
278
  const id = ++generation;
205
- set({ status: snapshot.attempt === 0 ? "connecting" : "reconnecting" });
279
+ if (!handoff) set({ status: snapshot.attempt === 0 ? "connecting" : "reconnecting" });
280
+ const source = moved ?? url;
206
281
  let target;
207
282
  try {
208
- target = typeof url === "string" ? url : url();
283
+ target = typeof source === "string" ? source : source();
209
284
  } catch (error) {
210
- urlFailed(error);
285
+ urlFailed(error, handoff);
211
286
  return;
212
287
  }
213
288
  if (typeof target === "string") {
214
- start(Impl, target);
289
+ start(Impl, target, handoff);
215
290
  return;
216
291
  }
217
292
  Promise.resolve(target).then((resolved) => {
218
- if (id === generation && wanted) start(Impl, resolved);
293
+ if (id === generation && wanted) start(Impl, resolved, handoff);
219
294
  }, (error) => {
220
- if (id === generation && wanted) urlFailed(error);
295
+ if (id === generation && wanted) urlFailed(error, handoff);
221
296
  });
222
297
  }
223
- /** `url()` threw or rejected (e.g. a token refresh failed): count it as a failed attempt. */
224
- function urlFailed(error) {
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) {
225
303
  const reason = error instanceof Error ? error : new Error(String(error));
226
304
  log("url() failed", reason);
227
305
  emit("error", reason);
228
- retryOrFail({
306
+ if (!handoff) retryOrFail({
229
307
  code: 1006,
230
308
  reason: "url() failed",
231
309
  wasClean: false
232
310
  }, reason);
233
311
  }
234
- function start(Impl, target) {
312
+ function start(Impl, target, handoff) {
235
313
  let socket;
236
314
  try {
237
315
  socket = new Impl(target, options.protocols);
238
316
  } catch (error) {
239
317
  emit("error", error);
240
- fail(error);
318
+ if (!handoff) fail(error);
241
319
  return;
242
320
  }
243
321
  if (options.binaryType) socket.binaryType = options.binaryType;
244
- ws = socket;
322
+ if (handoff) incoming = socket;
323
+ else ws = socket;
245
324
  log("connecting", target);
246
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
+ }
247
338
  log("open");
248
339
  opened = true;
249
340
  set({
250
341
  status: "open",
251
342
  attempt: 0
252
343
  });
344
+ if (retry) stableTimer = setTimeout(() => failures = 0, retry.minUptime);
253
345
  startHeartbeat(socket);
254
346
  ready?.resolve();
255
347
  if (refs === 0 && !pinned) {
@@ -260,11 +352,20 @@ function createClient(url, options = {}) {
260
352
  if (ws !== socket) return;
261
353
  const queued = pending;
262
354
  pending = [];
263
- for (const { data } of queued) socket.send(data);
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);
264
358
  };
265
359
  socket.onmessage = (event) => receive(event, socket);
266
- socket.onerror = (event) => emit("error", event);
360
+ socket.onerror = (event) => {
361
+ if (ws === socket) emit("error", event);
362
+ };
267
363
  socket.onclose = (event) => {
364
+ if (ws !== socket) {
365
+ incoming = null;
366
+ log("migrate() failed", event.code);
367
+ return;
368
+ }
268
369
  detach(socket, false);
269
370
  handleClose({
270
371
  code: event.code,
@@ -274,15 +375,19 @@ function createClient(url, options = {}) {
274
375
  };
275
376
  }
276
377
  function handleClose(info) {
277
- stopHeartbeat();
378
+ replaced = null;
379
+ stopSocketTimers();
278
380
  log("closed", info);
279
381
  retryOrFail(info);
280
382
  emit("close", info);
281
383
  }
282
384
  function retryOrFail(info, error) {
283
- if (wanted && retry && snapshot.attempt < retry.maxAttempts && (retry.shouldReconnect?.(info) ?? true)) {
284
- const attempt = snapshot.attempt + 1;
285
- const delay = backoff(attempt);
385
+ if (wanted && retry && failures < retry.maxAttempts && (retry.shouldReconnect ?? retryableClose)(info)) {
386
+ const attempt = ++failures;
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;
286
391
  set({
287
392
  status: "reconnecting",
288
393
  attempt
@@ -291,12 +396,34 @@ function createClient(url, options = {}) {
291
396
  retryTimer = setTimeout(open, delay);
292
397
  } else fail(error ?? /* @__PURE__ */ new Error(`WebSocket closed (code ${info.code}${info.reason ? `: ${info.reason}` : ""})`));
293
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
+ }
294
416
  function fail(reason) {
295
417
  wanted = false;
296
418
  opened = false;
419
+ unheldStop = refs === 0;
297
420
  unbindWindow();
421
+ stopMigrating();
298
422
  pending = [];
299
- set({ status: "closed" });
423
+ set({
424
+ status: "closed",
425
+ attempt: failures
426
+ });
300
427
  rejectReady(reason);
301
428
  }
302
429
  function receive(event, socket) {
@@ -344,7 +471,7 @@ function createClient(url, options = {}) {
344
471
  else deliver(result.value, event, socket);
345
472
  }
346
473
  function deliver(message, event, socket) {
347
- if (ws !== socket) return;
474
+ if (ws !== socket && replaced !== socket) return;
348
475
  set({ lastMessage: message });
349
476
  emit("message", message, event);
350
477
  }
@@ -356,7 +483,9 @@ function createClient(url, options = {}) {
356
483
  log("dropped message: send buffer above maxBuffered");
357
484
  return false;
358
485
  }
359
- ws.send(data);
486
+ const datagram = sendOptions.volatile ? ws[DATAGRAM] : void 0;
487
+ if (datagram) datagram.call(ws, data);
488
+ else ws.send(data);
360
489
  return true;
361
490
  }
362
491
  if (queueLimit === 0 || sendOptions.volatile) {
@@ -367,18 +496,18 @@ function createClient(url, options = {}) {
367
496
  log("dropped message: no WebSocket implementation available");
368
497
  return false;
369
498
  }
370
- if (key !== void 0) pending = pending.filter((entry) => {
371
- if (entry.key !== key) return true;
372
- dropped(entry);
373
- return false;
374
- });
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);
375
503
  let head = pending.findIndex((entry) => !entry.priority);
376
504
  if (head < 0) head = pending.length;
377
505
  const entry = {
378
506
  data,
379
507
  key,
380
508
  priority,
381
- onDrop: sendOptions.onDrop
509
+ onDrop: sendOptions.onDrop,
510
+ expires: now + (sendOptions.maxAge ?? queueMaxAge)
382
511
  };
383
512
  if (priority) pending.splice(head++, 0, entry);
384
513
  else pending.push(entry);
@@ -388,6 +517,10 @@ function createClient(url, options = {}) {
388
517
  }
389
518
  return false;
390
519
  }
520
+ function expired(entry) {
521
+ log("dropped queued message: older than maxAge");
522
+ dropped(entry);
523
+ }
391
524
  function dropped(entry) {
392
525
  try {
393
526
  entry.onDrop?.();
@@ -397,10 +530,12 @@ function createClient(url, options = {}) {
397
530
  }
398
531
  function begin() {
399
532
  wanted = true;
533
+ unheldStop = false;
400
534
  if (snapshot.status === "idle" || snapshot.status === "closed") {
401
535
  opened = false;
402
536
  if (ready?.settled) ready = null;
403
537
  }
538
+ failures = 0;
404
539
  snapshot = {
405
540
  ...snapshot,
406
541
  attempt: 0
@@ -418,23 +553,25 @@ function createClient(url, options = {}) {
418
553
  ensureConnecting();
419
554
  }
420
555
  function stop(code, reason) {
556
+ const socket = ws;
557
+ socket?.close(code, reason);
421
558
  wanted = false;
422
559
  generation++;
423
560
  clearTimeout(retryTimer);
424
561
  retryTimer = void 0;
425
- stopHeartbeat();
562
+ failures = 0;
563
+ stopSocketTimers();
426
564
  unbindWindow();
565
+ stopMigrating();
427
566
  pending = [];
428
- if (ws) {
429
- const socket = ws;
430
- detach(socket, true);
431
- socket.close(code, reason);
432
- }
567
+ replaced = null;
568
+ if (socket) detach(socket, true);
433
569
  }
434
570
  function close(code = 1e3, reason) {
571
+ stop(code, reason);
435
572
  pinned = false;
436
573
  opened = false;
437
- stop(code, reason);
574
+ unheldStop = refs === 0;
438
575
  set({
439
576
  status: "closed",
440
577
  attempt: 0
@@ -445,7 +582,7 @@ function createClient(url, options = {}) {
445
582
  if (!getImpl()) return;
446
583
  clearTimeout(retryTimer);
447
584
  retryTimer = void 0;
448
- stopHeartbeat();
585
+ stopSocketTimers();
449
586
  if (ws) {
450
587
  const socket = ws;
451
588
  detach(socket, true);
@@ -468,7 +605,7 @@ function createClient(url, options = {}) {
468
605
  function acquire() {
469
606
  refs++;
470
607
  clearTimeout(teardownTimer);
471
- if (snapshot.status === "idle") ensureConnecting();
608
+ if (snapshot.status === "idle" || snapshot.status === "closed" && unheldStop) ensureConnecting();
472
609
  let released = false;
473
610
  return () => {
474
611
  if (released) return;
@@ -522,7 +659,9 @@ function createClient(url, options = {}) {
522
659
  connect,
523
660
  close,
524
661
  reconnect,
525
- acquire
662
+ migrate,
663
+ acquire,
664
+ [INSPECT]: () => ({ queued: pending.length })
526
665
  };
527
666
  }
528
667
  /**
@@ -548,6 +687,175 @@ function createWebSocket(url, options) {
548
687
  return createClient(url, options);
549
688
  }
550
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
551
859
  //#region src/latency.ts
552
860
  const EMPTY = Object.freeze({
553
861
  rtt: void 0,
@@ -657,9 +965,118 @@ function createWebSocketLatency(client, options) {
657
965
  };
658
966
  }
659
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
660
1078
  //#region src/requests.ts
661
1079
  const lastIds = /* @__PURE__ */ new WeakMap();
662
- const VOLATILE$2 = { volatile: true };
663
1080
  const noop = () => {};
664
1081
  /**
665
1082
  * Send requests and await their replies, matched by an id the library generates.
@@ -713,6 +1130,10 @@ function createWebSocketRequests(client, options) {
713
1130
  done(true, signal.reason);
714
1131
  return noop;
715
1132
  }
1133
+ if (client.getSnapshot().status === "closed") {
1134
+ done(true, /* @__PURE__ */ new Error("WebSocket is closed: request not sent"));
1135
+ return noop;
1136
+ }
716
1137
  const id = (lastIds.get(client) ?? 0) + 1;
717
1138
  lastIds.set(client, id);
718
1139
  const key = String(id);
@@ -737,7 +1158,8 @@ function createWebSocketRequests(client, options) {
737
1158
  release();
738
1159
  done(failed, reason);
739
1160
  if (cancel && options.cancel) try {
740
- client.send(options.cancel(id), entry.socket ? VOLATILE$2 : { priority });
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));
741
1163
  } catch (error) {
742
1164
  queueMicrotask(() => {
743
1165
  throw error;
@@ -748,8 +1170,7 @@ function createWebSocketRequests(client, options) {
748
1170
  waiting.set(key, entry);
749
1171
  signal?.addEventListener("abort", onAbort, { once: true });
750
1172
  arm();
751
- const { status } = client.getSnapshot();
752
- 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"));
753
1174
  else try {
754
1175
  if (client.send(options.send(message, id), {
755
1176
  ...requestOptions,
@@ -820,7 +1241,6 @@ function createWebSocketRequests(client, options) {
820
1241
  }
821
1242
  //#endregion
822
1243
  //#region src/responder.ts
823
- const VOLATILE$1 = { volatile: true };
824
1244
  const abortError = (message) => new DOMException(message, "AbortError");
825
1245
  /**
826
1246
  * Answer requests the server sends: run a handler for tools that only exist in the browser
@@ -858,12 +1278,16 @@ function createWebSocketResponder(client, options) {
858
1278
  waiting = next;
859
1279
  for (const listener of [...listeners]) listener();
860
1280
  }
861
- /** Send a reply, or an `error` reply if building or encoding it throws. */
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
+ */
862
1285
  function reply(id, build) {
1286
+ if (client.socket?.readyState !== 1) return false;
863
1287
  try {
864
- return client.send(build(), VOLATILE$1);
1288
+ return client.send(build());
865
1289
  } catch (error) {
866
- return client.send(options.error(id, error), VOLATILE$1);
1290
+ return client.send(options.error(id, error));
867
1291
  }
868
1292
  }
869
1293
  function receive(incoming) {
@@ -962,6 +1386,221 @@ function createWebSocketResponder(client, options) {
962
1386
  };
963
1387
  }
964
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
965
1604
  //#region src/sendrate.ts
966
1605
  const VOLATILE = { volatile: true };
967
1606
  /**
@@ -1085,6 +1724,9 @@ function createWebSocketSendRate(client, options = {}) {
1085
1724
  * It starts listening immediately and keeps the state across reconnects; it doesn't open
1086
1725
  * the connection by itself.
1087
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
+ *
1088
1730
  * @example
1089
1731
  * ```ts
1090
1732
  * const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });
@@ -1098,7 +1740,8 @@ function createWebSocketSendRate(client, options = {}) {
1098
1740
  * const acme = useWebSocketState(priceState, state => state.ACME);
1099
1741
  * ```
1100
1742
  */
1101
- function createWebSocketState(client, options) {
1743
+ function createWebSocketState(source, options) {
1744
+ const client = "client" in source ? source.client : source;
1102
1745
  const { initialState, reduce, batch } = options;
1103
1746
  let state = initialState;
1104
1747
  let notified = state;
@@ -1136,93 +1779,10 @@ function createWebSocketState(client, options) {
1136
1779
  },
1137
1780
  setState: (change) => update(change),
1138
1781
  reset: () => update(initialState),
1139
- dispose: client.on("message", (message) => update((current) => reduce(current, message), batch === "frame"))
1140
- };
1141
- }
1142
- //#endregion
1143
- //#region src/topics.ts
1144
- /**
1145
- * Manage topic subscriptions that survive reconnects.
1146
- *
1147
- * - The first holder of a topic sends `subscribe(topic)`. Later holders share it.
1148
- * - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode
1149
- * and route transitions don't churn subscriptions.
1150
- * - After every reconnect, every held topic is subscribed again on the new socket, before
1151
- * messages queued while disconnected are sent.
1152
- *
1153
- * Subscriptions are only sent while the socket is open; nothing is queued for them.
1154
- *
1155
- * @example
1156
- * ```ts
1157
- * export const topics = createWebSocketTopics(live, {
1158
- * subscribe: topic => ({ type: 'subscribe', topic }),
1159
- * unsubscribe: topic => ({ type: 'unsubscribe', topic })
1160
- * });
1161
- *
1162
- * // In a component:
1163
- * useWebSocketTopic(topics, `prices:${symbol}`);
1164
- * ```
1165
- */
1166
- function createWebSocketTopics(client, options) {
1167
- const counts = /* @__PURE__ */ new Map();
1168
- const pendingRemoval = /* @__PURE__ */ new Map();
1169
- let sentOn = null;
1170
- let sent = /* @__PURE__ */ new Set();
1171
- function isOpen() {
1172
- return client.getSnapshot().status === "open" && client.socket !== null;
1173
- }
1174
- function subscribe(topic) {
1175
- if (!isOpen()) return;
1176
- if (client.socket !== sentOn) {
1177
- sentOn = client.socket;
1178
- sent = /* @__PURE__ */ new Set();
1179
- }
1180
- if (sent.has(topic)) return;
1181
- sent.add(topic);
1182
- client.send(options.subscribe(topic));
1183
- }
1184
- function unsubscribe(topic) {
1185
- if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;
1186
- if (options.unsubscribe) client.send(options.unsubscribe(topic));
1187
- }
1188
- const off = client.on("open", () => {
1189
- sentOn = client.socket;
1190
- sent = /* @__PURE__ */ new Set();
1191
- for (const topic of counts.keys()) subscribe(topic);
1192
- });
1193
- return {
1194
- client,
1195
- acquire(topic) {
1196
- const releaseConnection = client.acquire();
1197
- clearTimeout(pendingRemoval.get(topic));
1198
- pendingRemoval.delete(topic);
1199
- counts.set(topic, (counts.get(topic) ?? 0) + 1);
1200
- subscribe(topic);
1201
- let released = false;
1202
- return () => {
1203
- if (released) return;
1204
- released = true;
1205
- const remaining = (counts.get(topic) ?? 1) - 1;
1206
- if (remaining > 0) counts.set(topic, remaining);
1207
- else {
1208
- counts.delete(topic);
1209
- pendingRemoval.set(topic, setTimeout(() => {
1210
- pendingRemoval.delete(topic);
1211
- if (!counts.has(topic)) unsubscribe(topic);
1212
- }, 0));
1213
- }
1214
- releaseConnection();
1215
- };
1216
- },
1217
- active: () => [...counts.keys()],
1218
- dispose() {
1219
- off();
1220
- for (const timer of pendingRemoval.values()) clearTimeout(timer);
1221
- pendingRemoval.clear();
1222
- }
1782
+ dispose: source.on("message", (message) => update((current) => reduce(current, message), batch === "frame"))
1223
1783
  };
1224
1784
  }
1225
1785
  //#endregion
1226
- export { MessageError, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketResponder, createWebSocketSendRate, createWebSocketState, createWebSocketTopics };
1786
+ export { MessageError, createWebSocket, createWebSocketFeeds, createWebSocketLatency, createWebSocketPresence, createWebSocketRequests, createWebSocketResponder, createWebSocketRuns, createWebSocketSendRate, createWebSocketState, createWebSocketTopics, retryAfter, retryableClose };
1227
1787
 
1228
1788
  //# sourceMappingURL=core.js.map