react-websocket 3.1.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/index.d.cts 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,6 +357,150 @@ 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
 
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
+
338
504
  /** Options for {@link createWebSocketState}. */
339
505
  interface WebSocketStateOptions<S, In> {
340
506
  /** The state before any message arrives, and what `reset()` returns to. */
@@ -457,8 +623,11 @@ interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSoc
457
623
  }
458
624
  /** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */
459
625
  interface UseWebSocketResult<In, Out> extends Snapshot<In> {
460
- /** Send now, or queue until open. Returns `true` if sent immediately. Stable identity. */
461
- send: (message: Out) => boolean;
626
+ /**
627
+ * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).
628
+ * Returns `true` if sent immediately. Stable identity.
629
+ */
630
+ send: (message: Out, options?: SendOptions) => boolean;
462
631
  /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
463
632
  close: (code?: number, reason?: string) => void;
464
633
  /** Drop the socket and connect again right away. Stable identity. */
@@ -537,5 +706,17 @@ declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (st
537
706
  * ```
538
707
  */
539
708
  declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, topic: string | readonly string[] | null | undefined): void;
709
+ /**
710
+ * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.
711
+ * Re-renders when a measurement changes, about once per ping interval. Keeps the connection
712
+ * open while mounted. On the server, and during hydration, both values are `undefined`.
713
+ *
714
+ * @example
715
+ * ```tsx
716
+ * const { rtt } = useWebSocketLatency(latency);
717
+ * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;
718
+ * ```
719
+ */
720
+ declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
540
721
 
541
- export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketClient, type WebSocketHandlers, type WebSocketOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketState, createWebSocketTopics, useWebSocket, useWebSocketEvent, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
722
+ 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 UseWebSocketOptions, type UseWebSocketResult, type WebSocketClient, type WebSocketHandlers, type WebSocketLatency, type WebSocketLatencyOptions, type WebSocketOptions, type WebSocketRequests, type WebSocketRequestsOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketState, createWebSocketTopics, useWebSocket, useWebSocketEvent, useWebSocketLatency, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
package/dist/index.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,6 +357,150 @@ 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
 
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
+
338
504
  /** Options for {@link createWebSocketState}. */
339
505
  interface WebSocketStateOptions<S, In> {
340
506
  /** The state before any message arrives, and what `reset()` returns to. */
@@ -457,8 +623,11 @@ interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSoc
457
623
  }
458
624
  /** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */
459
625
  interface UseWebSocketResult<In, Out> extends Snapshot<In> {
460
- /** Send now, or queue until open. Returns `true` if sent immediately. Stable identity. */
461
- send: (message: Out) => boolean;
626
+ /**
627
+ * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).
628
+ * Returns `true` if sent immediately. Stable identity.
629
+ */
630
+ send: (message: Out, options?: SendOptions) => boolean;
462
631
  /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
463
632
  close: (code?: number, reason?: string) => void;
464
633
  /** Drop the socket and connect again right away. Stable identity. */
@@ -537,5 +706,17 @@ declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (st
537
706
  * ```
538
707
  */
539
708
  declare function useWebSocketTopic(topics: WebSocketTopics | null | undefined, topic: string | readonly string[] | null | undefined): void;
709
+ /**
710
+ * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.
711
+ * Re-renders when a measurement changes, about once per ping interval. Keeps the connection
712
+ * open while mounted. On the server, and during hydration, both values are `undefined`.
713
+ *
714
+ * @example
715
+ * ```tsx
716
+ * const { rtt } = useWebSocketLatency(latency);
717
+ * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;
718
+ * ```
719
+ */
720
+ declare function useWebSocketLatency(latency: WebSocketLatency): Latency;
540
721
 
541
- export { type CloseInfo, type EventMap, type HeartbeatOptions, MessageError, type QueueOptions, type ReconnectOptions, type SendData, type Snapshot, type StandardSchemaV1, type Status, type UrlSource, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketClient, type WebSocketHandlers, type WebSocketOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketState, createWebSocketTopics, useWebSocket, useWebSocketEvent, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
722
+ 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 UseWebSocketOptions, type UseWebSocketResult, type WebSocketClient, type WebSocketHandlers, type WebSocketLatency, type WebSocketLatencyOptions, type WebSocketOptions, type WebSocketRequests, type WebSocketRequestsOptions, type WebSocketState, type WebSocketStateOptions, type WebSocketTopics, type WebSocketTopicsOptions, createWebSocket, createWebSocketLatency, createWebSocketRequests, createWebSocketState, createWebSocketTopics, useWebSocket, useWebSocketEvent, useWebSocketLatency, useWebSocketState, useWebSocketStatus, useWebSocketTopic };
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import { createWebSocket } from "./core.js";
9
9
  var IDLE = Object.freeze({ status: "idle", lastMessage: void 0, attempt: 0 });
10
10
  var idleSnapshot = () => IDLE;
11
11
  var idleStatus = () => "idle";
12
+ var NO_LATENCY = Object.freeze({ rtt: void 0, offset: void 0 });
13
+ var noLatency = () => NO_LATENCY;
12
14
  var noopSubscribe = () => () => {
13
15
  };
14
16
  var noopSend = () => false;
@@ -124,9 +126,15 @@ function useWebSocketTopic(topics, topic) {
124
126
  return () => releases.forEach((release) => release());
125
127
  }, [topics, key]);
126
128
  }
