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/README.md +6 -2
- package/dist/core.cjs +193 -4
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +169 -3
- package/dist/core.d.ts +169 -3
- package/dist/core.js +191 -4
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +186 -5
- package/dist/index.d.ts +186 -5
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
- **Shared connections.** Components using the same URL share one socket. StrictMode, `<Activity>` and route transitions never open duplicates.
|
|
15
15
|
- **Auto-reconnect.** Exponential backoff with jitter, plus an immediate retry when the browser comes back online or the tab becomes visible. Optional heartbeats detect dead links.
|
|
16
|
-
- **Send queue.** `send()` before the socket is open just works. The message goes out on connect.
|
|
16
|
+
- **Send queue.** `send()` before the socket is open just works. The message goes out on connect. Mark input `volatile` so it's never replayed late, or `key` it so only the newest is sent.
|
|
17
17
|
- **Typed messages.** `json: true`, your own codec, or validation with any [Standard Schema](https://standardschema.dev) library (zod, valibot, arktype), with inferred types.
|
|
18
18
|
- **Suspense.** `use(client.ready)` waits for the first connection, and reconnects never flash a fallback.
|
|
19
19
|
- **Topic subscriptions.** Reference-counted `subscribe`/`unsubscribe` messages that are sent again after every reconnect, so feeds don't silently stop.
|
|
20
20
|
- **Message state.** Reduce messages into a store, and let each component re-render only when its slice changes.
|
|
21
|
+
- **Requests and latency.** Await replies with timeouts and cancellation, and measure round-trip time and the server's clock for ping displays and synchronized countdowns.
|
|
21
22
|
- **SSR-safe.** Never connects during a server render, hydrates cleanly, and ships a `'use client'` entry for Next.js and other Server Components frameworks.
|
|
22
23
|
- **Small and typed.** Zero dependencies, ESM + CJS, TypeScript declarations with inline docs, plus a React-free `react-websocket/core` entry.
|
|
23
24
|
|
|
@@ -112,9 +113,12 @@ function Room() {
|
|
|
112
113
|
| `useWebSocketStatus(client)` | The status; re-renders only when it changes |
|
|
113
114
|
| `useWebSocketTopic(topics, topic)` | Hold topic subscriptions that survive reconnects |
|
|
114
115
|
| `useWebSocketState(store, selector?)` | Read state reduced from messages; re-renders only when the selection changes |
|
|
116
|
+
| `useWebSocketLatency(latency)` | Round-trip time and server clock offset |
|
|
115
117
|
| `createWebSocket(url, options?)` | A shareable client: `send`, `on`, `connect`, `close`, `reconnect`, `ready`, `getSnapshot`, `subscribe` |
|
|
116
118
|
| `createWebSocketTopics(client, { subscribe, unsubscribe })` | Topic subscription manager for `useWebSocketTopic` |
|
|
117
119
|
| `createWebSocketState(client, { initialState, reduce })` | Message-state store for `useWebSocketState` |
|
|
120
|
+
| `createWebSocketRequests(client, { send, reply })` | `request(message)` returns a promise of the reply, with timeouts |
|
|
121
|
+
| `createWebSocketLatency(client, { ping, pong })` | Ping tracker for `useWebSocketLatency`, plus `serverNow()` |
|
|
118
122
|
| `MessageError` | Reported to `onError` when a message fails `parse` or `schema` |
|
|
119
123
|
|
|
120
124
|
| Option | Default | |
|
|
@@ -136,7 +140,7 @@ function Room() {
|
|
|
136
140
|
|
|
137
141
|
- [Shared clients](https://mehmetkose.github.io/react-websocket/guide/shared-clients) · [Messages and types](https://mehmetkose.github.io/react-websocket/guide/messages) · [Reconnecting and heartbeats](https://mehmetkose.github.io/react-websocket/guide/reconnecting)
|
|
138
142
|
- [Suspense](https://mehmetkose.github.io/react-websocket/guide/suspense) · [Server rendering and Next.js](https://mehmetkose.github.io/react-websocket/guide/ssr) · [StrictMode, Activity and lifecycle](https://mehmetkose.github.io/react-websocket/guide/lifecycle)
|
|
139
|
-
- [Authentication](https://mehmetkose.github.io/react-websocket/guide/authentication) · [Performance](https://mehmetkose.github.io/react-websocket/guide/performance) · [Testing](https://mehmetkose.github.io/react-websocket/guide/testing) · [Without React](https://mehmetkose.github.io/react-websocket/guide/without-react)
|
|
143
|
+
- [Authentication](https://mehmetkose.github.io/react-websocket/guide/authentication) · [Performance](https://mehmetkose.github.io/react-websocket/guide/performance) · [Real-time games](https://mehmetkose.github.io/react-websocket/guide/games) · [Testing](https://mehmetkose.github.io/react-websocket/guide/testing) · [Without React](https://mehmetkose.github.io/react-websocket/guide/without-react)
|
|
140
144
|
- [Recipes](https://mehmetkose.github.io/react-websocket/recipes) · [Troubleshooting](https://mehmetkose.github.io/react-websocket/troubleshooting) · [Migrating from v2](https://mehmetkose.github.io/react-websocket/migration)
|
|
141
145
|
|
|
142
146
|
The docs source lives in [`docs/`](docs). Run `npm run docs:dev` to browse it locally.
|
package/dist/core.cjs
CHANGED
|
@@ -22,6 +22,8 @@ var core_exports = {};
|
|
|
22
22
|
__export(core_exports, {
|
|
23
23
|
MessageError: () => MessageError,
|
|
24
24
|
createWebSocket: () => createWebSocket,
|
|
25
|
+
createWebSocketLatency: () => createWebSocketLatency,
|
|
26
|
+
createWebSocketRequests: () => createWebSocketRequests,
|
|
25
27
|
createWebSocketState: () => createWebSocketState,
|
|
26
28
|
createWebSocketTopics: () => createWebSocketTopics
|
|
27
29
|
});
|
|
@@ -249,7 +251,7 @@ function createClient(url, options = {}) {
|
|
|
249
251
|
if (ws !== socket) return;
|
|
250
252
|
const queued = pending;
|
|
251
253
|
pending = [];
|
|
252
|
-
for (const data of queued) socket.send(data);
|
|
254
|
+
for (const { data } of queued) socket.send(data);
|
|
253
255
|
};
|
|
254
256
|
socket.onmessage = (event) => receive(event, socket);
|
|
255
257
|
socket.onerror = (event) => emit("error", event);
|
|
@@ -345,13 +347,13 @@ function createClient(url, options = {}) {
|
|
|
345
347
|
set({ lastMessage: message });
|
|
346
348
|
emit("message", message, event);
|
|
347
349
|
}
|
|
348
|
-
function send(message) {
|
|
350
|
+
function send(message, sendOptions) {
|
|
349
351
|
const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
|
|
350
352
|
if (ws && ws.readyState === OPEN) {
|
|
351
353
|
ws.send(data);
|
|
352
354
|
return true;
|
|
353
355
|
}
|
|
354
|
-
if (queueLimit === 0) {
|
|
356
|
+
if (queueLimit === 0 || sendOptions?.volatile) {
|
|
355
357
|
log("dropped message: socket is not open");
|
|
356
358
|
return false;
|
|
357
359
|
}
|
|
@@ -359,7 +361,9 @@ function createClient(url, options = {}) {
|
|
|
359
361
|
log("dropped message: no WebSocket implementation available");
|
|
360
362
|
return false;
|
|
361
363
|
}
|
|
362
|
-
|
|
364
|
+
const key = sendOptions?.key;
|
|
365
|
+
if (key !== void 0) pending = pending.filter((entry) => entry.key !== key);
|
|
366
|
+
pending.push({ data, key });
|
|
363
367
|
if (pending.length > queueLimit) {
|
|
364
368
|
pending.shift();
|
|
365
369
|
log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
|
|
@@ -492,6 +496,189 @@ function createWebSocket(url, options) {
|
|
|
492
496
|
return createClient(url, options);
|
|
493
497
|
}
|
|
494
498
|
|
|
499
|
+
// src/latency.ts
|
|
500
|
+
var EMPTY = Object.freeze({ rtt: void 0, offset: void 0 });
|
|
501
|
+
function median(values) {
|
|
502
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
503
|
+
const middle = sorted.length >> 1;
|
|
504
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
505
|
+
}
|
|
506
|
+
function createWebSocketLatency(client, options) {
|
|
507
|
+
const interval = options.interval ?? 2e3;
|
|
508
|
+
const keep = Math.max(1, options.samples ?? 5);
|
|
509
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
510
|
+
let snapshot = EMPTY;
|
|
511
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
512
|
+
let samples = [];
|
|
513
|
+
let nextId = 0;
|
|
514
|
+
let timer;
|
|
515
|
+
function ping() {
|
|
516
|
+
if (client.getSnapshot().status !== "open") return;
|
|
517
|
+
const id = ++nextId;
|
|
518
|
+
inFlight.set(String(id), performance.now());
|
|
519
|
+
if (inFlight.size > keep * 2) inFlight.delete(inFlight.keys().next().value);
|
|
520
|
+
client.send(options.ping(id), { volatile: true });
|
|
521
|
+
}
|
|
522
|
+
function start() {
|
|
523
|
+
stop();
|
|
524
|
+
inFlight.clear();
|
|
525
|
+
samples = [];
|
|
526
|
+
ping();
|
|
527
|
+
timer = setInterval(ping, interval);
|
|
528
|
+
}
|
|
529
|
+
function stop() {
|
|
530
|
+
clearInterval(timer);
|
|
531
|
+
timer = void 0;
|
|
532
|
+
}
|
|
533
|
+
function measure(id, serverTime) {
|
|
534
|
+
const sentAt = inFlight.get(id);
|
|
535
|
+
if (sentAt === void 0) return;
|
|
536
|
+
inFlight.delete(id);
|
|
537
|
+
const rtt = performance.now() - sentAt;
|
|
538
|
+
const offset = serverTime === void 0 ? void 0 : serverTime + rtt / 2 - Date.now();
|
|
539
|
+
samples.push({ rtt, offset });
|
|
540
|
+
if (samples.length > keep) samples.shift();
|
|
541
|
+
let best;
|
|
542
|
+
for (const sample of samples) {
|
|
543
|
+
if (sample.offset !== void 0 && (!best || sample.rtt < best.rtt)) best = sample;
|
|
544
|
+
}
|
|
545
|
+
snapshot = {
|
|
546
|
+
rtt: median(samples.map((sample) => sample.rtt)),
|
|
547
|
+
offset: best ? best.offset : snapshot.offset
|
|
548
|
+
};
|
|
549
|
+
for (const listener of [...listeners]) listener();
|
|
550
|
+
}
|
|
551
|
+
const offs = [
|
|
552
|
+
client.on("open", start),
|
|
553
|
+
client.on("message", (message) => {
|
|
554
|
+
const pong = options.pong(message);
|
|
555
|
+
if (pong != null) measure(String(pong.id), pong.time);
|
|
556
|
+
}),
|
|
557
|
+
client.subscribe(() => {
|
|
558
|
+
if (client.getSnapshot().status !== "open") stop();
|
|
559
|
+
})
|
|
560
|
+
];
|
|
561
|
+
if (client.getSnapshot().status === "open") start();
|
|
562
|
+
return {
|
|
563
|
+
client,
|
|
564
|
+
getSnapshot: () => snapshot,
|
|
565
|
+
subscribe(listener) {
|
|
566
|
+
listeners.add(listener);
|
|
567
|
+
return () => {
|
|
568
|
+
listeners.delete(listener);
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
serverNow: () => Date.now() + (snapshot.offset ?? 0),
|
|
572
|
+
ping,
|
|
573
|
+
dispose() {
|
|
574
|
+
stop();
|
|
575
|
+
offs.forEach((off) => off());
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/requests.ts
|
|
581
|
+
function createWebSocketRequests(client, options) {
|
|
582
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
583
|
+
let nextId = 0;
|
|
584
|
+
function rejectWhere(test, reason) {
|
|
585
|
+
for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
|
|
586
|
+
}
|
|
587
|
+
const offs = [
|
|
588
|
+
client.on("message", (message) => {
|
|
589
|
+
const id = options.reply(message);
|
|
590
|
+
const entry = id == null ? void 0 : waiting.get(String(id));
|
|
591
|
+
if (!entry) return;
|
|
592
|
+
const error = options.error?.(message);
|
|
593
|
+
if (error === void 0) entry.resolve(message);
|
|
594
|
+
else entry.reject(error);
|
|
595
|
+
}),
|
|
596
|
+
// `open` listeners run before the client flushes its queue: queued requests go out on this socket.
|
|
597
|
+
client.on("open", () => {
|
|
598
|
+
for (const entry of waiting.values()) entry.socket ??= client.socket;
|
|
599
|
+
}),
|
|
600
|
+
client.subscribe(() => {
|
|
601
|
+
const { status } = client.getSnapshot();
|
|
602
|
+
if (status === "closed" || status === "idle") {
|
|
603
|
+
rejectWhere(
|
|
604
|
+
() => true,
|
|
605
|
+
() => new Error("WebSocket was closed before the request was answered")
|
|
606
|
+
);
|
|
607
|
+
} else {
|
|
608
|
+
rejectWhere(
|
|
609
|
+
(entry) => entry.socket !== null && entry.socket !== client.socket,
|
|
610
|
+
() => new Error("WebSocket connection was lost before the request was answered")
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
];
|
|
615
|
+
return {
|
|
616
|
+
client,
|
|
617
|
+
request(message, requestOptions = {}) {
|
|
618
|
+
const { signal } = requestOptions;
|
|
619
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
620
|
+
const id = ++nextId;
|
|
621
|
+
const key = String(id);
|
|
622
|
+
const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
|
|
623
|
+
return new Promise((resolve, reject) => {
|
|
624
|
+
let timer;
|
|
625
|
+
const release = client.acquire();
|
|
626
|
+
const onAbort = () => entry.reject(signal.reason);
|
|
627
|
+
function settle() {
|
|
628
|
+
waiting.delete(key);
|
|
629
|
+
clearTimeout(timer);
|
|
630
|
+
signal?.removeEventListener("abort", onAbort);
|
|
631
|
+
release();
|
|
632
|
+
}
|
|
633
|
+
const entry = {
|
|
634
|
+
socket: null,
|
|
635
|
+
resolve(reply) {
|
|
636
|
+
settle();
|
|
637
|
+
resolve(reply);
|
|
638
|
+
},
|
|
639
|
+
reject(reason) {
|
|
640
|
+
settle();
|
|
641
|
+
reject(reason);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
waiting.set(key, entry);
|
|
645
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
646
|
+
if (timeout > 0 && timeout < Infinity) {
|
|
647
|
+
timer = setTimeout(
|
|
648
|
+
() => entry.reject(
|
|
649
|
+
new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")
|
|
650
|
+
),
|
|
651
|
+
timeout
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const { status } = client.getSnapshot();
|
|
655
|
+
if (status === "closed" || status === "idle") {
|
|
656
|
+
entry.reject(new Error(`WebSocket is ${status}: request not sent`));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
let sent;
|
|
660
|
+
try {
|
|
661
|
+
sent = client.send(options.send(message, id), requestOptions);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
entry.reject(error);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (sent) entry.socket = client.socket;
|
|
667
|
+
else if (requestOptions.volatile)
|
|
668
|
+
entry.reject(new Error("WebSocket is not open: request dropped"));
|
|
669
|
+
});
|
|
670
|
+
},
|
|
671
|
+
pending: () => waiting.size,
|
|
672
|
+
dispose() {
|
|
673
|
+
offs.forEach((off) => off());
|
|
674
|
+
rejectWhere(
|
|
675
|
+
() => true,
|
|
676
|
+
() => new Error("Requests were disposed")
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
495
682
|
// src/state.ts
|
|
496
683
|
function createWebSocketState(client, options) {
|
|
497
684
|
const { initialState, reduce } = options;
|
|
@@ -588,6 +775,8 @@ function createWebSocketTopics(client, options) {
|
|
|
588
775
|
0 && (module.exports = {
|
|
589
776
|
MessageError,
|
|
590
777
|
createWebSocket,
|
|
778
|
+
createWebSocketLatency,
|
|
779
|
+
createWebSocketRequests,
|
|
591
780
|
createWebSocketState,
|
|
592
781
|
createWebSocketTopics
|
|
593
782
|
});
|
package/dist/core.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts","../src/types.ts","../src/client.ts","../src/state.ts","../src/topics.ts"],"sourcesContent":["export { createWebSocket } from './client';\nexport { createWebSocketState } from './state';\nexport type { WebSocketState, WebSocketStateOptions } from './state';\nexport { createWebSocketTopics } from './topics';\nexport type { WebSocketTopics, WebSocketTopicsOptions } from './topics';\nexport { MessageError } from './types';\nexport type {\n CloseInfo,\n EventMap,\n HeartbeatOptions,\n QueueOptions,\n ReconnectOptions,\n SendData,\n Snapshot,\n StandardSchemaV1,\n Status,\n UrlSource,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n","/**\n * Where to connect: a URL, or a function that returns one (sync or async).\n *\n * A function is called before **every** connection attempt, including reconnects,\n * so it can put a fresh auth token in the URL. If it throws or rejects, that counts\n * as a failed attempt and is retried with backoff.\n *\n * @example\n * ```ts\n * createWebSocket(async () => `wss://api.example.com/live?token=${await getToken()}`);\n * ```\n */\nexport type UrlSource = string | (() => string | Promise<string>);\n\n/**\n * Connection state.\n *\n * - `idle`: not connected and not trying to be. Nobody is using the client yet, or everyone stopped.\n * - `connecting`: the first attempt is in progress.\n * - `open`: connected. `send()` goes out immediately.\n * - `reconnecting`: the connection dropped (or an attempt failed) and a retry is scheduled or in progress.\n * - `closed`: stopped for good, by `close()` or after giving up on reconnecting.\n * It stays closed until `connect()` or `reconnect()` is called, or every hook unmounts.\n */\nexport type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n/** The client's state as seen by React. A new object is created on every change. */\nexport interface Snapshot<In> {\n /** Connection state. */\n readonly status: Status;\n /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */\n readonly lastMessage: In | undefined;\n /** Consecutive failed attempts since the last successful open. `0` while open. */\n readonly attempt: number;\n}\n\n/** Details of a closed connection, from the browser's `CloseEvent`. */\nexport interface CloseInfo {\n /** Close code: `1000` is a normal close, `1006` an abnormal one (e.g. network loss), `4000`–`4999` are yours. */\n readonly code: number;\n /** Close reason sent by the side that closed the connection. Often empty. */\n readonly reason: string;\n /** Whether the closing handshake completed. */\n readonly wasClean: boolean;\n}\n\n/** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */\nexport type SendData = Parameters<WebSocket['send']>[0];\n\n/** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */\nexport interface ReconnectOptions {\n /**\n * Maximum consecutive reconnect attempts. When the last one fails too, the client\n * gives up: the status becomes `closed` and `ready` rejects. `0` never retries.\n * @default Infinity\n */\n maxAttempts?: number;\n /**\n * Delay before the first retry, in ms.\n * @default 1000\n */\n minDelay?: number;\n /**\n * Upper bound for the delay, in ms.\n * @default 30000\n */\n maxDelay?: number;\n /**\n * Backoff multiplier applied per attempt.\n * @default 2\n */\n factor?: number;\n /**\n * Randomly shave up to this fraction (0–1) off each delay, so that many clients\n * dropped at once don't all reconnect at the same moment.\n * @default 0.5\n */\n jitter?: number;\n /**\n * Called on every unexpected close. Return `false` to stop reconnecting.\n *\n * @example\n * ```ts\n * // Don't retry when the server rejects our credentials.\n * shouldReconnect: close => close.code !== 4001\n * ```\n * @default () => true\n */\n shouldReconnect?: (close: CloseInfo) => boolean;\n}\n\n/** Application-level keep-alive. Useful behind proxies that drop idle connections. */\nexport interface HeartbeatOptions {\n /** Send `message` every `interval` ms while open. */\n interval: number;\n /**\n * Sent as-is (it is not passed through `serialize` or `json`).\n * @default 'ping'\n */\n message?: string;\n /** Incoming data equal to this string is swallowed instead of being delivered as a message. */\n pong?: string;\n /**\n * If nothing at all arrives within this many ms after a ping, the connection is\n * treated as dead: it closes with code `4000` and reconnects.\n */\n timeout?: number;\n}\n\n/** Bounds for the send queue. */\nexport interface QueueOptions {\n /**\n * Maximum number of messages held while not connected. When full, the oldest\n * message is dropped.\n * @default Infinity\n */\n limit?: number;\n}\n\n/**\n * The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented\n * by zod (3.24+), valibot (1.0+), arktype (2.0+) and others. You don't need to\n * import this; pass your schema object directly.\n */\nexport interface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (\n value: unknown\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n readonly types?: { readonly input: Input; readonly output: Output } | undefined;\n };\n}\n\n/** The result of a Standard Schema `validate` call. */\nexport type StandardSchemaResult<Output> =\n | { readonly value: Output; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> };\n\n/**\n * Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.\n *\n * @typeParam In - Type of incoming messages (after `parse`/`json`/`schema`).\n * @typeParam Out - Type accepted by `send()`.\n */\nexport interface WebSocketOptions<In = unknown, Out = unknown> {\n /** Sub-protocol(s) passed to the `WebSocket` constructor. */\n protocols?: string | string[];\n /**\n * How binary messages are exposed.\n * @default 'blob'\n */\n binaryType?: BinaryType;\n /**\n * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail\n * to parse are reported to `onError` as a {@link MessageError} and not delivered.\n * @default false\n */\n json?: boolean;\n /**\n * Custom decoder for incoming `event.data`. Takes precedence over `json`.\n * Throwing reports a {@link MessageError} and drops the message.\n */\n parse?: (data: unknown) => unknown;\n /** Custom encoder for outgoing messages. Takes precedence over `json`. */\n serialize?: (message: Out) => SendData;\n /**\n * Validate incoming messages (after parsing) with any Standard Schema library.\n * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.\n * The message type `In` is inferred from it.\n *\n * @example\n * ```ts\n * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });\n * ```\n */\n schema?: StandardSchemaV1<unknown, In>;\n /**\n * Reconnect with exponential backoff after an unexpected close. `false` disables it.\n * @default true\n */\n reconnect?: boolean | ReconnectOptions;\n /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */\n heartbeat?: HeartbeatOptions;\n /**\n * Buffer `send()` calls made while not connected and send them, in order, on open\n * (right after `open` listeners run).\n * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.\n * The queue is cleared by `close()` or when the client gives up.\n * @default true\n */\n queue?: boolean | QueueOptions;\n /**\n * Receives debug messages about connection attempts, retries and dropped messages.\n * @example\n * ```ts\n * logger: console.debug\n * ```\n */\n logger?: (message: string, ...details: unknown[]) => void;\n /**\n * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.\n * When omitted, the global `WebSocket` is used in browsers. During a server render\n * (no `window`), nothing connects unless an implementation is passed.\n */\n WebSocket?: typeof WebSocket;\n}\n\n/**\n * Reported via the `error` event (and `onError`) when an incoming message can't be\n * decoded by `parse`/`json` or fails `schema` validation. The message is not delivered.\n *\n * @example\n * ```ts\n * onError(error) {\n * if (error instanceof MessageError) report(error.data, error.issues);\n * }\n * ```\n */\nexport class MessageError extends Error {\n /** The raw `event.data` that failed. */\n readonly data: unknown;\n /** Schema validation issues, when the message failed `schema`. */\n readonly issues: ReadonlyArray<{ readonly message: string }> | undefined;\n\n constructor(\n message: string,\n data: unknown,\n options?: { cause?: unknown; issues?: ReadonlyArray<{ readonly message: string }> }\n ) {\n super(message, options?.cause === undefined ? undefined : { cause: options.cause });\n this.name = 'MessageError';\n this.data = data;\n this.issues = options?.issues;\n }\n}\n\n/** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */\nexport interface EventMap<In> {\n /**\n * The socket opened (initially or after a reconnect). Listeners run before queued\n * messages are sent, so a handshake sent from here goes out first.\n */\n open: (event: Event) => void;\n /** A message arrived and passed `parse` and `schema`. */\n message: (message: In, event: MessageEvent) => void;\n /** A socket closed, whether it will be retried or not. */\n close: (close: CloseInfo) => void;\n /**\n * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,\n * or the `Error` thrown by the `WebSocket` constructor or a `url` function.\n */\n error: (error: Event | MessageError | Error) => void;\n}\n\n/**\n * A WebSocket connection that reconnects, queues, decodes and can be shared.\n * Create one with `createWebSocket()`.\n *\n * The connection is reference-counted: it opens when the first hook mounts (or\n * `ready` is read) and closes one tick after the last hook unmounts. Calling\n * `connect()` keeps it open regardless, until `close()`.\n *\n * @typeParam In - Type of incoming messages.\n * @typeParam Out - Type accepted by `send()`.\n */\nexport interface WebSocketClient<In = unknown, Out = unknown> {\n /** The URL (or URL function) the client was created with. */\n readonly url: UrlSource;\n /**\n * A promise that resolves once the socket has opened. Reading it starts an idle client.\n *\n * It stays resolved through automatic reconnects, so `use(client.ready)` shows a\n * Suspense fallback only for the first connection, never on a network blip; use\n * `status` to show reconnects. It rejects when the client gives up reconnecting, when\n * `close()` is called, and during a server render. After that, `connect()` or\n * `reconnect()` make it a new pending promise.\n *\n * It is safe to pass to React 19's `use()`: the same promise is returned until its state\n * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is\n * already settled.\n */\n readonly ready: Promise<void>;\n /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */\n readonly socket: WebSocket | null;\n /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** The current {@link Snapshot}. Returns the same object until something changes. */\n getSnapshot(): Snapshot<In>;\n /**\n * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown\n * asynchronously, so one failing listener doesn't break the others.\n */\n on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;\n /**\n * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't\n * open, the message is queued and sent on open (see the `queue` option).\n * @returns `true` if it was sent immediately, `false` if it was queued or dropped.\n */\n send(message: Out): boolean;\n /**\n * Start connecting, and keep the connection open even when no hook is using the\n * client, until `close()` is called. Does nothing if already connecting or open.\n */\n connect(): void;\n /**\n * Close the connection and stop reconnecting. The status becomes `closed` and the\n * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`\n * to start again.\n * @param code - Close code. @default 1000\n */\n close(code?: number, reason?: string): void;\n /**\n * Drop the current socket (if any) and connect again immediately, resetting the\n * backoff. Also restarts a `closed` client.\n */\n reconnect(): void;\n /**\n * Hold the connection open until the returned function is called. This is what the\n * hooks use; call it yourself to integrate with non-React code.\n * @returns A release function (safe to call more than once).\n */\n acquire(): () => void;\n}\n","import {\n MessageError,\n type CloseInfo,\n type EventMap,\n type ReconnectOptions,\n type SendData,\n type Snapshot,\n type StandardSchemaResult,\n type UrlSource,\n type WebSocketClient,\n type WebSocketOptions\n} from './types';\n\nconst OPEN = 1;\n\nconst RECONNECT_DEFAULTS = {\n maxAttempts: Infinity,\n minDelay: 1000,\n maxDelay: 30_000,\n factor: 2,\n jitter: 0.5\n} satisfies ReconnectOptions;\n\ntype Timer = ReturnType<typeof setTimeout> | undefined;\n\n// React's `use()` reads `status`/`value`/`reason` off a thenable to skip suspending\n// when it is already settled.\ntype TrackedPromise = Promise<void> & {\n status: 'pending' | 'fulfilled' | 'rejected';\n value?: void;\n reason?: unknown;\n};\n\ninterface Deferred {\n readonly promise: TrackedPromise;\n settled: boolean;\n resolve(): void;\n reject(reason: unknown): void;\n}\n\nfunction createDeferred(): Deferred {\n let resolve!: () => void;\n let reject!: (reason: unknown) => void;\n const promise = new Promise<void>((res, rej) => {\n resolve = res;\n reject = rej;\n }) as TrackedPromise;\n promise.status = 'pending';\n // Nobody may be awaiting `ready`; don't surface that as an unhandled rejection.\n promise.catch(() => {});\n const deferred: Deferred = {\n promise,\n settled: false,\n resolve() {\n if (deferred.settled) return;\n deferred.settled = true;\n promise.status = 'fulfilled';\n promise.value = undefined;\n resolve();\n },\n reject(reason) {\n if (deferred.settled) return;\n deferred.settled = true;\n promise.status = 'rejected';\n promise.reason = reason;\n reject(reason);\n }\n };\n return deferred;\n}\n\nfunction rethrowAsync(error: unknown) {\n queueMicrotask(() => {\n throw error;\n });\n}\n\nfunction isThenable<T>(value: unknown): value is PromiseLike<T> {\n return typeof (value as PromiseLike<T> | null)?.then === 'function';\n}\n\n/**\n * @internal Option key for a callback run when the client tears down, which lets the hooks'\n * URL registry forget clients nobody holds anymore. A registered symbol rather than an export,\n * because the hooks entry imports the client from the `core` entry at runtime.\n */\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\nfunction createClient<In = unknown, Out = unknown>(\n url: UrlSource,\n options: WebSocketOptions<In, Out> & { [ON_TEARDOWN]?: () => void } = {}\n): WebSocketClient<In, Out> {\n const onTeardown = options[ON_TEARDOWN];\n const log = options.logger ?? (() => {});\n const retry =\n options.reconnect === false\n ? null\n : {\n ...RECONNECT_DEFAULTS,\n ...(typeof options.reconnect === 'object' ? options.reconnect : {})\n };\n const queueLimit =\n options.queue === false\n ? 0\n : typeof options.queue === 'object'\n ? Math.max(0, options.queue.limit ?? Infinity)\n : Infinity;\n\n let snapshot: Snapshot<In> = { status: 'idle', lastMessage: undefined, attempt: 0 };\n const storeListeners = new Set<() => void>();\n const listeners: { [K in keyof EventMap<In>]: Set<EventMap<In>[K]> } = {\n open: new Set(),\n message: new Set(),\n close: new Set(),\n error: new Set()\n };\n\n let ws: WebSocket | null = null;\n let wanted = false;\n let refs = 0;\n // Set by a public `connect()`: stay connected after the last hook releases, until `close()`.\n let pinned = false;\n let pending: SendData[] = [];\n let ready: Deferred | null = null;\n // Opened at least once since the client was last started. `ready` stays resolved through\n // automatic reconnects so `use(client.ready)` doesn't swap content for a fallback on every blip.\n let opened = false;\n let unavailable: Deferred | null = null;\n let startScheduled = false;\n let inbox: Promise<void> | null = null;\n // Bumped per connection attempt so a slow `url()` can't resurrect a cancelled one.\n let generation = 0;\n let windowBound = false;\n let retryTimer: Timer;\n let teardownTimer: Timer;\n let heartbeatTimer: ReturnType<typeof setInterval> | undefined;\n let pongTimer: Timer;\n\n function set(patch: Partial<Snapshot<In>>) {\n snapshot = { ...snapshot, ...patch };\n for (const listener of [...storeListeners]) listener();\n }\n\n function emit<K extends keyof EventMap<In>>(type: K, ...args: Parameters<EventMap<In>[K]>) {\n for (const listener of [...listeners[type]]) {\n try {\n (listener as (...a: Parameters<EventMap<In>[K]>) => void)(...args);\n } catch (error) {\n rethrowAsync(error);\n }\n }\n }\n\n function getImpl(): typeof WebSocket | undefined {\n if (options.WebSocket) return options.WebSocket;\n // Node has a global WebSocket too; don't connect from a server render.\n return typeof window === 'undefined' ? undefined : globalThis.WebSocket;\n }\n\n function rejectReady(reason: unknown) {\n if (!ready || ready.settled) ready = createDeferred();\n ready.reject(reason);\n }\n\n function retryNow() {\n if (retryTimer !== undefined) {\n log('network or visibility change, retrying now');\n open();\n }\n }\n\n function onVisibilityChange() {\n if (document.visibilityState === 'visible') retryNow();\n }\n\n function bindWindow() {\n if (windowBound || typeof window === 'undefined') return;\n windowBound = true;\n window.addEventListener('online', retryNow);\n document.addEventListener('visibilitychange', onVisibilityChange);\n }\n\n function unbindWindow() {\n if (!windowBound) return;\n windowBound = false;\n window.removeEventListener('online', retryNow);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n\n function startHeartbeat(socket: WebSocket) {\n const heartbeat = options.heartbeat;\n if (!heartbeat) return;\n heartbeatTimer = setInterval(() => {\n socket.send(heartbeat.message ?? 'ping');\n if (heartbeat.timeout && pongTimer === undefined) {\n pongTimer = setTimeout(() => {\n log('heartbeat timeout');\n // The close handshake may never finish on a dead link, so don't wait for it.\n detach(socket, false);\n socket.close(4000, 'heartbeat timeout');\n handleClose({ code: 4000, reason: 'heartbeat timeout', wasClean: false });\n }, heartbeat.timeout);\n }\n }, heartbeat.interval);\n }\n\n function stopHeartbeat() {\n clearInterval(heartbeatTimer);\n clearTimeout(pongTimer);\n heartbeatTimer = pongTimer = undefined;\n }\n\n /** Stop tracking `socket`. Its eventual close event is still reported unless `reportClose` is false. */\n function detach(socket: WebSocket, reportClose: boolean) {\n if (ws === socket) ws = null;\n socket.onopen = socket.onmessage = socket.onerror = null;\n socket.onclose = reportClose\n ? event => emit('close', { code: event.code, reason: event.reason, wasClean: event.wasClean })\n : null;\n }\n\n function backoff(attempt: number) {\n const { minDelay, maxDelay, factor, jitter } = retry!;\n const base = Math.min(maxDelay, minDelay * factor ** (attempt - 1));\n return Math.round(base * (1 - jitter * Math.random()));\n }\n\n function open() {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n const Impl = getImpl();\n if (!Impl) return;\n const id = ++generation;\n set({ status: snapshot.attempt === 0 ? 'connecting' : 'reconnecting' });\n\n let target: string | Promise<string>;\n try {\n target = typeof url === 'string' ? url : url();\n } catch (error) {\n urlFailed(error);\n return;\n }\n if (typeof target === 'string') {\n start(Impl, target);\n return;\n }\n Promise.resolve(target).then(\n resolved => {\n if (id === generation && wanted) start(Impl, resolved);\n },\n error => {\n if (id === generation && wanted) urlFailed(error);\n }\n );\n }\n\n /** `url()` threw or rejected (e.g. a token refresh failed): count it as a failed attempt. */\n function urlFailed(error: unknown) {\n const reason = error instanceof Error ? error : new Error(String(error));\n log('url() failed', reason);\n emit('error', reason);\n retryOrFail({ code: 1006, reason: 'url() failed', wasClean: false }, reason);\n }\n\n function start(Impl: typeof WebSocket, target: string) {\n let socket: WebSocket;\n try {\n socket = new Impl(target, options.protocols);\n } catch (error) {\n // e.g. an invalid URL; retrying won't help.\n emit('error', error as Error);\n fail(error);\n return;\n }\n if (options.binaryType) socket.binaryType = options.binaryType;\n ws = socket;\n log('connecting', target);\n\n socket.onopen = event => {\n log('open');\n opened = true;\n set({ status: 'open', attempt: 0 });\n startHeartbeat(socket);\n ready?.resolve();\n // `open` listeners go first, so a handshake sent from `onOpen` (e.g. auth) precedes\n // whatever was queued while disconnected.\n emit('open', event);\n if (ws !== socket) return; // a listener closed or replaced it\n const queued = pending;\n pending = [];\n for (const data of queued) socket.send(data);\n };\n socket.onmessage = event => receive(event, socket);\n socket.onerror = event => emit('error', event);\n socket.onclose = event => {\n detach(socket, false);\n handleClose({ code: event.code, reason: event.reason, wasClean: event.wasClean });\n };\n }\n\n function handleClose(info: CloseInfo) {\n stopHeartbeat();\n log('closed', info);\n retryOrFail(info);\n emit('close', info);\n }\n\n function retryOrFail(info: CloseInfo, error?: Error) {\n if (\n wanted &&\n retry &&\n snapshot.attempt < retry.maxAttempts &&\n (retry.shouldReconnect?.(info) ?? true)\n ) {\n const attempt = snapshot.attempt + 1;\n const delay = backoff(attempt);\n set({ status: 'reconnecting', attempt });\n log(`reconnecting in ${delay}ms (attempt ${attempt})`);\n retryTimer = setTimeout(open, delay);\n } else {\n fail(\n error ??\n new Error(`WebSocket closed (code ${info.code}${info.reason ? `: ${info.reason}` : ''})`)\n );\n }\n }\n\n function fail(reason: unknown) {\n wanted = false;\n opened = false;\n unbindWindow();\n pending = [];\n set({ status: 'closed' });\n rejectReady(reason);\n }\n\n function receive(event: MessageEvent, socket: WebSocket) {\n clearTimeout(pongTimer);\n pongTimer = undefined;\n if (options.heartbeat?.pong !== undefined && event.data === options.heartbeat.pong) return;\n\n // Async schema validation must not reorder messages.\n const run = () => decode(event, socket);\n if (inbox) {\n const next: Promise<void> = inbox.then(run).then(() => {\n if (inbox === next) inbox = null;\n });\n inbox = next;\n return;\n }\n const result = run();\n if (result) {\n const next: Promise<void> = result.then(() => {\n if (inbox === next) inbox = null;\n });\n inbox = next;\n }\n }\n\n function decode(event: MessageEvent, socket: WebSocket): void | Promise<void> {\n let value: unknown;\n try {\n value = options.parse\n ? options.parse(event.data)\n : options.json\n ? JSON.parse(event.data as string)\n : event.data;\n } catch (cause) {\n emit('error', new MessageError('Failed to parse message', event.data, { cause }));\n return;\n }\n\n const schema = options.schema;\n if (!schema) return deliver(value as In, event, socket);\n\n let result: ReturnType<(typeof schema)['~standard']['validate']>;\n try {\n result = schema['~standard'].validate(value);\n } catch (cause) {\n emit('error', new MessageError('Schema validation threw', event.data, { cause }));\n return;\n }\n if (isThenable<StandardSchemaResult<In>>(result)) {\n return Promise.resolve(result).then(\n r => validated(r, event, socket),\n cause => emit('error', new MessageError('Schema validation threw', event.data, { cause }))\n );\n }\n validated(result, event, socket);\n }\n\n function validated(result: StandardSchemaResult<In>, event: MessageEvent, socket: WebSocket) {\n if (result.issues) {\n emit(\n 'error',\n new MessageError('Message failed schema validation', event.data, { issues: result.issues })\n );\n } else {\n deliver(result.value, event, socket);\n }\n }\n\n function deliver(message: In, event: MessageEvent, socket: WebSocket) {\n if (ws !== socket) return; // closed or replaced while validating\n set({ lastMessage: message });\n emit('message', message, event);\n }\n\n function send(message: Out): boolean {\n const data = options.serialize\n ? options.serialize(message)\n : options.json\n ? JSON.stringify(message)\n : (message as SendData);\n if (ws && ws.readyState === OPEN) {\n ws.send(data);\n return true;\n }\n if (queueLimit === 0) {\n log('dropped message: socket is not open');\n return false;\n }\n if (!getImpl()) {\n // e.g. during a server render: this client can never connect, so don't hold on to it.\n log('dropped message: no WebSocket implementation available');\n return false;\n }\n pending.push(data);\n if (pending.length > queueLimit) {\n pending.shift();\n log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);\n }\n return false;\n }\n\n function begin() {\n wanted = true;\n // Starting over after `close()` or giving up: wait for a new connection.\n if (snapshot.status === 'idle' || snapshot.status === 'closed') {\n opened = false;\n if (ready?.settled) ready = null;\n }\n snapshot = { ...snapshot, attempt: 0 }; // open() notifies\n bindWindow();\n open();\n }\n\n /** Start connecting unless already wanted. Used by holders (`acquire`, `ready`). */\n function ensureConnecting() {\n if (wanted || !getImpl()) return;\n begin();\n }\n\n function connect() {\n pinned = true;\n ensureConnecting();\n }\n\n function stop(code?: number, reason?: string) {\n wanted = false;\n generation++;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n stopHeartbeat();\n unbindWindow();\n pending = [];\n if (ws) {\n const socket = ws;\n detach(socket, true);\n socket.close(code, reason);\n }\n }\n\n function close(code = 1000, reason?: string) {\n pinned = false;\n opened = false;\n stop(code, reason);\n set({ status: 'closed', attempt: 0 });\n rejectReady(new Error('WebSocket was closed'));\n }\n\n function reconnect() {\n if (!getImpl()) return;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n stopHeartbeat();\n if (ws) {\n const socket = ws;\n detach(socket, true);\n socket.close(1000, 'reconnect');\n }\n begin();\n }\n\n function teardown() {\n if (refs > 0 || pinned) return;\n // Someone may be suspended on `ready`; let the attempt finish.\n if (ready && !ready.settled) return;\n stop();\n ready = null;\n opened = false;\n set({ status: 'idle', attempt: 0 });\n onTeardown?.();\n }\n\n function acquire() {\n refs++;\n clearTimeout(teardownTimer);\n if (snapshot.status === 'idle') ensureConnecting();\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--refs > 0) return;\n // Deferred so StrictMode's unmount/remount and route transitions don't churn the socket.\n clearTimeout(teardownTimer);\n teardownTimer = setTimeout(teardown, 0);\n };\n }\n\n return {\n url,\n get ready() {\n if (!getImpl()) {\n if (!unavailable) {\n unavailable = createDeferred();\n unavailable.reject(\n new Error('react-websocket: no WebSocket available here (server render?)')\n );\n }\n return unavailable.promise;\n }\n if (snapshot.status === 'idle' && !startScheduled) {\n // `ready` is read during render; start outside of it.\n startScheduled = true;\n queueMicrotask(() => {\n startScheduled = false;\n if (snapshot.status === 'idle') ensureConnecting();\n });\n }\n if (!ready) {\n ready = createDeferred();\n if (opened) ready.resolve();\n }\n return ready.promise;\n },\n get socket() {\n return ws;\n },\n subscribe(listener) {\n storeListeners.add(listener);\n return () => {\n storeListeners.delete(listener);\n };\n },\n getSnapshot: () => snapshot,\n on(type, listener) {\n // Wrap so the same function can be subscribed twice and unsubscribed independently.\n const entry = ((...args: unknown[]) =>\n (listener as (...a: unknown[]) => void)(...args)) as typeof listener;\n listeners[type].add(entry);\n return () => {\n listeners[type].delete(entry);\n };\n },\n send,\n connect,\n close,\n reconnect,\n acquire\n };\n}\n\n/**\n * Create a WebSocket client, usually at module level, to share across components.\n *\n * Nothing connects until a hook uses it, `ready` is read, or `connect()` is called.\n * It closes one tick after the last hook unmounts, unless `connect()` was called.\n *\n * @param url - A URL, or a function (sync or async) called before every connection\n * attempt, e.g. to put a fresh auth token in the URL on each reconnect.\n * @typeParam In - Type of incoming messages. Inferred from `options.schema` if given.\n * @typeParam Out - Type accepted by `send()`.\n *\n * @example\n * ```ts\n * type ChatIn = { user: string; text: string };\n * type ChatOut = { text: string };\n *\n * export const chat = createWebSocket<ChatIn, ChatOut>('wss://api.example.com/chat', { json: true });\n * ```\n */\nexport function createWebSocket<In = unknown, Out = unknown>(\n url: UrlSource,\n options?: WebSocketOptions<In, Out>\n): WebSocketClient<In, Out> {\n return createClient(url, options);\n}\n","import type { WebSocketClient } from './types';\n\n/** Options for {@link createWebSocketState}. */\nexport interface WebSocketStateOptions<S, In> {\n /** The state before any message arrives, and what `reset()` returns to. */\n initialState: S;\n /**\n * Fold one incoming message into the state. Must be pure: return a new object for a\n * change, or the same `state` to ignore the message (subscribers aren't notified).\n */\n reduce: (state: S, message: In) => S;\n}\n\n/**\n * State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,\n * or with `getState`/`subscribe` outside React.\n *\n * @typeParam S - The state type. Don't use a function type: `setState` treats functions as updaters.\n */\nexport interface WebSocketState<S> {\n /** The client whose messages this store reduces. Hooks reading the store keep it connected. */\n readonly client: WebSocketClient<any, any>;\n /** The `initialState` it was created with. Used for server rendering and hydration. */\n readonly initialState: S;\n /** The current state. Returns the same object until it changes. */\n getState(): S;\n /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */\n subscribe(listener: () => void): () => void;\n /** Replace the state, or update it from the current one, e.g. for optimistic updates. */\n setState(update: S | ((state: S) => S)): void;\n /** Go back to `initialState`. */\n reset(): void;\n /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */\n dispose(): void;\n}\n\n/**\n * Keep state derived from a client's messages, like a reducer that runs on every message.\n * It starts listening immediately and keeps the state across reconnects; it doesn't open\n * the connection by itself.\n *\n * @example\n * ```ts\n * const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });\n *\n * export const priceState = createWebSocketState(prices, {\n * initialState: {} as Record<string, number>,\n * reduce: (state, tick) => ({ ...state, [tick.symbol]: tick.price })\n * });\n *\n * // In a component: re-renders only when ACME's price changes.\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function createWebSocketState<S, In>(\n client: WebSocketClient<In, any>,\n // `In` comes from the client only, not from how `reduce` annotates its parameter.\n options: WebSocketStateOptions<S, NoInfer<In>>\n): WebSocketState<S> {\n const { initialState, reduce } = options;\n let state = initialState;\n const listeners = new Set<() => void>();\n\n function setState(update: S | ((state: S) => S)) {\n const next = typeof update === 'function' ? (update as (state: S) => S)(state) : update;\n if (Object.is(next, state)) return;\n state = next;\n for (const listener of [...listeners]) listener();\n }\n\n // A throwing reducer leaves the state unchanged; the client re-throws the error asynchronously.\n const off = client.on('message', message => setState(current => reduce(current, message)));\n\n return {\n client,\n initialState,\n getState: () => state,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n setState,\n reset: () => setState(initialState),\n dispose: off\n };\n}\n","import type { WebSocketClient } from './types';\n\ntype Timer = ReturnType<typeof setTimeout>;\n\n/** Options for {@link createWebSocketTopics}: the messages your server expects. */\nexport interface WebSocketTopicsOptions<Out> {\n /** The message that subscribes to `topic`. */\n subscribe: (topic: string) => Out;\n /** The message that unsubscribes from `topic`. Omit it if your server has none. */\n unsubscribe?: (topic: string) => Out;\n}\n\n/** Reference-counted topic subscriptions on one client. Create with {@link createWebSocketTopics}. */\nexport interface WebSocketTopics {\n /** The client the subscription messages are sent on. */\n readonly client: WebSocketClient<any, any>;\n /**\n * Hold a subscription to `topic`, and the client's connection, until the returned\n * function is called. The release function is safe to call more than once.\n */\n acquire(topic: string): () => void;\n /** The topics currently held, in the order they were first acquired. */\n active(): string[];\n /** Stop managing subscriptions. Only needed for managers created dynamically. */\n dispose(): void;\n}\n\n/**\n * Manage topic subscriptions that survive reconnects.\n *\n * - The first holder of a topic sends `subscribe(topic)`. Later holders share it.\n * - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode\n * and route transitions don't churn subscriptions.\n * - After every reconnect, every held topic is subscribed again on the new socket, before\n * messages queued while disconnected are sent.\n *\n * Subscriptions are only sent while the socket is open; nothing is queued for them.\n *\n * @example\n * ```ts\n * export const topics = createWebSocketTopics(live, {\n * subscribe: topic => ({ type: 'subscribe', topic }),\n * unsubscribe: topic => ({ type: 'unsubscribe', topic })\n * });\n *\n * // In a component:\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * ```\n */\n// `const` + `NoInfer`: take `Out` from the client only, and keep literals such as\n// `type: 'subscribe'` from widening to `string` inside the callbacks.\nexport function createWebSocketTopics<const Out>(\n client: WebSocketClient<any, Out>,\n options: WebSocketTopicsOptions<NoInfer<Out>>\n): WebSocketTopics {\n const counts = new Map<string, number>();\n const pendingRemoval = new Map<string, Timer>();\n // Topics subscribed on `sentOn`. A new socket starts with none, whatever the old one had.\n let sentOn: WebSocket | null = null;\n let sent = new Set<string>();\n\n function isOpen() {\n return client.getSnapshot().status === 'open' && client.socket !== null;\n }\n\n function subscribe(topic: string) {\n if (!isOpen()) return; // sent on `open` instead\n if (client.socket !== sentOn) {\n sentOn = client.socket;\n sent = new Set();\n }\n if (sent.has(topic)) return;\n sent.add(topic);\n client.send(options.subscribe(topic));\n }\n\n function unsubscribe(topic: string) {\n if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;\n if (options.unsubscribe) client.send(options.unsubscribe(topic));\n }\n\n // `open` listeners run before the client flushes its queue.\n const off = client.on('open', () => {\n sentOn = client.socket;\n sent = new Set();\n for (const topic of counts.keys()) subscribe(topic);\n });\n\n return {\n client,\n acquire(topic) {\n const releaseConnection = client.acquire();\n clearTimeout(pendingRemoval.get(topic));\n pendingRemoval.delete(topic);\n counts.set(topic, (counts.get(topic) ?? 0) + 1);\n subscribe(topic);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const remaining = (counts.get(topic) ?? 1) - 1;\n if (remaining > 0) {\n counts.set(topic, remaining);\n } else {\n counts.delete(topic);\n pendingRemoval.set(\n topic,\n setTimeout(() => {\n pendingRemoval.delete(topic);\n if (!counts.has(topic)) unsubscribe(topic);\n }, 0)\n );\n }\n releaseConnection();\n };\n },\n active: () => [...counts.keys()],\n dispose() {\n off();\n for (const timer of pendingRemoval.values()) clearTimeout(timer);\n pendingRemoval.clear();\n }\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/types.ts","../src/client.ts","../src/latency.ts","../src/requests.ts","../src/state.ts","../src/topics.ts"],"sourcesContent":["export { createWebSocket } from './client';\nexport { createWebSocketLatency } from './latency';\nexport type { Latency, WebSocketLatency, WebSocketLatencyOptions } from './latency';\nexport { createWebSocketRequests } from './requests';\nexport type { RequestOptions, WebSocketRequests, WebSocketRequestsOptions } from './requests';\nexport { createWebSocketState } from './state';\nexport type { WebSocketState, WebSocketStateOptions } from './state';\nexport { createWebSocketTopics } from './topics';\nexport type { WebSocketTopics, WebSocketTopicsOptions } from './topics';\nexport { MessageError } from './types';\nexport type {\n CloseInfo,\n EventMap,\n HeartbeatOptions,\n QueueOptions,\n ReconnectOptions,\n SendData,\n SendOptions,\n Snapshot,\n StandardSchemaV1,\n Status,\n UrlSource,\n WebSocketClient,\n WebSocketOptions\n} from './types';\n","/**\n * Where to connect: a URL, or a function that returns one (sync or async).\n *\n * A function is called before **every** connection attempt, including reconnects,\n * so it can put a fresh auth token in the URL. If it throws or rejects, that counts\n * as a failed attempt and is retried with backoff.\n *\n * @example\n * ```ts\n * createWebSocket(async () => `wss://api.example.com/live?token=${await getToken()}`);\n * ```\n */\nexport type UrlSource = string | (() => string | Promise<string>);\n\n/**\n * Connection state.\n *\n * - `idle`: not connected and not trying to be. Nobody is using the client yet, or everyone stopped.\n * - `connecting`: the first attempt is in progress.\n * - `open`: connected. `send()` goes out immediately.\n * - `reconnecting`: the connection dropped (or an attempt failed) and a retry is scheduled or in progress.\n * - `closed`: stopped for good, by `close()` or after giving up on reconnecting.\n * It stays closed until `connect()` or `reconnect()` is called, or every hook unmounts.\n */\nexport type Status = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n/** The client's state as seen by React. A new object is created on every change. */\nexport interface Snapshot<In> {\n /** Connection state. */\n readonly status: Status;\n /** Most recent message (after `parse`/`json` and `schema`), kept across reconnects. */\n readonly lastMessage: In | undefined;\n /** Consecutive failed attempts since the last successful open. `0` while open. */\n readonly attempt: number;\n}\n\n/** Details of a closed connection, from the browser's `CloseEvent`. */\nexport interface CloseInfo {\n /** Close code: `1000` is a normal close, `1006` an abnormal one (e.g. network loss), `4000`–`4999` are yours. */\n readonly code: number;\n /** Close reason sent by the side that closed the connection. Often empty. */\n readonly reason: string;\n /** Whether the closing handshake completed. */\n readonly wasClean: boolean;\n}\n\n/** Anything `WebSocket.prototype.send` accepts: a string, `Blob`, `ArrayBuffer` or typed array. */\nexport type SendData = Parameters<WebSocket['send']>[0];\n\n/** Reconnect timing. Delay before attempt `n` = `min(maxDelay, minDelay * factor ** (n - 1))`, minus jitter. */\nexport interface ReconnectOptions {\n /**\n * Maximum consecutive reconnect attempts. When the last one fails too, the client\n * gives up: the status becomes `closed` and `ready` rejects. `0` never retries.\n * @default Infinity\n */\n maxAttempts?: number;\n /**\n * Delay before the first retry, in ms.\n * @default 1000\n */\n minDelay?: number;\n /**\n * Upper bound for the delay, in ms.\n * @default 30000\n */\n maxDelay?: number;\n /**\n * Backoff multiplier applied per attempt.\n * @default 2\n */\n factor?: number;\n /**\n * Randomly shave up to this fraction (0–1) off each delay, so that many clients\n * dropped at once don't all reconnect at the same moment.\n * @default 0.5\n */\n jitter?: number;\n /**\n * Called on every unexpected close. Return `false` to stop reconnecting.\n *\n * @example\n * ```ts\n * // Don't retry when the server rejects our credentials.\n * shouldReconnect: close => close.code !== 4001\n * ```\n * @default () => true\n */\n shouldReconnect?: (close: CloseInfo) => boolean;\n}\n\n/** Application-level keep-alive. Useful behind proxies that drop idle connections. */\nexport interface HeartbeatOptions {\n /** Send `message` every `interval` ms while open. */\n interval: number;\n /**\n * Sent as-is (it is not passed through `serialize` or `json`).\n * @default 'ping'\n */\n message?: string;\n /** Incoming data equal to this string is swallowed instead of being delivered as a message. */\n pong?: string;\n /**\n * If nothing at all arrives within this many ms after a ping, the connection is\n * treated as dead: it closes with code `4000` and reconnects.\n */\n timeout?: number;\n}\n\n/** Per-message options for `send()`. */\nexport interface SendOptions {\n /**\n * Drop the message instead of queueing it when the socket isn't open. For data that\n * is stale by the time a reconnect finishes, such as player input or cursor positions.\n * @default false\n */\n volatile?: boolean;\n /**\n * Keep only the newest queued message with this key. Sending again with the same key\n * while disconnected replaces the earlier one, which moves to the back of the queue.\n * For state where only the latest value matters, such as \"my position is now (x, y)\".\n */\n key?: string;\n}\n\n/** Bounds for the send queue. */\nexport interface QueueOptions {\n /**\n * Maximum number of messages held while not connected. When full, the oldest\n * message is dropped.\n * @default Infinity\n */\n limit?: number;\n}\n\n/**\n * The minimal [Standard Schema v1](https://standardschema.dev) interface, implemented\n * by zod (3.24+), valibot (1.0+), arktype (2.0+) and others. You don't need to\n * import this; pass your schema object directly.\n */\nexport interface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (\n value: unknown\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n readonly types?: { readonly input: Input; readonly output: Output } | undefined;\n };\n}\n\n/** The result of a Standard Schema `validate` call. */\nexport type StandardSchemaResult<Output> =\n | { readonly value: Output; readonly issues?: undefined }\n | { readonly issues: ReadonlyArray<{ readonly message: string }> };\n\n/**\n * Connection options, shared by `createWebSocket(url, options)` and `useWebSocket(url, options)`.\n *\n * @typeParam In - Type of incoming messages (after `parse`/`json`/`schema`).\n * @typeParam Out - Type accepted by `send()`.\n */\nexport interface WebSocketOptions<In = unknown, Out = unknown> {\n /** Sub-protocol(s) passed to the `WebSocket` constructor. */\n protocols?: string | string[];\n /**\n * How binary messages are exposed.\n * @default 'blob'\n */\n binaryType?: BinaryType;\n /**\n * `JSON.parse` incoming and `JSON.stringify` outgoing messages. Messages that fail\n * to parse are reported to `onError` as a {@link MessageError} and not delivered.\n * @default false\n */\n json?: boolean;\n /**\n * Custom decoder for incoming `event.data`. Takes precedence over `json`.\n * Throwing reports a {@link MessageError} and drops the message.\n */\n parse?: (data: unknown) => unknown;\n /** Custom encoder for outgoing messages. Takes precedence over `json`. */\n serialize?: (message: Out) => SendData;\n /**\n * Validate incoming messages (after parsing) with any Standard Schema library.\n * Invalid messages are reported to `onError` as a {@link MessageError} and not delivered.\n * The message type `In` is inferred from it.\n *\n * @example\n * ```ts\n * createWebSocket(url, { json: true, schema: z.object({ price: z.number() }) });\n * ```\n */\n schema?: StandardSchemaV1<unknown, In>;\n /**\n * Reconnect with exponential backoff after an unexpected close. `false` disables it.\n * @default true\n */\n reconnect?: boolean | ReconnectOptions;\n /** Send a ping on an interval and, optionally, reconnect when the server goes quiet. */\n heartbeat?: HeartbeatOptions;\n /**\n * Buffer `send()` calls made while not connected and send them, in order, on open\n * (right after `open` listeners run).\n * `false` drops them instead. `{ limit }` caps the buffer, dropping the oldest.\n * The queue is cleared by `close()` or when the client gives up.\n * @default true\n */\n queue?: boolean | QueueOptions;\n /**\n * Receives debug messages about connection attempts, retries and dropped messages.\n * @example\n * ```ts\n * logger: console.debug\n * ```\n */\n logger?: (message: string, ...details: unknown[]) => void;\n /**\n * `WebSocket` implementation to use, e.g. a mock in tests or the `ws` package in Node.\n * When omitted, the global `WebSocket` is used in browsers. During a server render\n * (no `window`), nothing connects unless an implementation is passed.\n */\n WebSocket?: typeof WebSocket;\n}\n\n/**\n * Reported via the `error` event (and `onError`) when an incoming message can't be\n * decoded by `parse`/`json` or fails `schema` validation. The message is not delivered.\n *\n * @example\n * ```ts\n * onError(error) {\n * if (error instanceof MessageError) report(error.data, error.issues);\n * }\n * ```\n */\nexport class MessageError extends Error {\n /** The raw `event.data` that failed. */\n readonly data: unknown;\n /** Schema validation issues, when the message failed `schema`. */\n readonly issues: ReadonlyArray<{ readonly message: string }> | undefined;\n\n constructor(\n message: string,\n data: unknown,\n options?: { cause?: unknown; issues?: ReadonlyArray<{ readonly message: string }> }\n ) {\n super(message, options?.cause === undefined ? undefined : { cause: options.cause });\n this.name = 'MessageError';\n this.data = data;\n this.issues = options?.issues;\n }\n}\n\n/** Events emitted by a client. Subscribe with `client.on(type, listener)` or `useWebSocketEvent`. */\nexport interface EventMap<In> {\n /**\n * The socket opened (initially or after a reconnect). Listeners run before queued\n * messages are sent, so a handshake sent from here goes out first.\n */\n open: (event: Event) => void;\n /** A message arrived and passed `parse` and `schema`. */\n message: (message: In, event: MessageEvent) => void;\n /** A socket closed, whether it will be retried or not. */\n close: (close: CloseInfo) => void;\n /**\n * A socket error `Event`, a {@link MessageError} for an undecodable or invalid message,\n * or the `Error` thrown by the `WebSocket` constructor or a `url` function.\n */\n error: (error: Event | MessageError | Error) => void;\n}\n\n/**\n * A WebSocket connection that reconnects, queues, decodes and can be shared.\n * Create one with `createWebSocket()`.\n *\n * The connection is reference-counted: it opens when the first hook mounts (or\n * `ready` is read) and closes one tick after the last hook unmounts. Calling\n * `connect()` keeps it open regardless, until `close()`.\n *\n * @typeParam In - Type of incoming messages.\n * @typeParam Out - Type accepted by `send()`.\n */\nexport interface WebSocketClient<In = unknown, Out = unknown> {\n /** The URL (or URL function) the client was created with. */\n readonly url: UrlSource;\n /**\n * A promise that resolves once the socket has opened. Reading it starts an idle client.\n *\n * It stays resolved through automatic reconnects, so `use(client.ready)` shows a\n * Suspense fallback only for the first connection, never on a network blip; use\n * `status` to show reconnects. It rejects when the client gives up reconnecting, when\n * `close()` is called, and during a server render. After that, `connect()` or\n * `reconnect()` make it a new pending promise.\n *\n * It is safe to pass to React 19's `use()`: the same promise is returned until its state\n * changes, and it carries `status`/`value`/`reason` so `use()` doesn't suspend when it is\n * already settled.\n */\n readonly ready: Promise<void>;\n /** The current underlying `WebSocket`, if any. An escape hatch; prefer the client's methods. */\n readonly socket: WebSocket | null;\n /** Subscribe to snapshot changes. Compatible with `useSyncExternalStore`. Returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** The current {@link Snapshot}. Returns the same object until something changes. */\n getSnapshot(): Snapshot<In>;\n /**\n * Listen to an event. Returns an unsubscribe function. Listener errors are re-thrown\n * asynchronously, so one failing listener doesn't break the others.\n */\n on<K extends keyof EventMap<In>>(type: K, listener: EventMap<In>[K]): () => void;\n /**\n * Send a message (encoded with `serialize`/`json` if configured). If the socket isn't\n * open, the message is queued and sent on open (see the `queue` option), unless\n * `options.volatile` is set.\n * @returns `true` if it was sent immediately, `false` if it was queued or dropped.\n *\n * @example\n * ```ts\n * game.send({ type: 'input', dx, dy }, { volatile: true }); // never replayed after a reconnect\n * game.send({ type: 'position', x, y }, { key: 'position' }); // only the newest is queued\n * ```\n */\n send(message: Out, options?: SendOptions): boolean;\n /**\n * Start connecting, and keep the connection open even when no hook is using the\n * client, until `close()` is called. Does nothing if already connecting or open.\n */\n connect(): void;\n /**\n * Close the connection and stop reconnecting. The status becomes `closed` and the\n * send queue is cleared. Mounted hooks stay mounted; call `connect()` or `reconnect()`\n * to start again.\n * @param code - Close code. @default 1000\n */\n close(code?: number, reason?: string): void;\n /**\n * Drop the current socket (if any) and connect again immediately, resetting the\n * backoff. Also restarts a `closed` client.\n */\n reconnect(): void;\n /**\n * Hold the connection open until the returned function is called. This is what the\n * hooks use; call it yourself to integrate with non-React code.\n * @returns A release function (safe to call more than once).\n */\n acquire(): () => void;\n}\n","import {\n MessageError,\n type CloseInfo,\n type EventMap,\n type ReconnectOptions,\n type SendData,\n type SendOptions,\n type Snapshot,\n type StandardSchemaResult,\n type UrlSource,\n type WebSocketClient,\n type WebSocketOptions\n} from './types';\n\nconst OPEN = 1;\n\nconst RECONNECT_DEFAULTS = {\n maxAttempts: Infinity,\n minDelay: 1000,\n maxDelay: 30_000,\n factor: 2,\n jitter: 0.5\n} satisfies ReconnectOptions;\n\ntype Timer = ReturnType<typeof setTimeout> | undefined;\n\n// React's `use()` reads `status`/`value`/`reason` off a thenable to skip suspending\n// when it is already settled.\ntype TrackedPromise = Promise<void> & {\n status: 'pending' | 'fulfilled' | 'rejected';\n value?: void;\n reason?: unknown;\n};\n\ninterface Deferred {\n readonly promise: TrackedPromise;\n settled: boolean;\n resolve(): void;\n reject(reason: unknown): void;\n}\n\nfunction createDeferred(): Deferred {\n let resolve!: () => void;\n let reject!: (reason: unknown) => void;\n const promise = new Promise<void>((res, rej) => {\n resolve = res;\n reject = rej;\n }) as TrackedPromise;\n promise.status = 'pending';\n // Nobody may be awaiting `ready`; don't surface that as an unhandled rejection.\n promise.catch(() => {});\n const deferred: Deferred = {\n promise,\n settled: false,\n resolve() {\n if (deferred.settled) return;\n deferred.settled = true;\n promise.status = 'fulfilled';\n promise.value = undefined;\n resolve();\n },\n reject(reason) {\n if (deferred.settled) return;\n deferred.settled = true;\n promise.status = 'rejected';\n promise.reason = reason;\n reject(reason);\n }\n };\n return deferred;\n}\n\nfunction rethrowAsync(error: unknown) {\n queueMicrotask(() => {\n throw error;\n });\n}\n\nfunction isThenable<T>(value: unknown): value is PromiseLike<T> {\n return typeof (value as PromiseLike<T> | null)?.then === 'function';\n}\n\n/**\n * @internal Option key for a callback run when the client tears down, which lets the hooks'\n * URL registry forget clients nobody holds anymore. A registered symbol rather than an export,\n * because the hooks entry imports the client from the `core` entry at runtime.\n */\nconst ON_TEARDOWN = Symbol.for('react-websocket.onTeardown');\n\nfunction createClient<In = unknown, Out = unknown>(\n url: UrlSource,\n options: WebSocketOptions<In, Out> & { [ON_TEARDOWN]?: () => void } = {}\n): WebSocketClient<In, Out> {\n const onTeardown = options[ON_TEARDOWN];\n const log = options.logger ?? (() => {});\n const retry =\n options.reconnect === false\n ? null\n : {\n ...RECONNECT_DEFAULTS,\n ...(typeof options.reconnect === 'object' ? options.reconnect : {})\n };\n const queueLimit =\n options.queue === false\n ? 0\n : typeof options.queue === 'object'\n ? Math.max(0, options.queue.limit ?? Infinity)\n : Infinity;\n\n let snapshot: Snapshot<In> = { status: 'idle', lastMessage: undefined, attempt: 0 };\n const storeListeners = new Set<() => void>();\n const listeners: { [K in keyof EventMap<In>]: Set<EventMap<In>[K]> } = {\n open: new Set(),\n message: new Set(),\n close: new Set(),\n error: new Set()\n };\n\n let ws: WebSocket | null = null;\n let wanted = false;\n let refs = 0;\n // Set by a public `connect()`: stay connected after the last hook releases, until `close()`.\n let pinned = false;\n let pending: { data: SendData; key: string | undefined }[] = [];\n let ready: Deferred | null = null;\n // Opened at least once since the client was last started. `ready` stays resolved through\n // automatic reconnects so `use(client.ready)` doesn't swap content for a fallback on every blip.\n let opened = false;\n let unavailable: Deferred | null = null;\n let startScheduled = false;\n let inbox: Promise<void> | null = null;\n // Bumped per connection attempt so a slow `url()` can't resurrect a cancelled one.\n let generation = 0;\n let windowBound = false;\n let retryTimer: Timer;\n let teardownTimer: Timer;\n let heartbeatTimer: ReturnType<typeof setInterval> | undefined;\n let pongTimer: Timer;\n\n function set(patch: Partial<Snapshot<In>>) {\n snapshot = { ...snapshot, ...patch };\n for (const listener of [...storeListeners]) listener();\n }\n\n function emit<K extends keyof EventMap<In>>(type: K, ...args: Parameters<EventMap<In>[K]>) {\n for (const listener of [...listeners[type]]) {\n try {\n (listener as (...a: Parameters<EventMap<In>[K]>) => void)(...args);\n } catch (error) {\n rethrowAsync(error);\n }\n }\n }\n\n function getImpl(): typeof WebSocket | undefined {\n if (options.WebSocket) return options.WebSocket;\n // Node has a global WebSocket too; don't connect from a server render.\n return typeof window === 'undefined' ? undefined : globalThis.WebSocket;\n }\n\n function rejectReady(reason: unknown) {\n if (!ready || ready.settled) ready = createDeferred();\n ready.reject(reason);\n }\n\n function retryNow() {\n if (retryTimer !== undefined) {\n log('network or visibility change, retrying now');\n open();\n }\n }\n\n function onVisibilityChange() {\n if (document.visibilityState === 'visible') retryNow();\n }\n\n function bindWindow() {\n if (windowBound || typeof window === 'undefined') return;\n windowBound = true;\n window.addEventListener('online', retryNow);\n document.addEventListener('visibilitychange', onVisibilityChange);\n }\n\n function unbindWindow() {\n if (!windowBound) return;\n windowBound = false;\n window.removeEventListener('online', retryNow);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n\n function startHeartbeat(socket: WebSocket) {\n const heartbeat = options.heartbeat;\n if (!heartbeat) return;\n heartbeatTimer = setInterval(() => {\n socket.send(heartbeat.message ?? 'ping');\n if (heartbeat.timeout && pongTimer === undefined) {\n pongTimer = setTimeout(() => {\n log('heartbeat timeout');\n // The close handshake may never finish on a dead link, so don't wait for it.\n detach(socket, false);\n socket.close(4000, 'heartbeat timeout');\n handleClose({ code: 4000, reason: 'heartbeat timeout', wasClean: false });\n }, heartbeat.timeout);\n }\n }, heartbeat.interval);\n }\n\n function stopHeartbeat() {\n clearInterval(heartbeatTimer);\n clearTimeout(pongTimer);\n heartbeatTimer = pongTimer = undefined;\n }\n\n /** Stop tracking `socket`. Its eventual close event is still reported unless `reportClose` is false. */\n function detach(socket: WebSocket, reportClose: boolean) {\n if (ws === socket) ws = null;\n socket.onopen = socket.onmessage = socket.onerror = null;\n socket.onclose = reportClose\n ? event => emit('close', { code: event.code, reason: event.reason, wasClean: event.wasClean })\n : null;\n }\n\n function backoff(attempt: number) {\n const { minDelay, maxDelay, factor, jitter } = retry!;\n const base = Math.min(maxDelay, minDelay * factor ** (attempt - 1));\n return Math.round(base * (1 - jitter * Math.random()));\n }\n\n function open() {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n const Impl = getImpl();\n if (!Impl) return;\n const id = ++generation;\n set({ status: snapshot.attempt === 0 ? 'connecting' : 'reconnecting' });\n\n let target: string | Promise<string>;\n try {\n target = typeof url === 'string' ? url : url();\n } catch (error) {\n urlFailed(error);\n return;\n }\n if (typeof target === 'string') {\n start(Impl, target);\n return;\n }\n Promise.resolve(target).then(\n resolved => {\n if (id === generation && wanted) start(Impl, resolved);\n },\n error => {\n if (id === generation && wanted) urlFailed(error);\n }\n );\n }\n\n /** `url()` threw or rejected (e.g. a token refresh failed): count it as a failed attempt. */\n function urlFailed(error: unknown) {\n const reason = error instanceof Error ? error : new Error(String(error));\n log('url() failed', reason);\n emit('error', reason);\n retryOrFail({ code: 1006, reason: 'url() failed', wasClean: false }, reason);\n }\n\n function start(Impl: typeof WebSocket, target: string) {\n let socket: WebSocket;\n try {\n socket = new Impl(target, options.protocols);\n } catch (error) {\n // e.g. an invalid URL; retrying won't help.\n emit('error', error as Error);\n fail(error);\n return;\n }\n if (options.binaryType) socket.binaryType = options.binaryType;\n ws = socket;\n log('connecting', target);\n\n socket.onopen = event => {\n log('open');\n opened = true;\n set({ status: 'open', attempt: 0 });\n startHeartbeat(socket);\n ready?.resolve();\n // `open` listeners go first, so a handshake sent from `onOpen` (e.g. auth) precedes\n // whatever was queued while disconnected.\n emit('open', event);\n if (ws !== socket) return; // a listener closed or replaced it\n const queued = pending;\n pending = [];\n for (const { data } of queued) socket.send(data);\n };\n socket.onmessage = event => receive(event, socket);\n socket.onerror = event => emit('error', event);\n socket.onclose = event => {\n detach(socket, false);\n handleClose({ code: event.code, reason: event.reason, wasClean: event.wasClean });\n };\n }\n\n function handleClose(info: CloseInfo) {\n stopHeartbeat();\n log('closed', info);\n retryOrFail(info);\n emit('close', info);\n }\n\n function retryOrFail(info: CloseInfo, error?: Error) {\n if (\n wanted &&\n retry &&\n snapshot.attempt < retry.maxAttempts &&\n (retry.shouldReconnect?.(info) ?? true)\n ) {\n const attempt = snapshot.attempt + 1;\n const delay = backoff(attempt);\n set({ status: 'reconnecting', attempt });\n log(`reconnecting in ${delay}ms (attempt ${attempt})`);\n retryTimer = setTimeout(open, delay);\n } else {\n fail(\n error ??\n new Error(`WebSocket closed (code ${info.code}${info.reason ? `: ${info.reason}` : ''})`)\n );\n }\n }\n\n function fail(reason: unknown) {\n wanted = false;\n opened = false;\n unbindWindow();\n pending = [];\n set({ status: 'closed' });\n rejectReady(reason);\n }\n\n function receive(event: MessageEvent, socket: WebSocket) {\n clearTimeout(pongTimer);\n pongTimer = undefined;\n if (options.heartbeat?.pong !== undefined && event.data === options.heartbeat.pong) return;\n\n // Async schema validation must not reorder messages.\n const run = () => decode(event, socket);\n if (inbox) {\n const next: Promise<void> = inbox.then(run).then(() => {\n if (inbox === next) inbox = null;\n });\n inbox = next;\n return;\n }\n const result = run();\n if (result) {\n const next: Promise<void> = result.then(() => {\n if (inbox === next) inbox = null;\n });\n inbox = next;\n }\n }\n\n function decode(event: MessageEvent, socket: WebSocket): void | Promise<void> {\n let value: unknown;\n try {\n value = options.parse\n ? options.parse(event.data)\n : options.json\n ? JSON.parse(event.data as string)\n : event.data;\n } catch (cause) {\n emit('error', new MessageError('Failed to parse message', event.data, { cause }));\n return;\n }\n\n const schema = options.schema;\n if (!schema) return deliver(value as In, event, socket);\n\n let result: ReturnType<(typeof schema)['~standard']['validate']>;\n try {\n result = schema['~standard'].validate(value);\n } catch (cause) {\n emit('error', new MessageError('Schema validation threw', event.data, { cause }));\n return;\n }\n if (isThenable<StandardSchemaResult<In>>(result)) {\n return Promise.resolve(result).then(\n r => validated(r, event, socket),\n cause => emit('error', new MessageError('Schema validation threw', event.data, { cause }))\n );\n }\n validated(result, event, socket);\n }\n\n function validated(result: StandardSchemaResult<In>, event: MessageEvent, socket: WebSocket) {\n if (result.issues) {\n emit(\n 'error',\n new MessageError('Message failed schema validation', event.data, { issues: result.issues })\n );\n } else {\n deliver(result.value, event, socket);\n }\n }\n\n function deliver(message: In, event: MessageEvent, socket: WebSocket) {\n if (ws !== socket) return; // closed or replaced while validating\n set({ lastMessage: message });\n emit('message', message, event);\n }\n\n function send(message: Out, sendOptions?: SendOptions): boolean {\n const data = options.serialize\n ? options.serialize(message)\n : options.json\n ? JSON.stringify(message)\n : (message as SendData);\n if (ws && ws.readyState === OPEN) {\n ws.send(data);\n return true;\n }\n if (queueLimit === 0 || sendOptions?.volatile) {\n log('dropped message: socket is not open');\n return false;\n }\n if (!getImpl()) {\n // e.g. during a server render: this client can never connect, so don't hold on to it.\n log('dropped message: no WebSocket implementation available');\n return false;\n }\n const key = sendOptions?.key;\n if (key !== undefined) pending = pending.filter(entry => entry.key !== key);\n pending.push({ data, key });\n if (pending.length > queueLimit) {\n pending.shift();\n log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);\n }\n return false;\n }\n\n function begin() {\n wanted = true;\n // Starting over after `close()` or giving up: wait for a new connection.\n if (snapshot.status === 'idle' || snapshot.status === 'closed') {\n opened = false;\n if (ready?.settled) ready = null;\n }\n snapshot = { ...snapshot, attempt: 0 }; // open() notifies\n bindWindow();\n open();\n }\n\n /** Start connecting unless already wanted. Used by holders (`acquire`, `ready`). */\n function ensureConnecting() {\n if (wanted || !getImpl()) return;\n begin();\n }\n\n function connect() {\n pinned = true;\n ensureConnecting();\n }\n\n function stop(code?: number, reason?: string) {\n wanted = false;\n generation++;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n stopHeartbeat();\n unbindWindow();\n pending = [];\n if (ws) {\n const socket = ws;\n detach(socket, true);\n socket.close(code, reason);\n }\n }\n\n function close(code = 1000, reason?: string) {\n pinned = false;\n opened = false;\n stop(code, reason);\n set({ status: 'closed', attempt: 0 });\n rejectReady(new Error('WebSocket was closed'));\n }\n\n function reconnect() {\n if (!getImpl()) return;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n stopHeartbeat();\n if (ws) {\n const socket = ws;\n detach(socket, true);\n socket.close(1000, 'reconnect');\n }\n begin();\n }\n\n function teardown() {\n if (refs > 0 || pinned) return;\n // Someone may be suspended on `ready`; let the attempt finish.\n if (ready && !ready.settled) return;\n stop();\n ready = null;\n opened = false;\n set({ status: 'idle', attempt: 0 });\n onTeardown?.();\n }\n\n function acquire() {\n refs++;\n clearTimeout(teardownTimer);\n if (snapshot.status === 'idle') ensureConnecting();\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--refs > 0) return;\n // Deferred so StrictMode's unmount/remount and route transitions don't churn the socket.\n clearTimeout(teardownTimer);\n teardownTimer = setTimeout(teardown, 0);\n };\n }\n\n return {\n url,\n get ready() {\n if (!getImpl()) {\n if (!unavailable) {\n unavailable = createDeferred();\n unavailable.reject(\n new Error('react-websocket: no WebSocket available here (server render?)')\n );\n }\n return unavailable.promise;\n }\n if (snapshot.status === 'idle' && !startScheduled) {\n // `ready` is read during render; start outside of it.\n startScheduled = true;\n queueMicrotask(() => {\n startScheduled = false;\n if (snapshot.status === 'idle') ensureConnecting();\n });\n }\n if (!ready) {\n ready = createDeferred();\n if (opened) ready.resolve();\n }\n return ready.promise;\n },\n get socket() {\n return ws;\n },\n subscribe(listener) {\n storeListeners.add(listener);\n return () => {\n storeListeners.delete(listener);\n };\n },\n getSnapshot: () => snapshot,\n on(type, listener) {\n // Wrap so the same function can be subscribed twice and unsubscribed independently.\n const entry = ((...args: unknown[]) =>\n (listener as (...a: unknown[]) => void)(...args)) as typeof listener;\n listeners[type].add(entry);\n return () => {\n listeners[type].delete(entry);\n };\n },\n send,\n connect,\n close,\n reconnect,\n acquire\n };\n}\n\n/**\n * Create a WebSocket client, usually at module level, to share across components.\n *\n * Nothing connects until a hook uses it, `ready` is read, or `connect()` is called.\n * It closes one tick after the last hook unmounts, unless `connect()` was called.\n *\n * @param url - A URL, or a function (sync or async) called before every connection\n * attempt, e.g. to put a fresh auth token in the URL on each reconnect.\n * @typeParam In - Type of incoming messages. Inferred from `options.schema` if given.\n * @typeParam Out - Type accepted by `send()`.\n *\n * @example\n * ```ts\n * type ChatIn = { user: string; text: string };\n * type ChatOut = { text: string };\n *\n * export const chat = createWebSocket<ChatIn, ChatOut>('wss://api.example.com/chat', { json: true });\n * ```\n */\nexport function createWebSocket<In = unknown, Out = unknown>(\n url: UrlSource,\n options?: WebSocketOptions<In, Out>\n): WebSocketClient<In, Out> {\n return createClient(url, options);\n}\n","import type { WebSocketClient } from './types';\n\n/** Options for {@link createWebSocketLatency}: how pings and pongs look on the wire. */\nexport interface WebSocketLatencyOptions<In, Out> {\n /** The ping message, tagged with `id`. Sent with `volatile`, so pings are never queued. */\n ping: (id: number) => Out;\n /**\n * Recognize a pong: return the `id` of the ping it answers, and the server's clock\n * (`Date.now()` on the server, in ms) when it replied. Return `undefined` (or `null`)\n * for any other message. Without `time`, only the round-trip time is measured.\n */\n pong: (message: In) => { id: number | string; time?: number } | null | undefined;\n /**\n * How often to ping while open, in ms. The first ping goes out as soon as the socket opens.\n * @default 2000\n */\n interval?: number;\n /**\n * How many recent measurements to keep. `rtt` is their median, and `offset` comes from\n * the one with the lowest round trip, the most accurate estimate.\n * @default 5\n */\n samples?: number;\n}\n\n/** Latest latency measurements. A new object is created on every change. */\nexport interface Latency {\n /** Round-trip time in ms: the median of recent pings. `undefined` until the first pong. */\n readonly rtt: number | undefined;\n /**\n * Estimated server clock minus local clock, in ms. Add it to `Date.now()` for server time.\n * `undefined` until a pong carries `time`.\n */\n readonly offset: number | undefined;\n}\n\n/** Round-trip time and server clock offset for one client. Create with {@link createWebSocketLatency}. */\nexport interface WebSocketLatency {\n /** The client being measured. Hooks reading the latency keep it connected. */\n readonly client: WebSocketClient<any, any>;\n /** The current {@link Latency}. Returns the same object until it changes. */\n getSnapshot(): Latency;\n /** Called whenever a measurement changes. Compatible with `useSyncExternalStore`. */\n subscribe(listener: () => void): () => void;\n /** The estimated server time: `Date.now() + offset`, or `Date.now()` before any estimate. */\n serverNow(): number;\n /** Ping now instead of waiting for the next interval. Does nothing unless the socket is open. */\n ping(): void;\n /** Stop pinging and listening. Only needed for instances created dynamically. */\n dispose(): void;\n}\n\nconst EMPTY: Latency = Object.freeze({ rtt: undefined, offset: undefined });\n\nfunction median(values: number[]) {\n const sorted = [...values].sort((a, b) => a - b);\n const middle = sorted.length >> 1;\n return sorted.length % 2 ? sorted[middle]! : (sorted[middle - 1]! + sorted[middle]!) / 2;\n}\n\n/**\n * Measure round-trip time and estimate the server's clock, for ping displays, countdowns\n * and timestamps that must agree across players.\n *\n * Pings are sent on an interval while the socket is open, and paused otherwise. Measurements\n * from a previous connection are discarded on reconnect, but the last values stay readable\n * until new ones arrive. It doesn't open the connection by itself.\n *\n * Pongs are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.\n *\n * @example\n * ```ts\n * export const latency = createWebSocketLatency(game, {\n * ping: id => ({ type: 'ping', id }),\n * pong: message => (message.type === 'pong' ? { id: message.id, time: message.time } : undefined)\n * });\n *\n * // In a component:\n * const { rtt } = useWebSocketLatency(latency);\n *\n * // Anywhere: seconds left in a round that ends at the server's `endsAt`.\n * const left = (endsAt - latency.serverNow()) / 1000;\n * ```\n */\n// `const` + `NoInfer`: take `In` and `Out` from the client only, and keep literals such as\n// `type: 'ping'` from widening to `string` inside the callbacks.\nexport function createWebSocketLatency<In, const Out>(\n client: WebSocketClient<In, Out>,\n options: WebSocketLatencyOptions<NoInfer<In>, NoInfer<Out>>\n): WebSocketLatency {\n const interval = options.interval ?? 2000;\n const keep = Math.max(1, options.samples ?? 5);\n const listeners = new Set<() => void>();\n let snapshot = EMPTY;\n // Ping id -> `performance.now()` when sent. Pongs that never come are evicted oldest first.\n const inFlight = new Map<string, number>();\n let samples: { rtt: number; offset: number | undefined }[] = [];\n let nextId = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n\n function ping() {\n if (client.getSnapshot().status !== 'open') return;\n const id = ++nextId;\n inFlight.set(String(id), performance.now());\n if (inFlight.size > keep * 2) inFlight.delete(inFlight.keys().next().value!);\n client.send(options.ping(id), { volatile: true });\n }\n\n function start() {\n stop();\n inFlight.clear();\n samples = [];\n ping();\n timer = setInterval(ping, interval);\n }\n\n function stop() {\n clearInterval(timer);\n timer = undefined;\n }\n\n function measure(id: string, serverTime: number | undefined) {\n const sentAt = inFlight.get(id);\n if (sentAt === undefined) return;\n inFlight.delete(id);\n const rtt = performance.now() - sentAt;\n // The server read its clock about halfway through the round trip.\n const offset = serverTime === undefined ? undefined : serverTime + rtt / 2 - Date.now();\n samples.push({ rtt, offset });\n if (samples.length > keep) samples.shift();\n\n let best: (typeof samples)[number] | undefined;\n for (const sample of samples) {\n if (sample.offset !== undefined && (!best || sample.rtt < best.rtt)) best = sample;\n }\n snapshot = {\n rtt: median(samples.map(sample => sample.rtt)),\n offset: best ? best.offset : snapshot.offset\n };\n for (const listener of [...listeners]) listener();\n }\n\n const offs = [\n client.on('open', start),\n client.on('message', message => {\n const pong = options.pong(message);\n if (pong != null) measure(String(pong.id), pong.time);\n }),\n client.subscribe(() => {\n if (client.getSnapshot().status !== 'open') stop();\n })\n ];\n if (client.getSnapshot().status === 'open') start();\n\n return {\n client,\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n serverNow: () => Date.now() + (snapshot.offset ?? 0),\n ping,\n dispose() {\n stop();\n offs.forEach(off => off());\n }\n };\n}\n","import type { SendOptions, WebSocketClient } from './types';\n\n/** Options for {@link createWebSocketRequests}: how requests and replies look on the wire. */\nexport interface WebSocketRequestsOptions<Req, In, Out> {\n /**\n * Build the outgoing message for `request(message)`, tagged with `id`. Annotate `message`\n * to type what `request()` accepts.\n */\n send: (message: Req, id: number) => Out;\n /**\n * The id an incoming message replies to, or `undefined` (or `null`) if it isn't a reply.\n * A string id matches the number it was sent as, so either echo works.\n */\n reply: (message: In) => number | string | null | undefined;\n /**\n * Called on a reply. Return something other than `undefined` to reject the request with\n * it instead of resolving, e.g. for a server-side error reply.\n */\n error?: (message: In) => unknown;\n /**\n * Default time to wait for a reply, in ms. `Infinity` waits forever.\n * @default 10000\n */\n timeout?: number;\n}\n\n/** Per-call options for {@link WebSocketRequests.request}. */\nexport interface RequestOptions extends SendOptions {\n /** Time to wait for a reply, in ms. Overrides the manager's `timeout`. */\n timeout?: number;\n /** Abort the request. It rejects with `signal.reason`. A late reply is ignored. */\n signal?: AbortSignal;\n}\n\n/** Request/response over one client. Create with {@link createWebSocketRequests}. */\nexport interface WebSocketRequests<Req, In> {\n /** The client requests are sent on. */\n readonly client: WebSocketClient<any, any>;\n /**\n * Send a request and resolve with its reply. Holds the connection until it settles.\n *\n * Rejects with a `TimeoutError` `DOMException` after `timeout`, with `signal.reason` when\n * aborted, and with an `Error` when the connection it was sent on closes (the reply can\n * no longer arrive) or the client stops. A request made while disconnected is queued like\n * any other message and waits for the connection.\n */\n request(message: Req, options?: RequestOptions): Promise<In>;\n /** Number of requests waiting for a reply. */\n pending(): number;\n /** Reject every pending request and stop listening. Only needed for managers created dynamically. */\n dispose(): void;\n}\n\ninterface Pending<In> {\n resolve(reply: In): void;\n reject(reason: unknown): void;\n /** The socket the request went out on, or `null` while it waits in the client's queue. */\n socket: WebSocket | null;\n}\n\n/**\n * Send requests and await their replies, matched by an id the library generates.\n *\n * Replies are ordinary messages: they also reach `onMessage`, `lastMessage` and state stores.\n *\n * @example\n * ```ts\n * type Request = { type: 'join'; room: string } | { type: 'leave' };\n *\n * export const rpc = createWebSocketRequests(game, {\n * send: (message: Request, id) => ({ ...message, id }),\n * reply: message => (message.type === 'reply' ? message.id : undefined),\n * error: message => (message.type === 'reply' && message.error ? new Error(message.error) : undefined)\n * });\n *\n * const reply = await rpc.request({ type: 'join', room: 'lobby' }, { timeout: 5000 });\n * ```\n */\n// `const` + `NoInfer`: `In` and `Out` come from the client only, with literals kept unwidened;\n// `Req` comes from how `send` annotates its parameter.\nexport function createWebSocketRequests<Req, In, const Out>(\n client: WebSocketClient<In, Out>,\n options: WebSocketRequestsOptions<Req, NoInfer<In>, NoInfer<Out>>\n): WebSocketRequests<Req, In> {\n const waiting = new Map<string, Pending<In>>();\n let nextId = 0;\n\n function rejectWhere(test: (entry: Pending<In>) => boolean, reason: () => unknown) {\n for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());\n }\n\n const offs = [\n client.on('message', message => {\n const id = options.reply(message);\n const entry = id == null ? undefined : waiting.get(String(id));\n if (!entry) return;\n const error = options.error?.(message);\n if (error === undefined) entry.resolve(message);\n else entry.reject(error);\n }),\n // `open` listeners run before the client flushes its queue: queued requests go out on this socket.\n client.on('open', () => {\n for (const entry of waiting.values()) entry.socket ??= client.socket;\n }),\n client.subscribe(() => {\n const { status } = client.getSnapshot();\n if (status === 'closed' || status === 'idle') {\n // The queue was cleared too, so nothing still waiting can be sent.\n rejectWhere(\n () => true,\n () => new Error('WebSocket was closed before the request was answered')\n );\n } else {\n rejectWhere(\n entry => entry.socket !== null && entry.socket !== client.socket,\n () => new Error('WebSocket connection was lost before the request was answered')\n );\n }\n })\n ];\n\n return {\n client,\n request(message, requestOptions = {}) {\n const { signal } = requestOptions;\n if (signal?.aborted) return Promise.reject(signal.reason);\n\n const id = ++nextId;\n const key = String(id);\n const timeout = requestOptions.timeout ?? options.timeout ?? 10_000;\n\n return new Promise<In>((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const release = client.acquire();\n const onAbort = () => entry.reject(signal!.reason);\n\n function settle() {\n waiting.delete(key);\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n release();\n }\n\n const entry: Pending<In> = {\n socket: null,\n resolve(reply) {\n settle();\n resolve(reply);\n },\n reject(reason) {\n settle();\n reject(reason);\n }\n };\n waiting.set(key, entry);\n signal?.addEventListener('abort', onAbort, { once: true });\n if (timeout > 0 && timeout < Infinity) {\n timer = setTimeout(\n () =>\n entry.reject(\n new DOMException(`Request ${id} timed out after ${timeout}ms`, 'TimeoutError')\n ),\n timeout\n );\n }\n\n const { status } = client.getSnapshot();\n if (status === 'closed' || status === 'idle') {\n // Closed until someone reconnects it, or (idle after acquiring) it can't connect here.\n entry.reject(new Error(`WebSocket is ${status}: request not sent`));\n return;\n }\n let sent: boolean;\n try {\n sent = client.send(options.send(message, id), requestOptions);\n } catch (error) {\n entry.reject(error);\n return;\n }\n if (sent) entry.socket = client.socket;\n else if (requestOptions.volatile)\n entry.reject(new Error('WebSocket is not open: request dropped'));\n });\n },\n pending: () => waiting.size,\n dispose() {\n offs.forEach(off => off());\n rejectWhere(\n () => true,\n () => new Error('Requests were disposed')\n );\n }\n };\n}\n","import type { WebSocketClient } from './types';\n\n/** Options for {@link createWebSocketState}. */\nexport interface WebSocketStateOptions<S, In> {\n /** The state before any message arrives, and what `reset()` returns to. */\n initialState: S;\n /**\n * Fold one incoming message into the state. Must be pure: return a new object for a\n * change, or the same `state` to ignore the message (subscribers aren't notified).\n */\n reduce: (state: S, message: In) => S;\n}\n\n/**\n * State derived from a client's messages. Read it with `useWebSocketState(store, selector)`,\n * or with `getState`/`subscribe` outside React.\n *\n * @typeParam S - The state type. Don't use a function type: `setState` treats functions as updaters.\n */\nexport interface WebSocketState<S> {\n /** The client whose messages this store reduces. Hooks reading the store keep it connected. */\n readonly client: WebSocketClient<any, any>;\n /** The `initialState` it was created with. Used for server rendering and hydration. */\n readonly initialState: S;\n /** The current state. Returns the same object until it changes. */\n getState(): S;\n /** Called whenever the state changes. Compatible with `useSyncExternalStore`. */\n subscribe(listener: () => void): () => void;\n /** Replace the state, or update it from the current one, e.g. for optimistic updates. */\n setState(update: S | ((state: S) => S)): void;\n /** Go back to `initialState`. */\n reset(): void;\n /** Stop reducing messages. Only needed for stores created dynamically rather than at module level. */\n dispose(): void;\n}\n\n/**\n * Keep state derived from a client's messages, like a reducer that runs on every message.\n * It starts listening immediately and keeps the state across reconnects; it doesn't open\n * the connection by itself.\n *\n * @example\n * ```ts\n * const prices = createWebSocket<Tick>('wss://api.example.com/prices', { json: true });\n *\n * export const priceState = createWebSocketState(prices, {\n * initialState: {} as Record<string, number>,\n * reduce: (state, tick) => ({ ...state, [tick.symbol]: tick.price })\n * });\n *\n * // In a component: re-renders only when ACME's price changes.\n * const acme = useWebSocketState(priceState, state => state.ACME);\n * ```\n */\nexport function createWebSocketState<S, In>(\n client: WebSocketClient<In, any>,\n // `In` comes from the client only, not from how `reduce` annotates its parameter.\n options: WebSocketStateOptions<S, NoInfer<In>>\n): WebSocketState<S> {\n const { initialState, reduce } = options;\n let state = initialState;\n const listeners = new Set<() => void>();\n\n function setState(update: S | ((state: S) => S)) {\n const next = typeof update === 'function' ? (update as (state: S) => S)(state) : update;\n if (Object.is(next, state)) return;\n state = next;\n for (const listener of [...listeners]) listener();\n }\n\n // A throwing reducer leaves the state unchanged; the client re-throws the error asynchronously.\n const off = client.on('message', message => setState(current => reduce(current, message)));\n\n return {\n client,\n initialState,\n getState: () => state,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n setState,\n reset: () => setState(initialState),\n dispose: off\n };\n}\n","import type { WebSocketClient } from './types';\n\ntype Timer = ReturnType<typeof setTimeout>;\n\n/** Options for {@link createWebSocketTopics}: the messages your server expects. */\nexport interface WebSocketTopicsOptions<Out> {\n /** The message that subscribes to `topic`. */\n subscribe: (topic: string) => Out;\n /** The message that unsubscribes from `topic`. Omit it if your server has none. */\n unsubscribe?: (topic: string) => Out;\n}\n\n/** Reference-counted topic subscriptions on one client. Create with {@link createWebSocketTopics}. */\nexport interface WebSocketTopics {\n /** The client the subscription messages are sent on. */\n readonly client: WebSocketClient<any, any>;\n /**\n * Hold a subscription to `topic`, and the client's connection, until the returned\n * function is called. The release function is safe to call more than once.\n */\n acquire(topic: string): () => void;\n /** The topics currently held, in the order they were first acquired. */\n active(): string[];\n /** Stop managing subscriptions. Only needed for managers created dynamically. */\n dispose(): void;\n}\n\n/**\n * Manage topic subscriptions that survive reconnects.\n *\n * - The first holder of a topic sends `subscribe(topic)`. Later holders share it.\n * - A tick after the last holder releases it, `unsubscribe(topic)` is sent, so StrictMode\n * and route transitions don't churn subscriptions.\n * - After every reconnect, every held topic is subscribed again on the new socket, before\n * messages queued while disconnected are sent.\n *\n * Subscriptions are only sent while the socket is open; nothing is queued for them.\n *\n * @example\n * ```ts\n * export const topics = createWebSocketTopics(live, {\n * subscribe: topic => ({ type: 'subscribe', topic }),\n * unsubscribe: topic => ({ type: 'unsubscribe', topic })\n * });\n *\n * // In a component:\n * useWebSocketTopic(topics, `prices:${symbol}`);\n * ```\n */\n// `const` + `NoInfer`: take `Out` from the client only, and keep literals such as\n// `type: 'subscribe'` from widening to `string` inside the callbacks.\nexport function createWebSocketTopics<const Out>(\n client: WebSocketClient<any, Out>,\n options: WebSocketTopicsOptions<NoInfer<Out>>\n): WebSocketTopics {\n const counts = new Map<string, number>();\n const pendingRemoval = new Map<string, Timer>();\n // Topics subscribed on `sentOn`. A new socket starts with none, whatever the old one had.\n let sentOn: WebSocket | null = null;\n let sent = new Set<string>();\n\n function isOpen() {\n return client.getSnapshot().status === 'open' && client.socket !== null;\n }\n\n function subscribe(topic: string) {\n if (!isOpen()) return; // sent on `open` instead\n if (client.socket !== sentOn) {\n sentOn = client.socket;\n sent = new Set();\n }\n if (sent.has(topic)) return;\n sent.add(topic);\n client.send(options.subscribe(topic));\n }\n\n function unsubscribe(topic: string) {\n if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;\n if (options.unsubscribe) client.send(options.unsubscribe(topic));\n }\n\n // `open` listeners run before the client flushes its queue.\n const off = client.on('open', () => {\n sentOn = client.socket;\n sent = new Set();\n for (const topic of counts.keys()) subscribe(topic);\n });\n\n return {\n client,\n acquire(topic) {\n const releaseConnection = client.acquire();\n clearTimeout(pendingRemoval.get(topic));\n pendingRemoval.delete(topic);\n counts.set(topic, (counts.get(topic) ?? 0) + 1);\n subscribe(topic);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const remaining = (counts.get(topic) ?? 1) - 1;\n if (remaining > 0) {\n counts.set(topic, remaining);\n } else {\n counts.delete(topic);\n pendingRemoval.set(\n topic,\n setTimeout(() => {\n pendingRemoval.delete(topic);\n if (!counts.has(topic)) unsubscribe(topic);\n }, 0)\n );\n }\n releaseConnection();\n };\n },\n active: () => [...counts.keys()],\n dispose() {\n off();\n for (const timer of pendingRemoval.values()) clearTimeout(timer);\n pendingRemoval.clear();\n }\n };\n}\n"],"mappings":"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","names":[]}
|