react-websocket 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.d.ts CHANGED
@@ -100,6 +100,21 @@ interface HeartbeatOptions {
100
100
  */
101
101
  timeout?: number;
102
102
  }
103
+ /** Per-message options for `send()`. */
104
+ interface SendOptions {
105
+ /**
106
+ * Drop the message instead of queueing it when the socket isn't open. For data that
107
+ * is stale by the time a reconnect finishes, such as player input or cursor positions.
108
+ * @default false
109
+ */
110
+ volatile?: boolean;
111
+ /**
112
+ * Keep only the newest queued message with this key. Sending again with the same key
113
+ * while disconnected replaces the earlier one, which moves to the back of the queue.
114
+ * For state where only the latest value matters, such as "my position is now (x, y)".
115
+ */
116
+ key?: string;
117
+ }
103
118
  /** Bounds for the send queue. */
104
119
  interface QueueOptions {
105
120
  /**
@@ -285,10 +300,17 @@ interface WebSocketClient<In = unknown, Out = unknown> {
285
300
  on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;
286
301
  /**
287
302
  * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't
288
- * open, the message is queued and sent on open (see the `queue` option).
303
+ * open, the message is queued and sent on open (see the `queue` option), unless
304
+ * `options.volatile` is set.
289
305
  * @returns `true` if it was sent immediately, `false` if it was queued or dropped.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect
310
+ * game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued
311
+ * ```
290
312
  */
291
- send(message: Out): boolean;
313
+ send(message: Out, options?: SendOptions): boolean;
292
314
  /**
293
315
  * Start connecting, and keep the connection open even when no hook is using the
294
316
  * client, until `close()` is called. Does nothing if already connecting or open.
@@ -335,4 +357,245 @@ interface WebSocketClient<In = unknown, Out = unknown> {
335
357
  */
336
358
  declare function createWebSocket<In = unknown, Out = unknown>(url: UrlSource, options?: WebSocketOptions<In, Out>): WebSocketClient<In, Out>;
337
359
 
338
- export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketOptions, createWebSocket };
360
+ /** Options for {@link createWebSocketLatency}: how pings and pongs look on the wire. */
361
+ interface WebSocketLatencyOptions<In, Out> {
362
+ /** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */
363
+ ping: (id: number) => Out;
364
+ /**
365
+ * Recognize a pong: return the `id` of the ping it answers, and the server's clock
366
+ * (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)
367
+ * for any other message. Without `time`, only the round-trip time is measured.
368
+ */
369
+ pong: (message: In) => {
370
+ id: number | string;
371
+ time?: number;
372
+ } | null | undefined;
373
+ /**
374
+ * How often to ping while open, in ms. The first ping goes out as soon as the socket opens.
375
+ * @default 2000
376
+ */
377
+ interval?: number;
378
+ /**
379
+ * How many recent measurements to keep. `rtt` is their median, and `offset` comes from
380
+ * the one with the lowest round trip, the most accurate estimate.
381
+ * @default 5
382
+ */
383
+ samples?: number;
384
+ }
385
+ /** Latest latency measurements. A new object is created on every change. */
386
+ interface Latency {
387
+ /** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */
388
+ readonly rtt: number | undefined;
389
+ /**
390
+ * Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.
391
+ * `undefined` until a pong carries `time`.
392
+ */
393
+ readonly offset: number | undefined;
394
+ }
395
+ /** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */
396
+ interface WebSocketLatency {
397
+ /** The client being measured. Hooks reading the latency keep it connected. */
398
+ readonly client: WebSocketClient<any, any>;
399
+ /** The current {@link Latency}. Returns the same object until it changes. */
400
+ getSnapshot(): Latency;
401
+ /** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */
402
+ subscribe(listener: () => void): () => void;
403
+ /** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */
404
+ serverNow(): number;
405
+ /** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */
406
+ ping(): void;
407
+ /** Stop pinging and listening. Only needed for instances created dynamically. */
408
+ dispose(): void;
409
+ }
410
+ /**
411
+ * Measure round-trip time and estimate the server's clock, for ping displays, countdowns
412
+ * and timestamps that must agree across players.
413
+ *
414
+ * Pings are sent on an interval while the socket is open, and paused otherwise. Measurements
415
+ * from a previous connection are discarded on reconnect, but the last values stay readable
416
+ * until new ones arrive. It doesn't open the connection by itself.
417
+ *
418
+ * Pongs are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * export const latency = createWebSocketLatency(game, {
423
+ * ping: id => ({ type: 'ping', id }),
424
+ * pong: message => (message.type === 'pong' ? { id: message.id, time: message.time } : undefined)
425
+ * });
426
+ *
427
+ * // In a component:
428
+ * const { rtt } = useWebSocketLatency(latency);
429
+ *
430
+ * // Anywhere: seconds left in a round that ends at the server's `endsAt`.
431
+ * const left = (endsAt - latency.serverNow()) / 1000;
432
+ * ```
433
+ */
434
+ declare function createWebSocketLatency<In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>): WebSocketLatency;
435
+
436
+ /** Options for {@link createWebSocketRequests}: how requests and replies look on the wire. */
437
+ interface WebSocketRequestsOptions<Req, In, Out> {
438
+ /**
439
+ * Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`
440
+ * to type what `request()` accepts.
441
+ */
442
+ send: (message: Req, id: number) => Out;
443
+ /**
444
+ * The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.
445
+ * A string id matches the number it was sent as, so either echo works.
446
+ */
447
+ reply: (message: In) => number | string | null | undefined;
448
+ /**
449
+ * Called on a reply. Return something other than `undefined` to reject the request with
450
+ * it instead of resolving, e.g. for a server-side error reply.
451
+ */
452
+ error?: (message: In) => unknown;
453
+ /**
454
+ * Default time to wait for a reply, in ms. `Infinity` waits forever.
455
+ * @default 10000
456
+ */
457
+ timeout?: number;
458
+ }
459
+ /** Per-call options for {@link WebSocketRequests.request}. */
460
+ interface RequestOptions extends SendOptions {
461
+ /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */
462
+ timeout?: number;
463
+ /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */
464
+ signal?: AbortSignal;
465
+ }
466
+ /** Request/response over one client. Create with {@link createWebSocketRequests}. */
467
+ interface WebSocketRequests<Req, In> {
468
+ /** The client requests are sent on. */
469
+ readonly client: WebSocketClient<any, any>;
470
+ /**
471
+ * Send a request and resolve with its reply. Holds the connection until it settles.
472
+ *
473
+ * Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when
474
+ * aborted, and with an `Error` when the connection it was sent on closes (the reply can
475
+ * no longer arrive) or the client stops. A request made while disconnected is queued like
476
+ * any other message and waits for the connection.
477
+ */
478
+ request(message: Req, options?: RequestOptions): Promise<In>;
479
+ /** Number of requests waiting for a reply. */
480
+ pending(): number;
481
+ /** Reject every pending request and stop listening. Only needed for managers created dynamically. */
482
+ dispose(): void;
483
+ }
484
+ /**
485
+ * Send requests and await their replies, matched by an id the library generates.
486
+ *
487
+ * Replies are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * type Request = { type: 'join'; room: string } | { type: 'leave' };
492
+ *
493
+ * export const rpc = createWebSocketRequests(game, {
494
+ * send: (message: Request, id) => ({ ...message, id }),
495
+ * reply: message => (message.type === 'reply' ? message.id : undefined),
496
+ * error: message => (message.type === 'reply' && message.error ? new Error(message.error) : undefined)
497
+ * });
498
+ *
499
+ * const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });
500
+ * ```
501
+ */
502
+ declare function createWebSocketRequests<Req, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>): WebSocketRequests<Req, In>;
503
+
504
+ /** Options for {@link createWebSocketState}. */
505
+ interface WebSocketStateOptions<S, In> {
506
+ /** The state before any message arrives, and what `reset()` returns to. */
507
+ initialState: S;
508
+ /**
509
+ * Fold one incoming message into the state. Must be pure: return a new object for a
510
+ * change, or the same `state` to ignore the message (subscribers aren't notified).
511
+ */
512
+ reduce: (state: S, message: In) => S;
513
+ }
514
+ /**
515
+ * State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,
516
+ * or with `getState`/`subscribe` outside React.
517
+ *
518
+ * @typeParam S - The state type. Don't use a function type: `setState` treats functions as updaters.
519
+ */
520
+ interface WebSocketState<S> {
521
+ /** The client whose messages this store reduces. Hooks reading the store keep it connected. */
522
+ readonly client: WebSocketClient<any, any>;
523
+ /** The `initialState` it was created with. Used for server rendering and hydration. */
524
+ readonly initialState: S;
525
+ /** The current state. Returns the same object until it changes. */
526
+ getState(): S;
527
+ /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */
528
+ subscribe(listener: () => void): () => void;
529
+ /** Replace the state, or update it from the current one, e.g. for optimistic updates. */
530
+ setState(update: S | ((state: S) => S)): void;
531
+ /** Go back to `initialState`. */
532
+ reset(): void;
533
+ /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */
534
+ dispose(): void;
535
+ }
536
+ /**
537
+ * Keep state derived from a client's messages, like a reducer that runs on every message.
538
+ * It starts listening immediately and keeps the state across reconnects; it doesn't open
539
+ * the connection by itself.
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });
544
+ *
545
+ * export const priceState = createWebSocketState(prices, {
546
+ * initialState: {} as Record<string, number>,
547
+ * reduce: (state, tick) => ({ ...state, [tick.symbol]: tick.price })
548
+ * });
549
+ *
550
+ * // In a component: re-renders only when ACME's price changes.
551
+ * const acme = useWebSocketState(priceState, state => state.ACME);
552
+ * ```
553
+ */
554
+ declare function createWebSocketState<S, In>(client: WebSocketClient<In, any>, options: WebSocketStateOptions<S, NoInfer<In>>): WebSocketState<S>;
555
+
556
+ /** Options for {@link createWebSocketTopics}: the messages your server expects. */
557
+ interface WebSocketTopicsOptions<Out> {
558
+ /** The message that subscribes to `topic`. */
559
+ subscribe: (topic: string) => Out;
560
+ /** The message that unsubscribes from `topic`. Omit it if your server has none. */
561
+ unsubscribe?: (topic: string) => Out;
562
+ }
563
+ /** Reference-counted topic subscriptions on one client. Create with {@link createWebSocketTopics}. */
564
+ interface WebSocketTopics {
565
+ /** The client the subscription messages are sent on. */
566
+ readonly client: WebSocketClient<any, any>;
567
+ /**
568
+ * Hold a subscription to `topic`, and the client's connection, until the returned
569
+ * function is called. The release function is safe to call more than once.
570
+ */
571
+ acquire(topic: string): () => void;
572
+ /** The topics currently held, in the order they were first acquired. */
573
+ active(): string[];
574
+ /** Stop managing subscriptions. Only needed for managers created dynamically. */
575
+ dispose(): void;
576
+ }
577
+ /**
578
+ * Manage topic subscriptions that survive reconnects.
579
+ *
580
+ * - The first holder of a topic sends `subscribe(topic)`. Later holders share it.
581
+ * - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode
582
+ * and route transitions don't churn subscriptions.
583
+ * - After every reconnect, every held topic is subscribed again on the new socket, before
584
+ * messages queued while disconnected are sent.
585
+ *
586
+ * Subscriptions are only sent while the socket is open; nothing is queued for them.
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * export const topics = createWebSocketTopics(live, {
591
+ * subscribe: topic => ({ type: 'subscribe', topic }),
592
+ * unsubscribe: topic => ({ type: 'unsubscribe', topic })
593
+ * });
594
+ *
595
+ * // In a component:
596
+ * useWebSocketTopic(topics, `prices:${symbol}`);
597
+ * ```
598
+ */
599
+ declare function createWebSocketTopics<const Out>(client: WebSocketClient<any, Out>, options: WebSocketTopicsOptions<NoInfer<Out>>): WebSocketTopics;
600
+
601
+ export { type CloseInfo, type EventMap, type HeartbeatOptions, type Latency, MessageError, type QueueOptions, type ReconnectOptions, type RequestOptions, type SendData, type SendOptions, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type WebSocketClient, type WebSocketLatency, type WebSocketLatencyOptions, type WebSocketOptions, type WebSocketRequests, type WebSocketRequestsOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketState, createWebSocketTopics };
package/dist/core.js CHANGED
@@ -220,7 +220,7 @@ function createClient(url, options = {}) {
220
220
  if (ws !== socket) return;
221
221
  const queued = pending;
222
222
  pending = [];
223
- for (const data of queued) socket.send(data);
223
+ for (const { data } of queued) socket.send(data);
224
224
  };
