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