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