225
225
  socket.onmessage = (event) => receive(event, socket);
226
226
  socket.onerror = (event) => emit("error", event);
@@ -316,13 +316,13 @@ function createClient(url, options = {}) {
316
316
  set({ lastMessage: message });
317
317
  emit("message", message, event);
318
318
  }
319
- function send(message) {
319
+ function send(message, sendOptions) {
320
320
  const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
321
321
  if (ws && ws.readyState === OPEN) {
322
322
  ws.send(data);
323
323
  return true;
324
324
  }
325
- if (queueLimit === 0) {
325
+ if (queueLimit === 0 || sendOptions?.volatile) {
326
326
  log("dropped message: socket is not open");
327
327
  return false;
328
328
  }
@@ -330,7 +330,9 @@ function createClient(url, options = {}) {
330
330
  log("dropped message: no WebSocket implementation available");
331
331
  return false;
332
332
  }
333
- pending.push(data);
333
+ const key = sendOptions?.key;
334
+ if (key !== void 0) pending = pending.filter((entry) => entry.key !== key);
335
+ pending.push({ data, key });
334
336
  if (pending.length > queueLimit) {
335
337
  pending.shift();
336
338
  log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
@@ -462,8 +464,288 @@ function createClient(url, options = {}) {
462
464
  function createWebSocket(url, options) {
463
465
  return createClient(url, options);
464
466
  }
467
+
468
+ // src/latency.ts
469
+ var EMPTY = Object.freeze({ rtt: void 0, offset: void 0 });
470
+ function median(values) {
471
+ const sorted = [...values].sort((a, b) => a - b);
472
+ const middle = sorted.length >> 1;
473
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
474
+ }
475
+ function createWebSocketLatency(client, options) {
476
+ const interval = options.interval ?? 2e3;
477
+ const keep = Math.max(1, options.samples ?? 5);
478
+ const listeners = /* @__PURE__ */ new Set();
479
+ let snapshot = EMPTY;
480
+ const inFlight = /* @__PURE__ */ new Map();
481
+ let samples = [];
482
+ let nextId = 0;
483
+ let timer;
484
+ function ping() {
485
+ if (client.getSnapshot().status !== "open") return;
486
+ const id = ++nextId;
487
+ inFlight.set(String(id), performance.now());
488
+ if (inFlight.size > keep * 2) inFlight.delete(inFlight.keys().next().value);
489
+ client.send(options.ping(id), { volatile: true });
490
+ }
491
+ function start() {
492
+ stop();
493
+ inFlight.clear();
494
+ samples = [];
495
+ ping();
496
+ timer = setInterval(ping, interval);
497
+ }
498
+ function stop() {
499
+ clearInterval(timer);
500
+ timer = void 0;
501
+ }
502
+ function measure(id, serverTime) {
503
+ const sentAt = inFlight.get(id);
504
+ if (sentAt === void 0) return;
505
+ inFlight.delete(id);
506
+ const rtt = performance.now() - sentAt;
507
+ const offset = serverTime === void 0 ? void 0 : serverTime + rtt / 2 - Date.now();
508
+ samples.push({ rtt, offset });
509
+ if (samples.length > keep) samples.shift();
510
+ let best;
511
+ for (const sample of samples) {
512
+ if (sample.offset !== void 0 && (!best || sample.rtt < best.rtt)) best = sample;
513
+ }
514
+ snapshot = {
515
+ rtt: median(samples.map((sample) => sample.rtt)),
516
+ offset: best ? best.offset : snapshot.offset
517
+ };
518
+ for (const listener of [...listeners]) listener();
519
+ }
520
+ const offs = [
521
+ client.on("open", start),
522
+ client.on("message", (message) => {
523
+ const pong = options.pong(message);
524
+ if (pong != null) measure(String(pong.id), pong.time);
525
+ }),
526
+ client.subscribe(() => {
527
+ if (client.getSnapshot().status !== "open") stop();
528
+ })
529
+ ];
530
+ if (client.getSnapshot().status === "open") start();
531
+ return {
532
+ client,
533
+ getSnapshot: () => snapshot,
534
+ subscribe(listener) {
535
+ listeners.add(listener);
536
+ return () => {
537
+ listeners.delete(listener);
538
+ };
539
+ },
540
+ serverNow: () => Date.now() + (snapshot.offset ?? 0),
541
+ ping,
542
+ dispose() {
543
+ stop();
544
+ offs.forEach((off) => off());
545
+ }
546
+ };
547
+ }
548
+
549
+ // src/requests.ts
550
+ function createWebSocketRequests(client, options) {
551
+ const waiting = /* @__PURE__ */ new Map();
552
+ let nextId = 0;
553
+ function rejectWhere(test, reason) {
554
+ for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
555
+ }
556
+ const offs = [
557
+ client.on("message", (message) => {
558
+ const id = options.reply(message);
559
+ const entry = id == null ? void 0 : waiting.get(String(id));
560
+ if (!entry) return;
561
+ const error = options.error?.(message);
562
+ if (error === void 0) entry.resolve(message);
563
+ else entry.reject(error);
564
+ }),
565
+ // `open` listeners run before the client flushes its queue: queued requests go out on this socket.
566
+ client.on("open", () => {
567
+ for (const entry of waiting.values()) entry.socket ??= client.socket;
568
+ }),
569
+ client.subscribe(() => {
570
+ const { status } = client.getSnapshot();
571
+ if (status === "closed" || status === "idle") {
572
+ rejectWhere(
573
+ () => true,
574
+ () => new Error("WebSocket was closed before the request was answered")
575
+ );
576
+ } else {
577
+ rejectWhere(
578
+ (entry) => entry.socket !== null && entry.socket !== client.socket,
579
+ () => new Error("WebSocket connection was lost before the request was answered")
580
+ );
581
+ }
582
+ })
583
+ ];
584
+ return {
585
+ client,
586
+ request(message, requestOptions = {}) {
587
+ const { signal } = requestOptions;
588
+ if (signal?.aborted) return Promise.reject(signal.reason);
589
+ const id = ++nextId;
590
+ const key = String(id);
591
+ const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
592
+ return new Promise((resolve, reject) => {
593
+ let timer;
594
+ const release = client.acquire();
595
+ const onAbort = () => entry.reject(signal.reason);
596
+ function settle() {
597
+ waiting.delete(key);
598
+ clearTimeout(timer);
599
+ signal?.removeEventListener("abort", onAbort);
600
+ release();
601
+ }
602
+ const entry = {
603
+ socket: null,
604
+ resolve(reply) {
605
+ settle();
606
+ resolve(reply);
607
+ },
608
+ reject(reason) {
609
+ settle();
610
+ reject(reason);
611
+ }
612
+ };
613
+ waiting.set(key, entry);
614
+ signal?.addEventListener("abort", onAbort, { once: true });
615
+ if (timeout > 0 && timeout < Infinity) {
616
+ timer = setTimeout(
617
+ () => entry.reject(
618
+ new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")
619
+ ),
620
+ timeout
621
+ );
622
+ }
623
+ const { status } = client.getSnapshot();
624
+ if (status === "closed" || status === "idle") {
625
+ entry.reject(new Error(`WebSocket is ${status}: request not sent`));
626
+ return;
627
+ }
628
+ let sent;
629
+ try {
630
+ sent = client.send(options.send(message, id), requestOptions);
631
+ } catch (error) {
632
+ entry.reject(error);
633
+ return;
634
+ }
635
+ if (sent) entry.socket = client.socket;
636
+ else if (requestOptions.volatile)
637
+ entry.reject(new Error("WebSocket is not open: request dropped"));
638
+ });
639
+ },
640
+ pending: () => waiting.size,
641
+ dispose() {
642
+ offs.forEach((off) => off());
643
+ rejectWhere(
644
+ () => true,
645
+ () => new Error("Requests were disposed")
646
+ );
647
+ }
648
+ };
649
+ }
650
+
651
+ // src/state.ts
652
+ function createWebSocketState(client, options) {
653
+ const { initialState, reduce } = options;
654
+ let state = initialState;
655
+ const listeners = /* @__PURE__ */ new Set();
656
+ function setState(update) {
657
+ const next = typeof update === "function" ? update(state) : update;
658
+ if (Object.is(next, state)) return;
659
+ state = next;
660
+ for (const listener of [...listeners]) listener();
661
+ }
662
+ const off = client.on("message", (message) => setState((current) => reduce(current, message)));
663
+ return {
664
+ client,
665
+ initialState,
666
+ getState: () => state,
667
+ subscribe(listener) {
668
+ listeners.add(listener);
669
+ return () => {
670
+ listeners.delete(listener);
671
+ };
672
+ },
673
+ setState,
674
+ reset: () => setState(initialState),
675
+ dispose: off
676
+ };
677
+ }
678
+
679
+ // src/topics.ts
680
+ function createWebSocketTopics(client, options) {
681
+ const counts = /* @__PURE__ */ new Map();
682
+ const pendingRemoval = /* @__PURE__ */ new Map();
683
+ let sentOn = null;
684
+ let sent = /* @__PURE__ */ new Set();
685
+ function isOpen() {
686
+ return client.getSnapshot().status === "open" && client.socket !== null;
687
+ }
688
+ function subscribe(topic) {
689
+ if (!isOpen()) return;
690
+ if (client.socket !== sentOn) {
691
+ sentOn = client.socket;
692
+ sent = /* @__PURE__ */ new Set();
693
+ }
694
+ if (sent.has(topic)) return;
695
+ sent.add(topic);
696
+ client.send(options.subscribe(topic));
697
+ }
698
+ function unsubscribe(topic) {
699
+ if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;
700
+ if (options.unsubscribe) client.send(options.unsubscribe(topic));
701
+ }
702
+ const off = client.on("open", () => {
703
+ sentOn = client.socket;
704
+ sent = /* @__PURE__ */ new Set();
705
+ for (const topic of counts.keys()) subscribe(topic);
706
+ });
707
+ return {
708
+ client,
709
+ acquire(topic) {
710
+ const releaseConnection = client.acquire();
711
+ clearTimeout(pendingRemoval.get(topic));
712
+ pendingRemoval.delete(topic);
713
+ counts.set(topic, (counts.get(topic) ?? 0) + 1);
714
+ subscribe(topic);
715
+ let released = false;
716
+ return () => {
717
+ if (released) return;
718
+ released = true;
719
+ const remaining = (counts.get(topic) ?? 1) - 1;
720
+ if (remaining > 0) {
721
+ counts.set(topic, remaining);
722
+ } else {
723
+ counts.delete(topic);
724
+ pendingRemoval.set(
725
+ topic,
726
+ setTimeout(() => {
727
+ pendingRemoval.delete(topic);
728
+ if (!counts.has(topic)) unsubscribe(topic);
729
+ }, 0)
730
+ );
731
+ }
732
+ releaseConnection();
733
+ };
734
+ },
735
+ active: () => [...counts.keys()],
736
+ dispose() {
737
+ off();
738
+ for (const timer of pendingRemoval.values()) clearTimeout(timer);
739
+ pendingRemoval.clear();
740
+ }
741
+ };
742
+ }
465
743
  export {
466
744
  MessageError,
467
- createWebSocket
745
+ createWebSocket,
746
+ createWebSocketLatency,
747
+ createWebSocketRequests,
748
+ createWebSocketState,
749
+ createWebSocketTopics
468
750
  };
469
751
  //# sourceMappingURL=core.js.map