react-websocket 3.6.0 → 4.18.0

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