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/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,247 @@ 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
+
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
+
338
601
  /**
339
602
  * Per-component event handlers. They always see the component's latest props and
340
603
  * state, and changing them never reconnects.
@@ -360,8 +623,11 @@ interface UseWebSocketOptions<In, Out> extends WebSocketOptions<In, Out>, WebSoc
360
623
  }
361
624
  /** What `useWebSocket` returns: the current {@link Snapshot} plus stable actions. */
362
625
  interface UseWebSocketResult<In, Out> extends Snapshot<In> {
363
- /** Send now, or queue until open. Returns `true` if sent immediately. Stable identity. */
364
- 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;
365
631
  /** Close and stop reconnecting (for every component sharing the connection). Stable identity. */
366
632
  close: (code?: number, reason?: string) => void;
367
633
  /** Drop the socket and connect again right away. Stable identity. */
@@ -414,5 +680,43 @@ declare function useWebSocketEvent<In, K extends keyof EventMap<In>>(client: Web
414
680
  * ```
415
681
  */
416
682
  declare function useWebSocketStatus(client: WebSocketClient<any, any> | null | undefined): Status;
683
+ /**
684
+ * Read state built from messages by `createWebSocketState`. Re-renders only when the
685
+ * selected value changes (by `isEqual`, default `Object.is`), however often messages
686
+ * arrive. Keeps the connection open while mounted.
687
+ *
688
+ * On the server, and during hydration, it reads from `initialState`.
689
+ *
690
+ * @example
691
+ * ```tsx
692
+ * const acme = useWebSocketState(priceState, state => state.ACME);
693
+ * ```
694
+ */
695
+ declare function useWebSocketState<S>(store: WebSocketState<S>): S;
696
+ declare function useWebSocketState<S, T>(store: WebSocketState<S>, selector: (state: S) => T, isEqual?: (a: T, b: T) => boolean): T;
697
+ /**
698
+ * Subscribe to one topic (or several) while mounted, through a `createWebSocketTopics`
699
+ * manager. Components holding the same topic share one subscription, and it is sent again
700
+ * after every reconnect. Keeps the connection open while mounted. `null` holds nothing.
701
+ *
702
+ * @example
703
+ * ```tsx
704
+ * useWebSocketTopic(topics, `prices:${symbol}`);
705
+ * useWebSocketTopic(topics, watchlist.map(symbol => `prices:${symbol}`));
706
+ * ```
707
+ */
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;
417
721
 
418
- 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, createWebSocket, useWebSocket, useWebSocketEvent, useWebSocketStatus };
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
@@ -4,11 +4,13 @@
4
4
  export * from "./core.js";
5
5
 
6
6
  // src/react.ts
7
- import { useEffect, useInsertionEffect, useRef, useSyncExternalStore } from "react";
7
+ import { useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore } from "react";
8
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;
@@ -93,9 +95,48 @@ function useWebSocketStatus(client) {
93
95
  useHold(client ?? null, true);
94
96
  return status;
95
97
  }
98
+ var identity = (value) => value;
99
+ function useWebSocketState(store, selector = identity, isEqual = Object.is) {
100
+ const [getSelection, getServerSelection] = useMemo(() => {
101
+ const memoized = (read) => {
102
+ let memo;
103
+ return () => {
104
+ const state = read();
105
+ if (memo && Object.is(memo.state, state)) return memo.selection;
106
+ const selection2 = selector(state);
107
+ if (memo && isEqual(memo.selection, selection2)) {
108
+ memo = { state, selection: memo.selection };
109
+ return memo.selection;
110
+ }
111
+ memo = { state, selection: selection2 };
112
+ return selection2;
113
+ };
114
+ };
115
+ return [memoized(store.getState), memoized(() => store.initialState)];
116
+ }, [store, selector, isEqual]);
117
+ const selection = useSyncExternalStore(store.subscribe, getSelection, getServerSelection);
118
+ useHold(store.client, true);
119
+ return selection;
120
+ }
121
+ function useWebSocketTopic(topics, topic) {
122
+ const key = topic == null ? null : JSON.stringify(typeof topic === "string" ? [topic] : topic);
123
+ useEffect(() => {
124
+ if (!topics || key === null) return;
125
+ const releases = JSON.parse(key).map((t) => topics.acquire(t));
126
+ return () => releases.forEach((release) => release());
127
+ }, [topics, key]);
128
+ }
129
+ function useWebSocketLatency(latency) {
130
+ const snapshot = useSyncExternalStore(latency.subscribe, latency.getSnapshot, noLatency);
131
+ useHold(latency.client, true);
132
+ return snapshot;
133
+ }
96
134
  export {
97
135
  useWebSocket,
98
136
  useWebSocketEvent,
99
- useWebSocketStatus
137
+ useWebSocketLatency,
138
+ useWebSocketState,
139
+ useWebSocketStatus,
140
+ useWebSocketTopic
100
141
  };
101
142
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/react.ts"],"sourcesContent":["export * from './core';\nexport { useWebSocket, useWebSocketEvent, useWebSocketStatus } from './react';\nexport type { UseWebSocketOptions, UseWebSocketResult, WebSocketHandlers } from './react';\n","import { useEffect, useInsertionEffect, 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 {\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"],"mappings":"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","names":[]}
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.0.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",
@@ -82,6 +82,7 @@
82
82
  "devDependencies": {
83
83
  "@arethetypeswrong/cli": "^0.18.5",
84
84
  "@eslint/js": "^10.0.1",
85
+ "@fontsource-variable/geist-mono": "^5.3.0",
85
86
  "@size-limit/preset-small-lib": "^14.2.0",
86
87
  "@testing-library/react": "^16.3.3",
87
88
  "@types/react": "^19.3.0",
@@ -122,6 +123,24 @@
122
123
  "react"
123
124
  ],
124
125
  "limit": "2.6 kB"
126
+ },
127
+ {
128
+ "name": "react-websocket (topics + state hooks)",
129
+ "path": "dist/index.js",
130
+ "import": "{ useWebSocketTopic, useWebSocketState, createWebSocketTopics, createWebSocketState }",
131
+ "ignore": [
132
+ "react"
133
+ ],
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"
125
144
  }
126
145
  ],
127
146
  "overrides": {