129
+ function useWebSocketLatency(latency) {
130
+ const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);
131
+ useHold(latency.client, true);
132
+ return snapshot;
133
+ }
127
134
  export {
128
135
  useWebSocket,
129
136
  useWebSocketEvent,
137
+ useWebSocketLatency,
130
138
  useWebSocketState,
131
139
  useWebSocketStatus,
132
140
  useWebSocketTopic
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/react.ts"],"sourcesContent":["export * from './core';\nexport {\n useWebSocket,\n useWebSocketEvent,\n useWebSocketState,\n useWebSocketStatus,\n useWebSocketTopic\n} from './react';\nexport type { UseWebSocketOptions, UseWebSocketResult, WebSocketHandlers } from './react';\n","import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n// Import the client through the `core` entry: the build keeps it external, so both entries\n// share one copy of it (and of `MessageError`).\nimport { createWebSocket } from './core';\nimport type { WebSocketState } from './state';\nimport type { WebSocketTopics } from './topics';\nimport type {\n CloseInfo,\n EventMap,\n Snapshot,\n Status,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n\n/**\n * Per-component event handlers. They always see the component's latest props and\n * state, and changing them never reconnects.\n */\nexport interface WebSocketHandlers<In> {\n /** The socket opened (initially or after a reconnect). */\n onOpen?: (event: Event) => void;\n /** A message arrived and passed `parse`/`json` and `schema`. */\n onMessage?: (message: In, event: MessageEvent) => void;\n /** A socket closed. Called whether or not it will be retried. */\n onClose?: (close: CloseInfo) => void;\n /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */\n onError?: (error: Event | Error) => void;\n /**\n * `false` releases this component's hold on the connection and stops its handlers.\n * The connection closes once nothing else holds it.\n * @default true\n */\n enabled?: boolean;\n}\n\n/** Options for `useWebSocket(url, options)`: connection options plus handlers. */\nexport interface UseWebSocketOptions<In, Out>\n extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}\n\n/** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */\nexport interface UseWebSocketResult<In, Out> extends Snapshot<In> {\n /** Send now, or queue until open. Returns `true` if sent immediately. Stable identity. */\n send: (message: Out) => boolean;\n /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */\n close: (code?: number, reason?: string) => void;\n /** Drop the socket and connect again right away. Stable identity. */\n reconnect: () => void;\n /** The underlying client, or `null` when the URL is `null`/`undefined`. */\n client: WebSocketClient<In, Out> | null;\n}\n\nconst IDLE: Snapshot<never> = Object.freeze({ status: 'idle', lastMessage: undefined, attempt: 0 });\nconst idleSnapshot = () => IDLE;\nconst idleStatus = (): Status => 'idle';\nconst noopSubscribe = () => () => {};\nconst noopSend = () => false;\nconst noop = () => {};\n\n// Same key as `ON_TEARDOWN` in ./client, which this entry must not import directly.\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\n// Hooks given the same URL (and protocols) share one connection.\nconst registry = new Map<string, WebSocketClient<any, any>>();\n\nfunction sharedClient<In, Out>(url: string, options: WebSocketOptions<In, Out>) {\n // Don't let server renders accumulate clients that can never be torn down.\n if (typeof window === 'undefined') return createWebSocket(url, options);\n const key = `${url}\\u0000${JSON.stringify(options.protocols ?? null)}`;\n let client = registry.get(key) as WebSocketClient<In, Out> | undefined;\n if (!client) {\n const created: WebSocketClient<In, Out> = createWebSocket(url, {\n ...options,\n [ON_TEARDOWN]: () => {\n if (registry.get(key) === created) registry.delete(key);\n }\n } as WebSocketOptions<In, Out>);\n registry.set(key, created);\n client = created;\n }\n return client;\n}\n\n/** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */\nfunction useLatest<T>(value: T) {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\nfunction useHold(client: WebSocketClient<any, any> | null, enabled: boolean) {\n useEffect(() => {\n if (client && enabled) return client.acquire();\n }, [client, enabled]);\n}\n\n/**\n * Connect to a WebSocket and re-render with its status and latest message.\n *\n * - With a URL, every component using the same URL (and `protocols`) shares one\n * connection. Its options come from whichever component created it first.\n * - With a client from `createWebSocket()`, options are the client's; only handlers\n * and `enabled` apply here.\n * - With `null` or `undefined`, nothing connects and the status stays `idle`.\n *\n * The connection opens on mount and closes one tick after the last component using it\n * unmounts, so StrictMode and `<Activity>` don't open duplicate sockets.\n *\n * Re-renders on status changes and on every message. For high-frequency streams use\n * `useWebSocketEvent` and `useWebSocketStatus` instead.\n *\n * @example\n * ```tsx\n * const { status, lastMessage, send } = useWebSocket<Price, Order>('wss://api.example.com/prices', {\n * json: true,\n * onMessage: price => console.log(price)\n * });\n * ```\n */\nexport function useWebSocket<In = unknown, Out = unknown>(\n url: string | null | undefined,\n options?: UseWebSocketOptions<In, Out>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n client: WebSocketClient<In, Out> | null | undefined,\n options?: WebSocketHandlers<In>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n target: string | WebSocketClient<In, Out> | null | undefined,\n options: UseWebSocketOptions<In, Out> = {}\n): UseWebSocketResult<In, Out> {\n const client =\n target == null ? null : typeof target === 'string' ? sharedClient(target, options) : target;\n const enabled = options.enabled ?? true;\n\n const snapshot = useSyncExternalStore<Snapshot<In>>(\n client ? client.subscribe : noopSubscribe,\n client ? client.getSnapshot : idleSnapshot,\n idleSnapshot\n );\n\n const handlers = useLatest(options);\n useEffect(() => {\n if (!client || !enabled) return;\n const offs = [\n client.on('open', event => handlers.current.onOpen?.(event)),\n client.on('message', (message, event) => handlers.current.onMessage?.(message, event)),\n client.on('close', info => handlers.current.onClose?.(info)),\n client.on('error', error => handlers.current.onError?.(error))\n ];\n return () => offs.forEach(off => off());\n }, [client, enabled, handlers]);\n useHold(client, enabled);\n\n return {\n ...snapshot,\n send: client ? client.send : noopSend,\n close: client ? client.close : noop,\n reconnect: client ? client.reconnect : noop,\n client\n };\n}\n\n/**\n * Listen to a client's events without re-rendering the component. The listener always\n * sees the latest props and state. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));\n * ```\n */\nexport function useWebSocketEvent<In, K extends keyof EventMap<In>>(\n client: WebSocketClient<In, any> | null | undefined,\n type: K,\n listener: EventMap<In>[K]\n): void {\n const latest = useLatest(listener);\n useEffect(() => {\n if (!client) return;\n const off = client.on(type, ((...args: unknown[]) =>\n (latest.current as (...a: unknown[]) => void)(...args)) as EventMap<In>[K]);\n const release = client.acquire();\n return () => {\n off();\n release();\n };\n }, [client, type, latest]);\n}\n\n/**\n * The connection status of a client. Re-renders only when the status changes, not on\n * messages. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'\n * ```\n */\nexport function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status {\n const status = useSyncExternalStore(\n client ? client.subscribe : noopSubscribe,\n client ? () => client.getSnapshot().status : idleStatus,\n idleStatus\n );\n useHold(client ?? null, true);\n return status;\n}\n\nconst identity = <T>(value: T) => value;\n\n/**\n * Read state built from messages by `createWebSocketState`. Re-renders only when the\n * selected value changes (by `isEqual`, default `Object.is`), however often messages\n * arrive. Keeps the connection open while mounted.\n *\n * On the server, and during hydration, it reads from `initialState`.\n *\n * @example\n * ```tsx\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function useWebSocketState<S>(store: WebSocketState<S>): S;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T,\n isEqual?: (a: T, b: T) => boolean\n): T;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T = identity as (state: S) => T,\n isEqual: (a: T, b: T) => boolean = Object.is\n): T {\n const [getSelection, getServerSelection] = useMemo(() => {\n // Return the previous selection while the state is unchanged or the result is equal,\n // so inline selectors that build objects don't re-render (or loop) on every message.\n const memoized = (read: () => S) => {\n let memo: { state: S; selection: T } | undefined;\n return () => {\n const state = read();\n if (memo && Object.is(memo.state, state)) return memo.selection;\n const selection = selector(state);\n if (memo && isEqual(memo.selection, selection)) {\n memo = { state, selection: memo.selection };\n return memo.selection;\n }\n memo = { state, selection };\n return selection;\n };\n };\n return [memoized(store.getState), memoized(() => store.initialState)] as const;\n }, [store, selector, isEqual]);\n\n const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);\n useHold(store.client, true);\n return selection;\n}\n\n/**\n * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`\n * manager. Components holding the same topic share one subscription, and it is sent again\n * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.\n *\n * @example\n * ```tsx\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));\n * ```\n */\nexport function useWebSocketTopic(\n topics: WebSocketTopics | null | undefined,\n topic: string | readonly string[] | null | undefined\n): void {\n // A stable key, so a new array with the same topics doesn't resubscribe.\n const key = topic == null ? null : JSON.stringify(typeof topic === 'string' ? [topic] : topic);\n useEffect(() => {\n if (!topics || key === null) return;\n const releases = (JSON.parse(key) as string[]).map(t => topics.acquire(t));\n return () => releases.forEach(release => release());\n }, [topics, key]);\n}\n"],"mappings":"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","names":["selection"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/react.ts"],"sourcesContent":["export * from './core';\nexport {\n useWebSocket,\n useWebSocketEvent,\n useWebSocketLatency,\n useWebSocketState,\n useWebSocketStatus,\n useWebSocketTopic\n} from './react';\nexport type { UseWebSocketOptions, UseWebSocketResult, WebSocketHandlers } from './react';\n","import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from 'react';\n// Import the client through the `core` entry: the build keeps it external, so both entries\n// share one copy of it (and of `MessageError`).\nimport { createWebSocket } from './core';\nimport type { Latency, WebSocketLatency } from './latency';\nimport type { WebSocketState } from './state';\nimport type { WebSocketTopics } from './topics';\nimport type {\n CloseInfo,\n EventMap,\n SendOptions,\n Snapshot,\n Status,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n\n/**\n * Per-component event handlers. They always see the component's latest props and\n * state, and changing them never reconnects.\n */\nexport interface WebSocketHandlers<In> {\n /** The socket opened (initially or after a reconnect). */\n onOpen?: (event: Event) => void;\n /** A message arrived and passed `parse`/`json` and `schema`. */\n onMessage?: (message: In, event: MessageEvent) => void;\n /** A socket closed. Called whether or not it will be retried. */\n onClose?: (close: CloseInfo) => void;\n /** A socket error `Event`, a `MessageError`, or an `Error` from the constructor or `url` function. */\n onError?: (error: Event | Error) => void;\n /**\n * `false` releases this component's hold on the connection and stops its handlers.\n * The connection closes once nothing else holds it.\n * @default true\n */\n enabled?: boolean;\n}\n\n/** Options for `useWebSocket(url, options)`: connection options plus handlers. */\nexport interface UseWebSocketOptions<In, Out>\n extends WebSocketOptions<In, Out>, WebSocketHandlers<In> {}\n\n/** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */\nexport interface UseWebSocketResult<In, Out> extends Snapshot<In> {\n /**\n * Send now, or queue until open (see {@link SendOptions} for volatile and keyed sends).\n * Returns `true` if sent immediately. Stable identity.\n */\n send: (message: Out, options?: SendOptions) => boolean;\n /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */\n close: (code?: number, reason?: string) => void;\n /** Drop the socket and connect again right away. Stable identity. */\n reconnect: () => void;\n /** The underlying client, or `null` when the URL is `null`/`undefined`. */\n client: WebSocketClient<In, Out> | null;\n}\n\nconst IDLE: Snapshot<never> = Object.freeze({ status: 'idle', lastMessage: undefined, attempt: 0 });\nconst idleSnapshot = () => IDLE;\nconst idleStatus = (): Status => 'idle';\nconst NO_LATENCY: Latency = Object.freeze({ rtt: undefined, offset: undefined });\nconst noLatency = () => NO_LATENCY;\nconst noopSubscribe = () => () => {};\nconst noopSend = () => false;\nconst noop = () => {};\n\n// Same key as `ON_TEARDOWN` in ./client, which this entry must not import directly.\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\n// Hooks given the same URL (and protocols) share one connection.\nconst registry = new Map<string, WebSocketClient<any, any>>();\n\nfunction sharedClient<In, Out>(url: string, options: WebSocketOptions<In, Out>) {\n // Don't let server renders accumulate clients that can never be torn down.\n if (typeof window === 'undefined') return createWebSocket(url, options);\n const key = `${url}\\u0000${JSON.stringify(options.protocols ?? null)}`;\n let client = registry.get(key) as WebSocketClient<In, Out> | undefined;\n if (!client) {\n const created: WebSocketClient<In, Out> = createWebSocket(url, {\n ...options,\n [ON_TEARDOWN]: () => {\n if (registry.get(key) === created) registry.delete(key);\n }\n } as WebSocketOptions<In, Out>);\n registry.set(key, created);\n client = created;\n }\n return client;\n}\n\n/** Always-current value for use inside effects and event listeners (a `useEffectEvent` stand-in). */\nfunction useLatest<T>(value: T) {\n const ref = useRef(value);\n useInsertionEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\nfunction useHold(client: WebSocketClient<any, any> | null, enabled: boolean) {\n useEffect(() => {\n if (client && enabled) return client.acquire();\n }, [client, enabled]);\n}\n\n/**\n * Connect to a WebSocket and re-render with its status and latest message.\n *\n * - With a URL, every component using the same URL (and `protocols`) shares one\n * connection. Its options come from whichever component created it first.\n * - With a client from `createWebSocket()`, options are the client's; only handlers\n * and `enabled` apply here.\n * - With `null` or `undefined`, nothing connects and the status stays `idle`.\n *\n * The connection opens on mount and closes one tick after the last component using it\n * unmounts, so StrictMode and `<Activity>` don't open duplicate sockets.\n *\n * Re-renders on status changes and on every message. For high-frequency streams use\n * `useWebSocketEvent` and `useWebSocketStatus` instead.\n *\n * @example\n * ```tsx\n * const { status, lastMessage, send } = useWebSocket<Price, Order>('wss://api.example.com/prices', {\n * json: true,\n * onMessage: price => console.log(price)\n * });\n * ```\n */\nexport function useWebSocket<In = unknown, Out = unknown>(\n url: string | null | undefined,\n options?: UseWebSocketOptions<In, Out>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n client: WebSocketClient<In, Out> | null | undefined,\n options?: WebSocketHandlers<In>\n): UseWebSocketResult<In, Out>;\nexport function useWebSocket<In, Out>(\n target: string | WebSocketClient<In, Out> | null | undefined,\n options: UseWebSocketOptions<In, Out> = {}\n): UseWebSocketResult<In, Out> {\n const client =\n target == null ? null : typeof target === 'string' ? sharedClient(target, options) : target;\n const enabled = options.enabled ?? true;\n\n const snapshot = useSyncExternalStore<Snapshot<In>>(\n client ? client.subscribe : noopSubscribe,\n client ? client.getSnapshot : idleSnapshot,\n idleSnapshot\n );\n\n const handlers = useLatest(options);\n useEffect(() => {\n if (!client || !enabled) return;\n const offs = [\n client.on('open', event => handlers.current.onOpen?.(event)),\n client.on('message', (message, event) => handlers.current.onMessage?.(message, event)),\n client.on('close', info => handlers.current.onClose?.(info)),\n client.on('error', error => handlers.current.onError?.(error))\n ];\n return () => offs.forEach(off => off());\n }, [client, enabled, handlers]);\n useHold(client, enabled);\n\n return {\n ...snapshot,\n send: client ? client.send : noopSend,\n close: client ? client.close : noop,\n reconnect: client ? client.reconnect : noop,\n client\n };\n}\n\n/**\n * Listen to a client's events without re-rendering the component. The listener always\n * sees the latest props and state. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * useWebSocketEvent(chat, 'message', message => setMessages(list => [...list, message]));\n * ```\n */\nexport function useWebSocketEvent<In, K extends keyof EventMap<In>>(\n client: WebSocketClient<In, any> | null | undefined,\n type: K,\n listener: EventMap<In>[K]\n): void {\n const latest = useLatest(listener);\n useEffect(() => {\n if (!client) return;\n const off = client.on(type, ((...args: unknown[]) =>\n (latest.current as (...a: unknown[]) => void)(...args)) as EventMap<In>[K]);\n const release = client.acquire();\n return () => {\n off();\n release();\n };\n }, [client, type, latest]);\n}\n\n/**\n * The connection status of a client. Re-renders only when the status changes, not on\n * messages. Keeps the connection open while mounted.\n *\n * @example\n * ```tsx\n * const status = useWebSocketStatus(chat); // 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'\n * ```\n */\nexport function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status {\n const status = useSyncExternalStore(\n client ? client.subscribe : noopSubscribe,\n client ? () => client.getSnapshot().status : idleStatus,\n idleStatus\n );\n useHold(client ?? null, true);\n return status;\n}\n\nconst identity = <T>(value: T) => value;\n\n/**\n * Read state built from messages by `createWebSocketState`. Re-renders only when the\n * selected value changes (by `isEqual`, default `Object.is`), however often messages\n * arrive. Keeps the connection open while mounted.\n *\n * On the server, and during hydration, it reads from `initialState`.\n *\n * @example\n * ```tsx\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function useWebSocketState<S>(store: WebSocketState<S>): S;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T,\n isEqual?: (a: T, b: T) => boolean\n): T;\nexport function useWebSocketState<S, T>(\n store: WebSocketState<S>,\n selector: (state: S) => T = identity as (state: S) => T,\n isEqual: (a: T, b: T) => boolean = Object.is\n): T {\n const [getSelection, getServerSelection] = useMemo(() => {\n // Return the previous selection while the state is unchanged or the result is equal,\n // so inline selectors that build objects don't re-render (or loop) on every message.\n const memoized = (read: () => S) => {\n let memo: { state: S; selection: T } | undefined;\n return () => {\n const state = read();\n if (memo && Object.is(memo.state, state)) return memo.selection;\n const selection = selector(state);\n if (memo && isEqual(memo.selection, selection)) {\n memo = { state, selection: memo.selection };\n return memo.selection;\n }\n memo = { state, selection };\n return selection;\n };\n };\n return [memoized(store.getState), memoized(() => store.initialState)] as const;\n }, [store, selector, isEqual]);\n\n const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);\n useHold(store.client, true);\n return selection;\n}\n\n/**\n * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`\n * manager. Components holding the same topic share one subscription, and it is sent again\n * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.\n *\n * @example\n * ```tsx\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));\n * ```\n */\nexport function useWebSocketTopic(\n topics: WebSocketTopics | null | undefined,\n topic: string | readonly string[] | null | undefined\n): void {\n // A stable key, so a new array with the same topics doesn't resubscribe.\n const key = topic == null ? null : JSON.stringify(typeof topic === 'string' ? [topic] : topic);\n useEffect(() => {\n if (!topics || key === null) return;\n const releases = (JSON.parse(key) as string[]).map(t => topics.acquire(t));\n return () => releases.forEach(release => release());\n }, [topics, key]);\n}\n\n/**\n * Read round-trip time and server clock offset from a `createWebSocketLatency` instance.\n * Re-renders when a measurement changes, about once per ping interval. Keeps the connection\n * open while mounted. On the server, and during hydration, both values are `undefined`.\n *\n * @example\n * ```tsx\n * const { rtt } = useWebSocketLatency(latency);\n * return <span>{rtt === undefined ? '–' : `${Math.round(rtt)} ms`}</span>;\n * ```\n */\nexport function useWebSocketLatency(latency: WebSocketLatency): Latency {\n const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);\n useHold(latency.client, true);\n return snapshot;\n}\n"],"mappings":"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","names":["selection"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-websocket",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Hooks-first WebSocket client for React: shared connections, auto-reconnect, typed messages, Suspense-ready",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -132,6 +132,15 @@
132
132
  "react"
133
133
  ],
134
134
  "limit": "1 kB"
135
+ },
136
+ {
137
+ "name": "react-websocket (requests + latency)",
138
+ "path": "dist/index.js",
139
+ "import": "{ createWebSocketRequests, createWebSocketLatency, useWebSocketLatency }",
140
+ "ignore": [
141
+ "react"
142
+ ],
143
+ "limit": "1.3 kB"
135
144
  }
136
145
  ],
137
146
  "overrides": {