@maxtroost/use-websocket 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { Store as e, useStore as t } from "@tanstack/react-store";
2
- import { Store as n } from "@tanstack/store";
3
- import { deepEqual as r } from "fast-equals";
4
- import { createContext as i, useContext as a, useEffect as o, useId as s, useRef as c, useState as l } from "react";
5
- import { useIsomorphicLayoutEffect as u } from "usehooks-ts";
2
+ import { createContext as n, useContext as r, useEffect as i, useId as a, useLayoutEffect as o, useRef as s, useState as c } from "react";
3
+ import { Store as l } from "@tanstack/store";
4
+ import { deepEqual as u } from "fast-equals";
6
5
  import { jsx as d } from "react/jsx-runtime";
7
6
  //#region src/lib/types.ts
8
7
  var f = /* @__PURE__ */ function(e) {
@@ -45,15 +44,15 @@ var m = {
45
44
  }, g = 1e3, _ = { PONG_TIMEOUT_MS: 1e4 }, v = { enabled: !0 }, y = {
46
45
  enabled: !0,
47
46
  pongTimeoutMs: _.PONG_TIMEOUT_MS
48
- }, b = (e) => new Promise((t) => setTimeout(t, e)), x = (e) => Array.from(e).filter(([, e]) => "uri" in e).map(([, e]) => e.uri), S = (e, t, n) => e < n.first ? t.firstPhase : e < n.second ? t.secondPhase : t.thirdPhase, C = () => 40 * 1e3, w = (e) => typeof e == "object" && !!e && "uri" in e && typeof e.uri == "string", T = (e) => e ? [
47
+ }, b = (e) => new Promise((t) => setTimeout(t, e)), x = typeof window < "u" ? o : i, S = (e) => Array.from(e).filter(([, e]) => "uri" in e).map(([, e]) => e.uri), C = (e, t, n) => e < n.first ? t.firstPhase : e < n.second ? t.secondPhase : t.thirdPhase, w = () => 40 * 1e3, T = (e) => typeof e == "object" && !!e && "uri" in e && typeof e.uri == "string", E = (e) => e ? [
49
48
  "error",
50
49
  "conflict",
51
50
  "exception"
52
- ].includes(e) : !1, E = () => typeof window < "u" && window.navigator.onLine, D = (e) => typeof window < "u" && window.navigator.onLine && e !== void 0 && e.readyState === WebSocket.OPEN, O = () => ({
51
+ ].includes(e) : !1, D = () => typeof window < "u" && window.navigator.onLine, O = (e) => typeof window < "u" && window.navigator.onLine && e !== void 0 && e.readyState === WebSocket.OPEN, k = () => ({
53
52
  method: "post",
54
53
  uri: "ping",
55
54
  body: Date.now()
56
- }), k = (e) => e?.readyState === WebSocket.OPEN || e?.readyState === WebSocket.CONNECTING, A = (e) => e !== m.NORMAL_CLOSURE, j = class {
55
+ }), A = (e) => e?.readyState === WebSocket.OPEN || e?.readyState === WebSocket.CONNECTING, j = (e) => e !== m.NORMAL_CLOSURE, M = class {
57
56
  constructor(e, t) {
58
57
  this._listeners = /* @__PURE__ */ new Map(), this.reconnectTries = 0, this._isReconnecting = !1, this._maxRetriesExceeded = !1, this.cachedMessages = [], this.getSocket = () => this._socket, this.addListener = (e) => (e.setSendToConnection(this.handleSendMessage), this.connect(), this._listeners.set(e.key, e), clearTimeout(this.closeConnectionTimeOut), this._socket?.readyState === WebSocket.OPEN && e.onOpen && e.onOpen(), e), this.removeListener = (e) => {
59
58
  let t = this._listeners.get(e.key);
@@ -71,11 +70,11 @@ var m = {
71
70
  this.reconnectTries = 0, this._maxRetriesExceeded = !1, this.connect();
72
71
  }, this.connect = () => {
73
72
  let e = Array.from(this._listeners.values()).some((e) => e.isEnabled);
74
- k(this._socket) || !e || (this._client.connectionEvent?.({
73
+ A(this._socket) || !e || (this._client.connectionEvent?.({
75
74
  type: "connect",
76
75
  url: this._url,
77
76
  retries: this.reconnectTries,
78
- uriApis: x(this._listeners)
77
+ uriApis: S(this._listeners)
79
78
  }), this._socket = new WebSocket(this._url), this._socket.addEventListener("close", this.handleClose), this._socket.addEventListener("message", this.handleMessage), this._socket.addEventListener("open", this.handleOpen), this._socket.addEventListener("error", this.handleError));
80
79
  }, this.teardownSocket = () => {
81
80
  this.clearAllTimers(), this.removeListeners(), this._socket?.close(), this._socket = void 0;
@@ -113,7 +112,7 @@ var m = {
113
112
  retries: this.reconnectTries
114
113
  }), await b(r), this.deferReconnectionUntilOnline())) return;
115
114
  this.reconnectTries++;
116
- let i = S(this.reconnectTries, this._client.delays, this._client.phaseThresholds);
115
+ let i = C(this.reconnectTries, this._client.delays, this._client.phaseThresholds);
117
116
  this.reconnectTries > n && this._client.connectionEvent?.({
118
117
  type: "reconnecting",
119
118
  url: this._url,
@@ -123,7 +122,7 @@ var m = {
123
122
  url: this._url,
124
123
  retries: this.reconnectTries
125
124
  }), this.connect());
126
- }, this.deferReconnectionUntilOnline = () => E() ? !1 : (typeof window < "u" && window.addEventListener("online", this.handleOnlineForReconnection, { once: !0 }), !0), this.handleClose = async (e) => {
125
+ }, this.deferReconnectionUntilOnline = () => D() ? !1 : (typeof window < "u" && window.addEventListener("online", this.handleOnlineForReconnection, { once: !0 }), !0), this.handleClose = async (e) => {
127
126
  this.clearAllTimers(), this._client.connectionEvent?.({
128
127
  type: "close",
129
128
  url: this._url,
@@ -132,7 +131,7 @@ var m = {
132
131
  wasClean: e.wasClean,
133
132
  subscriptions: this._listeners.size
134
133
  });
135
- let t = A(e.code), n = this._listeners.size > 0;
134
+ let t = j(e.code), n = this._listeners.size > 0;
136
135
  t && n && await this.attemptReconnection(e.code), this.cleanupConnection();
137
136
  }, this.handleOpen = () => {
138
137
  typeof window < "u" && window.addEventListener("offline", this.handleOffline), this.reconnectTries = 0;
@@ -141,16 +140,16 @@ var m = {
141
140
  type: "open",
142
141
  url: this._url,
143
142
  retries: this.reconnectTries,
144
- uriApis: x(this._listeners)
143
+ uriApis: S(this._listeners)
145
144
  }), this.cachedMessages.forEach((t) => e.send(this.serializeMessage(t)))), this.cachedMessages = [], this._client.heartbeat.enabled && this.schedulePing();
146
145
  }, this.handleMessage = (e) => {
147
146
  try {
148
147
  let t = JSON.parse(e.data);
149
- if (!w(t)) {
148
+ if (!T(t)) {
150
149
  this._client.connectionEvent?.({
151
150
  type: "invalid-message",
152
151
  url: this._url,
153
- uriApis: x(this._listeners),
152
+ uriApis: S(this._listeners),
154
153
  message: t
155
154
  }), this._listeners.forEach((t) => t.onError({
156
155
  type: "transport",
@@ -162,12 +161,12 @@ var m = {
162
161
  this.clearPongTimeout(), this._client.heartbeat.enabled && this.schedulePing();
163
162
  return;
164
163
  }
165
- if (T(t.method)) {
164
+ if (E(t.method)) {
166
165
  this._client.connectionEvent?.({
167
166
  type: "message-error",
168
167
  url: this._url,
169
168
  uri: t.uri,
170
- uriApis: x(this._listeners),
169
+ uriApis: S(this._listeners),
171
170
  message: t
172
171
  }), this.forEachMatchingListener(t.uri, (e) => e.onMessageError({
173
172
  type: "server",
@@ -182,7 +181,7 @@ var m = {
182
181
  this._client.connectionEvent?.({
183
182
  type: "parse-error",
184
183
  url: this._url,
185
- uriApis: x(this._listeners),
184
+ uriApis: S(this._listeners),
186
185
  message: e.data,
187
186
  error: t
188
187
  }), this._listeners.forEach((t) => t.onError({
@@ -197,7 +196,7 @@ var m = {
197
196
  })), this._client.connectionEvent?.({
198
197
  type: "error",
199
198
  url: this._url,
200
- uriApis: x(this._listeners),
199
+ uriApis: S(this._listeners),
201
200
  event: e
202
201
  });
203
202
  }, this.handleOnline = () => {
@@ -219,7 +218,7 @@ var m = {
219
218
  }
220
219
  e.method !== "subscribe" && this.cachedMessages.push(e), this.connect();
221
220
  }, this.sendPing = () => {
222
- D(this._socket) && (this._socket?.send(this.serializeMessage(O())), this.schedulePongTimeout());
221
+ O(this._socket) && (this._socket?.send(this.serializeMessage(k())), this.schedulePongTimeout());
223
222
  }, this.clearPongTimeout = () => {
224
223
  clearTimeout(this.pongTimeOut), this.pongTimeOut = void 0;
225
224
  }, this.schedulePongTimeout = () => {
@@ -234,7 +233,7 @@ var m = {
234
233
  }, this.schedulePing = () => {
235
234
  this.pingTimeOut = setTimeout(() => {
236
235
  this.sendPing();
237
- }, C());
236
+ }, w());
238
237
  }, this.serializeMessage = (e) => {
239
238
  let t = this._client.transformMessagePayload;
240
239
  return t && (e = t(e)), JSON.stringify(e);
@@ -250,7 +249,7 @@ var m = {
250
249
  get url() {
251
250
  return this._url;
252
251
  }
253
- }, M = class {
252
+ }, N = class {
254
253
  constructor({ maxRetryAttempts: t, notificationThreshold: n, tryAgainLaterDelayMs: r, delays: i, phaseThresholds: a, connectionCleanupDelayMs: o, messageResponseTimeoutMs: s, heartbeat: c, transformMessagePayload: l, connectionEvent: u }) {
255
254
  this._connections = new e(/* @__PURE__ */ new Map()), this._listeners = new e(/* @__PURE__ */ new Map()), this.reconnectAllConnections = () => {
256
255
  this._connections.state.forEach((e) => {
@@ -269,7 +268,7 @@ var m = {
269
268
  }, this.getConnection = (e) => this._connections.state.get(e), this.addConnection = (e, t) => {
270
269
  let n = this._connections.state.get(e);
271
270
  if (n) return n;
272
- let r = new j(t, this);
271
+ let r = new M(t, this);
273
272
  return this._connections.setState((t) => {
274
273
  let n = new Map(t);
275
274
  return n.set(e, r), n;
@@ -295,14 +294,14 @@ var m = {
295
294
  let n = this._listeners.state.get(e);
296
295
  if (n && n.type === t) return n;
297
296
  }
298
- }, N = i(void 0), P = () => {
299
- let e = a(N);
297
+ }, P = n(void 0), F = () => {
298
+ let e = r(P);
300
299
  if (!e) throw Error("useWebsocketClient must be used within a WebsocketClientProvider");
301
300
  return e;
302
- }, F = ({ children: e, client: t }) => /* @__PURE__ */ d(N.Provider, {
301
+ }, I = ({ children: e, client: t }) => /* @__PURE__ */ d(P.Provider, {
303
302
  value: t,
304
303
  children: e
305
- }), I = class {
304
+ }), L = class {
306
305
  constructor(e, t) {
307
306
  this._sendToConnection = null, this._pendingByUri = /* @__PURE__ */ new Map(), this._pendingMessages = [], this._registeredHooks = /* @__PURE__ */ new Set(), this.type = "message", this.hasWaitingUri = (e) => this._pendingByUri.has(e), this.registerHook = (e) => {
308
307
  this._clearHookRemovalTimeout(), this._registeredHooks.add(e);
@@ -393,7 +392,7 @@ var m = {
393
392
  clearTimeout(e.timeoutId), e.reject(/* @__PURE__ */ Error("WebSocket connection closed"));
394
393
  }), this._pendingByUri.clear();
395
394
  }
396
- }, L = class {
395
+ }, R = class {
397
396
  constructor(t) {
398
397
  this._state = new e(p()), this._registeredHooks = /* @__PURE__ */ new Set(), this._sendToConnection = null, this._pendingMessages = [], this.type = "subscription", this.setSendToConnection = (e) => {
399
398
  this._sendToConnection = e, e ? this._flushPendingMessages(e) : (this._clearPendingTimeouts(), this._pendingMessages = []);
@@ -515,7 +514,7 @@ var m = {
515
514
  ...this._options,
516
515
  ...e
517
516
  };
518
- if (r(this._options, t)) return;
517
+ if (u(this._options, t)) return;
519
518
  let n = this._options;
520
519
  this._options = t, this._handleSubscriptionUpdates(n, t), this._handleUnsubscribeOnDisable(n, t);
521
520
  }
@@ -541,70 +540,70 @@ var m = {
541
540
  this._sendToConnection ? this._sendToConnection(e) : this._pendingMessages.push(e);
542
541
  }
543
542
  _handleSubscriptionUpdates(e, t) {
544
- let n = !r(e.body, t.body), i = !e.enabled && t.enabled;
545
- (n || i) && this.subscribe(t.body);
543
+ let n = !u(e.body, t.body), r = !e.enabled && t.enabled;
544
+ (n || r) && this.subscribe(t.body);
546
545
  }
547
546
  _handleUnsubscribeOnDisable(e, t) {
548
547
  let n = !t.enabled, r = e.enabled;
549
548
  n && r && this._state.state.subscribed && this.unsubscribe();
550
549
  }
551
- }, R = (e, t, n) => {
550
+ }, z = (e, t, n) => {
552
551
  let r = e.getListener(t, "subscription");
553
552
  if (r) return r;
554
- let i = new L(n);
553
+ let i = new R(n);
555
554
  return e.addListener(i), i;
556
- }, z = (e, t, n) => {
555
+ }, B = (e, t, n) => {
557
556
  let r = e.getListener(t, "message");
558
557
  if (r) return r;
559
- let i = new I(n, e);
558
+ let i = new L(n, e);
560
559
  return e.addListener(i), i;
561
- }, B = (e, t) => {
560
+ }, V = (e, t) => {
562
561
  e.getConnection(t.url)?.removeListener(t), e.removeListener(t);
563
562
  };
564
563
  //#endregion
565
564
  //#region src/lib/WebsocketHook.ts
566
- function V(e) {
567
- let t = c(e);
568
- return r(t.current, e) || (t.current = e), t.current;
565
+ function H(e) {
566
+ let t = s(e);
567
+ return u(t.current, e) || (t.current = e), t.current;
569
568
  }
570
- function H(e, t, n) {
571
- let r = s(), i = P();
572
- u(() => {
573
- n === !1 ? e.disconnect(() => B(i, e)) : i.addConnection(e.url, t).addListener(e);
569
+ function U(e, t, n) {
570
+ let r = a(), o = F();
571
+ x(() => {
572
+ n === !1 ? e.disconnect(() => V(o, e)) : o.addConnection(e.url, t).addListener(e);
574
573
  }, [
575
574
  n,
576
575
  e,
577
- i
578
- ]), u(() => {
579
- i.getConnection(t)?.replaceUrl(t);
580
- }, [t, i]), o(() => {
576
+ o
577
+ ]), x(() => {
578
+ o.getConnection(t)?.replaceUrl(t);
579
+ }, [t, o]), i(() => {
581
580
  let t = r;
582
581
  return n !== !1 && e.registerHook(r), () => {
583
- e.unregisterHook(t, () => B(i, e));
582
+ e.unregisterHook(t, () => V(o, e));
584
583
  };
585
584
  }, [
586
- i,
585
+ o,
587
586
  n,
588
587
  r,
589
588
  e
590
589
  ]);
591
590
  }
592
- function U(e) {
593
- let t = P(), [n] = l(() => R(t, e.key, e));
594
- H(n, e.url, e.enabled);
595
- let r = V(e);
596
- return u(() => {
591
+ function W(e) {
592
+ let t = F(), [n] = c(() => z(t, e.key, e));
593
+ U(n, e.url, e.enabled);
594
+ let r = H(e);
595
+ return x(() => {
597
596
  n.options = r;
598
597
  }, [r, n]), n;
599
598
  }
600
- var W = (e) => {
601
- let t = P().getListener(e, "subscription"), [r] = l(() => new n(p()));
602
- return t?.store ?? r;
603
- }, G = (e) => {
604
- let t = P(), [n] = l(() => z(t, e.key, e));
605
- return H(n, e.url, e.enabled), n;
606
- }, K = (e, n) => t(e, n);
599
+ var G = (e) => {
600
+ let t = F().getListener(e, "subscription"), [n] = c(() => new l(p()));
601
+ return t?.store ?? n;
602
+ }, K = (e) => {
603
+ let t = F(), [n] = c(() => B(t, e.key, e));
604
+ return U(n, e.url, e.enabled), n;
605
+ }, q = (e, n) => t(e, n);
607
606
  //#endregion
608
- export { f as ReadyState, M as WebsocketClient, F as WebsocketClientProvider, j as WebsocketConnection, I as WebsocketMessageApi, L as WebsocketSubscriptionApi, p as createInitialWebsocketSubscriptionStore, K as useSelector, P as useWebsocketClient, G as useWebsocketMessage, U as useWebsocketSubscription, W as useWebsocketSubscriptionByKey };
607
+ export { f as ReadyState, N as WebsocketClient, I as WebsocketClientProvider, M as WebsocketConnection, L as WebsocketMessageApi, R as WebsocketSubscriptionApi, p as createInitialWebsocketSubscriptionStore, q as useSelector, F as useWebsocketClient, K as useWebsocketMessage, W as useWebsocketSubscription, G as useWebsocketSubscriptionByKey };
609
608
 
610
609
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/lib/types.ts","../src/lib/constants.ts","../src/lib/utils.ts","../src/lib/WebsocketConnection.helpers.ts","../src/lib/WebsocketConnection.ts","../src/lib/WebsocketClient.ts","../src/lib/WebsocketProvider.tsx","../src/lib/WebsocketMessageApi.ts","../src/lib/WebsocketSubscriptionApi.ts","../src/lib/websocketClient.helpers.ts","../src/lib/WebsocketHook.ts"],"sourcesContent":["import { RECONNECTION_CONFIG } from './constants';\nimport { WebsocketMessageApi } from './WebsocketMessageApi';\nimport { WebsocketSubscriptionApi } from './WebsocketSubscriptionApi';\n\n/**\n * Type definitions for the WebSocket connection system.\n *\n * @module types\n */\n\n/**\n * WebSocket connection ready states.\n *\n * Values match the WebSocket API readyState constants, with an additional\n * UNINSTANTIATED state for connections that haven't been created yet.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState\n */\nexport enum ReadyState {\n /** Connection has not been instantiated yet */\n UNINSTANTIATED = -1,\n /** Connection is being established */\n CONNECTING = 0,\n /** Connection is open and ready to communicate */\n OPEN = 1,\n /** Connection is in the process of closing */\n CLOSING = 2,\n /** Connection is closed or couldn't be opened */\n CLOSED = 3\n}\n\n/**\n * Structure for outgoing WebSocket messages.\n *\n * Messages are sent with a method (HTTP-like), URI for routing, optional body,\n * and an automatically generated correlation ID for tracking.\n *\n * @template TMethod - The type of the HTTP method (e.g., 'subscribe', 'unsubscribe', 'post')\n * @template TUri - The type of the URI string\n * @template TBody - The type of the message body payload\n */\nexport interface SendMessage<TMethod = string, TUri = string, TBody = unknown> {\n /** HTTP-like method for the message (e.g., 'subscribe', 'unsubscribe', 'post') */\n method?: TMethod;\n /** URI path for routing the message to the correct handler */\n uri?: TUri;\n /** Optional message body/payload */\n body?: TBody;\n}\n\n/**\n * Callback function for sending messages from a {@link WebsocketSubscriptionApi} to its parent {@link WebsocketConnection}.\n *\n * This callback is injected by the connection when a URI API is registered,\n * replacing the previous EventTarget/CustomEvent indirection with a direct,\n * type-safe function call.\n */\nexport type SendToConnectionFn = (message: SendMessage<string, string, unknown>) => void;\n\n/**\n * Structure of incoming WebSocket messages.\n *\n * Messages must have a URI for routing to the correct handler and can include\n * an optional body with the actual message data.\n *\n * @template TBody - The type of the message body payload\n */\nexport interface IncomingWebsocketMessage<TBody = unknown> {\n /** URI path that identifies which handler should process this message */\n uri: string;\n /** Optional message body/payload */\n body?: TBody;\n /** HTTP-like method for the message (e.g., 'subscribe', 'unsubscribe', 'post') */\n method?: string;\n}\n\n/**\n * Error sent by the server via a message with method 'error', 'conflict', or 'exception'.\n * Contains the parsed message body for application-level error handling.\n *\n * @template TBody - The type of the error body payload\n */\nexport interface WebsocketServerError<TBody = unknown> {\n readonly type: 'server';\n readonly message: IncomingWebsocketMessage<TBody>;\n}\n\n/**\n * Error from the WebSocket transport layer (connection failure, network issues, etc.).\n * Contains the raw Event from the WebSocket 'error' handler.\n */\nexport interface WebsocketTransportError {\n readonly type: 'transport';\n readonly event: Event;\n}\n\n/**\n * Configuration options for WebSocket URI APIs.\n *\n * Subscriptions automatically subscribe when the WebSocket connection opens.\n *\n * @template TData - The type of data received from the WebSocket\n * @template TBody - The type of message body sent to the WebSocket\n */\nexport interface WebsocketSubscriptionOptions<TData = unknown, TBody = unknown> {\n /** The base URL of the WebSocket connection. */\n url: string;\n /** The URI path for this subscription. */\n uri: string;\n /**\n * Unique key for the URI API.\n *\n * Used to identify the URI API in the connection.\n */\n key: string;\n /** Whether this URI API is enabled (default: true). When disabled, messages are not sent. */\n enabled?: boolean;\n /** Optional body payload to send with subscription or initial message */\n body?: TBody;\n\n /** Optional HTTP method for custom messages sent via sendMessage */\n method?: string;\n /**\n * Callback invoked when subscription is successful.\n *\n * @param uri - The URI path that was subscribed to\n * @param body - The body that was sent with the subscription\n */\n onSubscribe?: (props: { uri: string; body?: TBody; uriApi: WebsocketSubscriptionApi<TData, TBody> }) => void;\n /**\n * Callback invoked when a message is received for this URI.\n *\n * @param data - The message data received from the WebSocket\n * @param uriApi - The URI API instance that received the message\n */\n onMessage?: (props: { data: TData; uriApi: WebsocketSubscriptionApi<TData, TBody> }) => void;\n /**\n * Callback invoked when a WebSocket error occurs.\n *\n * @param error - Discriminated error: use `error.type === 'server'` for server-sent error messages\n * (parsed body in `error.message`), or `error.type === 'transport'` for connection failures.\n */\n onError?: (error: WebsocketTransportError) => void;\n\n /**\n * Callback invoked when a server error message is received for this subscription.\n *\n * @param error - Server error with parsed message body (`error.type === 'server'`, `error.message` contains the incoming message)\n */\n onMessageError?: (error: WebsocketServerError<TBody>) => void;\n /**\n * Callback invoked when the WebSocket connection closes.\n *\n * @param event - The close event from the WebSocket connection\n */\n onClose?: (event: CloseEvent) => void;\n}\n\n/**\n * Configuration options for WebSocket Message API.\n *\n * Message API is for request/response style communication: send a message to any URI\n * and optionally wait for a response. No subscription support.\n *\n * @template TData - The type of data received in the response\n * @template TBody - The type of message body sent to the WebSocket\n */\nexport interface WebsocketMessageOptions {\n /** The base URL of the WebSocket connection. */\n url: string;\n /**\n * Unique key for the Message API.\n *\n * Used to identify the API in the connection.\n */\n key: string;\n /** Whether this Message API is enabled (default: true). When disabled, messages are not sent. */\n enabled?: boolean;\n /**\n * Default timeout in ms when waiting for a response.\n *\n * Can be overridden per sendMessage call.\n */\n responseTimeoutMs?: number;\n /**\n * Callback invoked when a WebSocket transport error occurs.\n */\n onError?: (error: WebsocketTransportError) => void;\n /**\n * Callback invoked when a server error message is received.\n */\n onMessageError?: (error: WebsocketServerError) => void;\n /**\n * Callback invoked when the WebSocket connection closes.\n */\n onClose?: (event: CloseEvent) => void;\n}\n\n/**\n * Options for WebsocketMessageApi.sendMessage.\n */\nexport interface SendMessageOptions {\n /** Timeout in ms when waiting for a response. Overrides the default from options. */\n timeout?: number;\n}\n\n/**\n * Common interface for WebSocket listeners registered with {@link WebsocketConnection}.\n *\n * Both {@link WebsocketSubscriptionApi} and {@link WebsocketMessageApi} implement this interface,\n * allowing the connection to treat them uniformly via {@link addListener} / {@link removeListener}.\n *\n * - **Subscription listeners**: Have `uri`, `onOpen`, `onMessage` — route by URI match\n * - **Message listeners**: Have `hasWaitingUri`, `deliverMessage` — route by pending URI\n */\nexport interface WebsocketListener {\n readonly key: string;\n readonly url: string;\n readonly isEnabled: boolean;\n setSendToConnection(callback: SendToConnectionFn | null): void;\n onError(error: WebsocketTransportError): void;\n onMessageError(error: WebsocketServerError<unknown>): void;\n onClose(event: CloseEvent): void;\n reset(): void;\n /** Subscription listeners: fixed URI for this endpoint */\n readonly uri?: string;\n /** Subscription listeners: called when connection opens */\n onOpen?(): void;\n /** Subscription listeners: called when a message is received for this URI */\n onMessage?(data: unknown): void;\n /** Message listeners: returns true if waiting for a response for the given URI */\n hasWaitingUri?(uri: string): boolean;\n /** Message listeners: delivers a response for a pending request */\n deliverMessage?(uri: string, data: unknown): void;\n readonly type: 'subscription' | 'message';\n}\n\nexport type WebsocketMessageApiPublic = Pick<\n WebsocketMessageApi,\n 'sendMessage' | 'sendMessageNoWait' | 'reset' | 'url' | 'key' | 'isEnabled'\n>;\n\nexport type WebsocketSubscriptionApiPublic<TData = unknown, TBody = unknown> = Pick<\n WebsocketSubscriptionApi<TData, TBody>,\n 'reset' | 'url' | 'key' | 'isEnabled' | 'store'\n>;\n\nexport interface WebsocketSubscriptionStore<TData = unknown> {\n message: TData | undefined;\n subscribed: boolean;\n /**\n * Whether a subscription has been sent but no response received yet.\n *\n * - `true`: A subscribe message was sent and we are waiting for the first (or next) message.\n * - `false`: No subscription is active, the connection is closed, or we have already received a response.\n *\n * Use this to show loading/placeholder UI while waiting for initial data after subscribing.\n */\n pendingSubscription: boolean;\n subscribedAt: number | undefined;\n receivedAt: number | undefined;\n connected: boolean;\n messageError: WebsocketTransportError | undefined;\n serverError: WebsocketServerError<unknown> | undefined;\n}\n\n/**\n * Creates the initial state for a {@link WebsocketSubscriptionStore}.\n *\n * @template TData - The type of data in the store's `message` field\n * @returns A new store with default values (message: undefined, subscribed: false, etc.)\n */\nexport function createInitialWebsocketSubscriptionStore<TData = unknown>(): WebsocketSubscriptionStore<TData> {\n return {\n message: undefined,\n subscribed: false,\n pendingSubscription: false,\n subscribedAt: undefined,\n receivedAt: undefined,\n connected: false,\n messageError: undefined,\n serverError: undefined\n };\n}\n\n/**\n * Optional custom logger for WebSocket connection events.\n * Set via {@link WebsocketConfig.setCustomLogger}.\n */\nexport interface WebsocketLogger {\n /** Logs connection events (e.g. ws-connect, ws-close, ws-error, ws-reconnect) */\n // log?(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void;\n /** Called when max retry attempts exceeded; use to trigger token refresh or other recovery */\n connectionEvents?: (event: WebsocketLoggerConnectionEvent) => void;\n}\n\n/** Union of all connection event types passed to {@link WebsocketClientOverrides.connectionEvent}. */\n\nexport type WebsocketLoggerConnectionEvent =\n | WebsocketLoggerCloseEvent\n | WebsocketLoggerOpenEvent\n | WebsocketLoggerMessageEvent\n | WebsocketLoggerErrorEvent\n | WebsocketLoggerReconnectingEvent\n | WebsocketLoggerPongTimeoutEvent\n | WebsocketLoggerInvalidMessageEvent\n | WebsocketLoggerMessageErrorEvent\n | WebsocketLoggerParseErrorEvent\n | WebsocketLoggerSendMessageEvent\n | WebsocketLoggerCleanupEvent;\n\n/** @internal */\ninterface WebsocketLoggerCloseEvent {\n /** WebSocket connection closed */\n type: 'close';\n url: string;\n code: number;\n reason: string;\n wasClean: boolean;\n subscriptions: number;\n}\n/** @internal */\ninterface WebsocketLoggerCleanupEvent {\n /** Connection cleaned up (no listeners remain) */\n type: 'cleanup';\n url: string;\n}\n\n/** @internal */\ninterface WebsocketLoggerOpenEvent {\n /** WebSocket connection opened or connecting */\n type: 'open' | 'connect';\n url: string;\n retries: number;\n uriApis: string[];\n}\n/** @internal */\ninterface WebsocketLoggerMessageEvent {\n /** Incoming message received */\n type: 'message';\n uri: string;\n url: string;\n body: unknown;\n method: string;\n}\n/** @internal */\ninterface WebsocketLoggerSendMessageEvent {\n /** Outgoing message sent */\n type: 'send-message';\n uri?: string;\n url: string;\n body: unknown;\n method?: string;\n}\n\n/** @internal */\ninterface WebsocketLoggerErrorEvent {\n /** WebSocket transport error */\n type: 'error';\n event: unknown;\n url: string;\n uriApis: string[];\n}\n/** @internal */\ninterface WebsocketLoggerParseErrorEvent {\n /** Failed to parse incoming message JSON */\n type: 'parse-error';\n error: unknown;\n url: string;\n uriApis: string[];\n message: unknown;\n}\n\n/** @internal */\ninterface WebsocketLoggerMessageErrorEvent {\n /** Server sent error message (method: error, conflict, or exception) */\n type: 'message-error';\n uri: string;\n url: string;\n uriApis: string[];\n message: unknown;\n}\n/** @internal */\ninterface WebsocketLoggerReconnectingEvent {\n /** Reconnection attempt or max retries exceeded */\n type: 'reconnecting' | 'max-retries-exceeded';\n retries: number;\n url: string;\n}\n\n/** @internal */\ninterface WebsocketLoggerInvalidMessageEvent {\n /** Incoming message missing required structure (e.g. uri) */\n type: 'invalid-message';\n url: string;\n uriApis: string[];\n message: unknown;\n}\n\n/** @internal */\ninterface WebsocketLoggerPongTimeoutEvent {\n /** No pong received within heartbeat timeout */\n type: 'pong-timeout';\n url: string;\n}\n\nexport type ReconnectionConfig = typeof RECONNECTION_CONFIG;\n\n/** Heartbeat (ping/pong) configuration */\nexport interface HeartbeatConfig {\n /** Whether to send ping messages and expect pong responses. Default: true */\n enabled: boolean;\n /** Time in ms to wait for a pong before considering the connection dead. Default: 10000 */\n pongTimeoutMs: number;\n}\n\n/** Overrides for the global WebSocket configuration. All fields are optional. */\nexport interface WebsocketClientOverrides {\n /** Maximum number of reconnection attempts before stopping and showing a permanent error. Prevents infinite retries on dead endpoints (CPU wake-ups, battery drain). ~10 attempts ≈ 12 minutes at phase 3 (90s interval). User can retry manually. */\n maxRetryAttempts?: number;\n /** Number of failed reconnection attempts before showing user notifications. Prevents notification spam during brief network interruptions. */\n notificationThreshold?: number;\n /** Initial delay (in ms) when server closes with 1013 Try Again Later. The server explicitly asks to wait before reconnecting. */\n tryAgainLaterDelayMs?: number;\n /** Delay durations (in milliseconds) for each reconnection phase. */\n delays?: {\n firstPhase?: number;\n secondPhase?: number;\n thirdPhase?: number;\n };\n /** Threshold values that determine when to transition between reconnection phases. */\n phaseThresholds?: {\n first?: number;\n second?: number;\n };\n /** Override connection cleanup delay when no listeners remain */\n connectionCleanupDelayMs?: number;\n /** Default timeout in ms when waiting for a message response. Used by WebsocketMessageApi */\n messageResponseTimeoutMs?: number;\n /** Override ping/pong heartbeat behavior */\n heartbeat?: Partial<HeartbeatConfig>;\n\n transformMessagePayload?: (payload: SendMessage<string, string, unknown>) => SendMessage<string, string, unknown>;\n /**\n * Callback for connection event logging. Receives events such as:\n * - `{ type: 'open' | 'connect', url, retries, uriApis }`\n * - `{ type: 'close', url, code, reason, wasClean, subscriptions }`\n * - `{ type: 'reconnecting' | 'max-retries-exceeded', url, retries }`\n * - `{ type: 'message-error', url, uri, uriApis, message }`\n * - `{ type: 'invalid-message', url, uriApis, message }`\n * - `{ type: 'parse-error', url, uriApis, message, error }`\n * - `{ type: 'send-message', url, uri?, body, method? }`\n * - `{ type: 'cleanup', url }`\n * - `{ type: 'pong-timeout', url }`\n *\n * @param event - The connection event\n */\n connectionEvent?: (event: WebsocketLoggerConnectionEvent) => void;\n}\n","import { HeartbeatConfig } from \"./types\";\n\n/**\n * WebSocket constants and configuration.\n *\n * @module constants\n */\n\n/**\n * WebSocket close codes used for connection state detection.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n */\nexport const WEBSOCKET_CLOSE_CODES = {\n /** Clean intentional closure — do not reconnect */\n NORMAL_CLOSURE: 1000,\n /** Endpoint going away (e.g. server restarting) — reconnect */\n GOING_AWAY: 1001,\n /** Server internal error — reconnect */\n INTERNAL_ERROR: 1011,\n /** Service restart — reconnect */\n SERVICE_RESTART: 1012,\n /** Try again later — reconnect */\n TRY_AGAIN_LATER: 1013,\n /**\n * Abnormal closure (1006) indicates the connection was closed without\n * receiving a close frame. Typically occurs due to network issues,\n * server crashes, or unexpected termination — reconnect.\n */\n ABNORMAL_CLOSURE: 1006\n} as const;\n\n/**\n * Configuration for WebSocket reconnection behavior with exponential backoff.\n *\n * The reconnection strategy uses three phases:\n * - First phase (attempts 0-4): 4 second delay\n * - Second phase (attempts 5-9): 30 second delay\n * - Third phase (attempts 10+): 90 second delay\n *\n * User notifications are only shown after the notification threshold is exceeded\n * to avoid spamming users during brief network interruptions.\n *\n * After MAX_RETRY_ATTEMPTS, reconnection stops to avoid infinite retries on dead\n * endpoints (e.g. ~10 attempts ≈ 12 minutes); the user can manually retry via the\n * notification action.\n */\nexport const RECONNECTION_CONFIG = {\n /**\n * Maximum number of reconnection attempts before stopping and showing a permanent\n * error. Prevents infinite retries on dead endpoints (CPU wake-ups, battery drain).\n * ~10 attempts ≈ 12 minutes at phase 3 (90s interval). User can retry manually.\n */\n MAX_RETRY_ATTEMPTS: 20,\n /**\n * Number of failed reconnection attempts before showing user notifications.\n * Prevents notification spam during brief network interruptions.\n */\n NOTIFICATION_THRESHOLD: 10,\n /**\n * Initial delay (in ms) when server closes with 1013 Try Again Later.\n * The server explicitly asks to wait before reconnecting.\n */\n TRY_AGAIN_LATER_DELAY_MS: 30000,\n /**\n * Delay durations (in milliseconds) for each reconnection phase.\n */\n DELAYS: {\n /** First phase delay: 4 seconds for the first 5 attempts */\n FIRST_PHASE: 4000,\n /** Second phase delay: 30 seconds for attempts 6-10 */\n SECOND_PHASE: 30000,\n /** Third phase delay: 90 seconds for attempts 11+ */\n THIRD_PHASE: 90000\n },\n /**\n * Threshold values that determine when to transition between reconnection phases.\n */\n PHASE_THRESHOLDS: {\n /** Maximum attempts in the first phase (0-4) */\n FIRST: 5,\n /** Maximum attempts in the second phase (5-9) */\n SECOND: 10\n }\n} as const;\n\n/**\n * Delay configuration for WebSocket connection cleanup.\n *\n * When all URI APIs are removed, the connection is closed after a delay to allow\n * for efficient resource cleanup while avoiding unnecessary reconnections for\n * quick re-registrations. Different delays are used for production vs test environments.\n */\n// export const CONNECTION_CLEANUP_DELAY = {\n// /** Production delay: 3 seconds to allow for quick re-registrations */\n// PRODUCTION_MS: 3000,\n// /** Test delay: 10ms for faster test execution */\n// TEST_MS: 10\n// } as const;\nexport const CONNECTION_CLEANUP_DELAY_MS = 3000;\n\n/**\n * Delay in milliseconds before reconnecting after teardown.\n *\n * Used in {@link WebsocketConnection.teardownAndReconnect} to allow cleanup\n * to complete before establishing a new connection.\n */\nexport const TEARDOWN_RECONNECT_DELAY_MS = 1000;\n\n/**\n * Delay in milliseconds before removing a WebSocket URI API initiator.\n *\n * When an initiator (component/hook) is removed, there's a short delay before\n * unsubscribing and cleaning up. This prevents rapid subscribe/unsubscribe cycles\n * that could occur during React component re-renders or fast user interactions.\n */\nexport const INITIATOR_REMOVAL_DELAY_MS = 200;\n\n/**\n * Configuration for WebSocket heartbeat (ping/pong) mechanism.\n *\n * After sending a ping, we expect a pong within PONG_TIMEOUT_MS. If no pong arrives,\n * the connection is considered dead and we force-close to trigger reconnection.\n */\nexport const HEARTBEAT_CONFIG = {\n /** Time in ms to wait for a pong before considering the connection dead. Default: 10 seconds */\n PONG_TIMEOUT_MS: 10000\n} as const;\n\n/**\n * Default options for WebSocket URI APIs.\n *\n * These defaults are used when creating a new URI API instance if options\n * are not explicitly provided. Subscriptions automatically subscribe when\n * the WebSocket connection opens.\n *\n * @see {@link WebsocketUriOptions} - The type definition for URI options\n * @see {@link WebsocketSubscriptionApi} - The class that uses these defaults\n */\nexport const DEFAULT_URI_OPTIONS: {\n enabled: boolean;\n} = {\n enabled: true\n};\n\n/**\n * Default timeout in milliseconds when waiting for a WebSocket message response.\n *\n * Used by {@link WebsocketMessageApi} when no explicit timeout is provided.\n */\nexport const DEFAULT_MESSAGE_RESPONSE_TIMEOUT_MS = 10000;\n\n\n/**\n * Default heartbeat configuration for WebSocket connections.\n *\n * Enables ping/pong with the default timeout from {@link HEARTBEAT_CONFIG}.\n */\nexport const DEFAULT_HEARTBEAT_CONFIG: HeartbeatConfig = {\n enabled: true,\n pongTimeoutMs: HEARTBEAT_CONFIG.PONG_TIMEOUT_MS\n};","export const wait = (ms: number) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * @fileoverview Pure helper functions for WebSocket connection management.\n *\n * These utilities support {@link WebsocketConnection} with reconnection timing,\n * message validation, heartbeat, and user notifications. All functions are\n * stateless and side-effect free except the notification helpers.\n *\n * @module WebsocketConnection.helpers\n */\n\nimport { RECONNECTION_CONFIG, WEBSOCKET_CLOSE_CODES } from \"./constants\";\nimport {\n IncomingWebsocketMessage,\n ReconnectionConfig,\n SendMessage,\n WebsocketClientOverrides,\n WebsocketListener,\n} from \"./types\";\n\n/**\n * Extracts URIs from subscription listeners for connection event logging.\n *\n * @param listeners - Map of listeners keyed by their unique key\n * @returns Array of URIs from subscription listeners (message APIs are excluded)\n * @internal\n */\nexport const getSubscriptionUris = (\n listeners: Map<string, WebsocketListener>\n): string[] =>\n Array.from(listeners)\n .filter(([, listener]) => \"uri\" in listener)\n .map(([, listener]) => (listener as { uri: string }).uri);\n\n/**\n * Calculates the wait time before attempting to reconnect based on the number of failed attempts.\n *\n * Uses a three-phase exponential backoff strategy to avoid hammering a failing server:\n * - **First phase** (attempts 0–4): 4 seconds — quick recovery for transient issues\n * - **Second phase** (attempts 5–9): 30 seconds — moderate backoff for persistent issues\n * - **Third phase** (attempts 10+): 90 seconds — long backoff to reduce load on dead endpoints\n *\n * @param tries - The number of reconnection attempts made so far\n * @param reconnectionConfig - Optional reconnection config (defaults to global config)\n * @returns Wait time in milliseconds before next reconnection attempt\n *\n * @see {@link RECONNECTION_CONFIG} - Phase thresholds and delay values\n * @internal\n */\nexport const reconnectWaitTime = (\n tries: number,\n delays: NonNullable<Required<WebsocketClientOverrides[\"delays\"]>>,\n phaseThresholds: NonNullable<\n Required<WebsocketClientOverrides[\"phaseThresholds\"]>\n >\n) => {\n if (tries < phaseThresholds.first) {\n return delays.firstPhase;\n }\n if (tries < phaseThresholds.second) {\n return delays.secondPhase;\n }\n return delays.thirdPhase;\n};\n\n/**\n * Gets the ping interval time in milliseconds for keeping WebSocket connections alive.\n *\n * The heartbeat sends a ping every 40 seconds. If no pong arrives within\n * {@link HEARTBEAT_CONFIG.PONG_TIMEOUT_MS}, the connection is force-closed to trigger reconnection.\n *\n * @returns The ping interval in milliseconds (40 seconds)\n *\n * @see {@link HEARTBEAT_CONFIG.PONG_TIMEOUT_MS} - Time to wait for pong before considering connection dead\n * @internal\n */\nexport const getPingTime = (): number => 40 * 1000;\n\n/**\n * Type guard to validate that a parsed value is a valid incoming WebSocket message.\n *\n * Valid messages must be an object with a string `uri` property. Messages without\n * a valid structure are rejected and trigger {@link WebsocketListener.onError} with\n * type `'transport'`.\n *\n * @param value - The value to check (typically from `JSON.parse`)\n * @returns `true` if the value is a valid {@link IncomingWebsocketMessage}\n *\n * @internal\n */\nexport const isValidIncomingMessage = (\n value: unknown\n): value is IncomingWebsocketMessage => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"uri\" in value &&\n typeof (value as Record<string, unknown>).uri === \"string\"\n );\n};\n\n/**\n * Checks if the method indicates a server-side error message.\n *\n * Server errors use methods `'error'`, `'conflict'`, or `'exception'`. These are\n * routed to {@link WebsocketListener.onMessageError} instead of `onMessage`.\n *\n * @param method - The message method to check (optional)\n * @returns `true` if the method is an error method; `false` if undefined or not an error\n *\n * @internal\n */\nexport const isErrorMethod = (method?: string): boolean => {\n if (!method) return false;\n const errorMethods = [\"error\", \"conflict\", \"exception\"];\n return errorMethods.includes(method);\n};\n\n/**\n * Checks if the browser reports an online network state.\n *\n * Uses `window.navigator.onLine`. Note: this can be unreliable — it may report\n * `true` when the user is on a network but has no internet (e.g. captive portal).\n *\n * @returns `true` if the browser is online; `false` in SSR or when offline\n *\n * @internal\n */\nexport const isBrowserOnline = (): boolean => {\n return typeof window !== \"undefined\" && window.navigator.onLine;\n};\n\n/**\n * Checks if the WebSocket is ready to send and receive messages.\n *\n * Requires both browser online state and socket in {@link WebSocket.OPEN} state.\n * Use this before sending messages (e.g. heartbeat ping).\n *\n * @param socket - The WebSocket instance to check\n * @returns `true` if browser is online and socket is OPEN\n *\n * @see {@link isConnectionReady} - Less strict: also allows CONNECTING\n * @internal\n */\nexport const isSocketOnline = (socket?: WebSocket): boolean => {\n return (\n typeof window !== \"undefined\" &&\n window.navigator.onLine &&\n socket !== undefined &&\n socket.readyState === WebSocket.OPEN\n );\n};\n\n/**\n * Creates a ping message for the WebSocket heartbeat mechanism.\n *\n * Format: `{ method: 'post', uri: 'ping', body: timestamp, correlation: uuid }`.\n * The server should respond with a pong; missing pong triggers reconnection.\n *\n * @returns JSON string of the ping message\n *\n * @internal\n */\nexport const createPingMessage = (): SendMessage<string, string, number> => {\n return {\n method: \"post\",\n uri: \"ping\",\n body: Date.now(),\n };\n};\n\n/**\n * Checks if the WebSocket connection is in a valid state (open or connecting).\n *\n * Used to avoid creating duplicate connections. Unlike {@link isSocketOnline},\n * this returns `true` for CONNECTING state — useful when deciding whether to\n * call `connect()`.\n *\n * @param socket - The WebSocket instance to check\n * @returns `true` if socket is OPEN or CONNECTING; `false` if undefined, CLOSING, or CLOSED\n *\n * @see {@link isSocketOnline} - Stricter: requires OPEN and browser online\n * @internal\n */\nexport const isConnectionReady = (socket?: WebSocket): boolean => {\n return (\n socket?.readyState === WebSocket.OPEN ||\n socket?.readyState === WebSocket.CONNECTING\n );\n};\n\n/**\n * Determines whether a WebSocket close event warrants an automatic reconnection attempt.\n *\n * **Only code 1000 (Normal Closure) does NOT trigger reconnection** — it indicates a\n * clean, intentional shutdown. All other codes trigger reconnection when listeners\n * are still registered, including:\n * - 1001 Going Away, 1011 Internal Error, 1012 Service Restart, 1013 Try Again Later\n * - 1006 Abnormal Closure (no close frame received — network/server crash)\n *\n * @param closeCode - The close event code from the WebSocket CloseEvent\n * @returns `true` if reconnection should be attempted; `false` for 1000 only\n *\n * @see {@link WEBSOCKET_CLOSE_CODES} - Close code constants\n */\nexport const isReconnectableCloseCode = (closeCode: number): boolean => {\n return closeCode !== WEBSOCKET_CLOSE_CODES.NORMAL_CLOSURE;\n};\n","/**\n * @fileoverview WebSocket connection management with automatic reconnection and URI-based routing.\n *\n * This module provides a robust WebSocket connection manager that handles:\n * - Connection lifecycle (connect, disconnect, reconnect)\n * - Automatic reconnection with three-phase exponential backoff\n * - Heartbeat/ping-pong to detect and recover from stale connections\n * - URI-based message routing to multiple listeners over a single connection\n * - Browser online/offline detection and deferred reconnection\n * - Singleton connection per URL key (via {@link WebsocketClient.addConnection})\n * - User notifications for connection status (with configurable threshold)\n *\n * ## Architecture\n *\n * Connections are created via {@link WebsocketClient.addConnection} which ensures one\n * connection per key. Listeners ({@link WebsocketSubscriptionApi} or {@link WebsocketMessageApi})\n * register via {@link addListener} and receive messages routed by URI.\n *\n * ## Edge Cases\n *\n * - **Cached messages**: Only non-subscribe messages are cached when the socket is not open;\n * subscribe messages trigger connect but are not queued.\n * - **replaceUrl vs reconnect**: Both use `teardownAndReconnect`; a guard prevents concurrent\n * cycles when both fire in the same render (e.g. auth context change).\n * - **Close code 1000**: Only this code does NOT trigger reconnection (intentional shutdown).\n * - **Max retries**: After {@link RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS}, automatic reconnection\n * stops; user must click Retry in the notification.\n *\n * @module WebsocketConnection\n */\n\nimport { wait } from \"./utils\";\nimport {\n TEARDOWN_RECONNECT_DELAY_MS,\n WEBSOCKET_CLOSE_CODES,\n} from \"./constants\";\nimport { SendMessage, WebsocketListener } from \"./types\";\nimport { WebsocketClient } from \"./WebsocketClient\";\nimport {\n createPingMessage,\n getPingTime,\n getSubscriptionUris,\n isBrowserOnline,\n isConnectionReady,\n isErrorMethod,\n isReconnectableCloseCode,\n isSocketOnline,\n isValidIncomingMessage,\n reconnectWaitTime,\n} from \"./WebsocketConnection.helpers\";\n\n/**\n * Manages a WebSocket connection with automatic reconnection, heartbeat monitoring, and URI-based message routing.\n *\n * This class provides:\n * - Automatic reconnection with exponential backoff on connection loss\n * - Heartbeat/ping mechanism to keep connections alive\n * - Multiple URI API registration for routing messages to different handlers\n * - Online/offline detection and handling\n * - Custom logger support for monitoring (configure via {@link WebsocketClient} connectionEvent)\n * - User notifications for connection status\n *\n * @example\n * ```typescript\n * const connection = new WebsocketConnection('ws://example.com/api');\n * const uriApi = new WebsocketSubscriptionApi({\n * key: 'messages',\n * url: '/api',\n * uri: '/messages',\n * onMessage: ({ data }) => console.log('Received:', data),\n * onError: (error) => console.log('Error:', error),\n * onClose: (event) => console.log('Closed:', event)\n * });\n * connection.addListener(uriApi);\n * ```\n *\n * @see {@link WebsocketClient.reconnectAllConnections} - Reconnect all connections (e.g. on auth change)\n */\nexport class WebsocketConnection {\n // ─── Properties ─────────────────────────────────────────────────────\n /** The underlying WebSocket instance */\n private _socket?: WebSocket;\n\n /** Map of all listeners (subscription and message APIs) keyed by their unique key */\n private _listeners: Map<string, WebsocketListener> = new Map();\n\n /** The WebSocket URL */\n private _url: string;\n\n /** Timeout for the next ping message */\n private pingTimeOut: ReturnType<typeof setTimeout> | undefined;\n\n /** Timeout for detecting missing pong after ping (dead-connection detection) */\n private pongTimeOut: ReturnType<typeof setTimeout> | undefined;\n\n /** Timeout for closing the connection when no URIs are registered */\n private closeConnectionTimeOut: ReturnType<typeof setTimeout> | undefined;\n\n /** Counter for reconnection attempts */\n private reconnectTries = 0;\n\n /** Guard flag that prevents concurrent teardown-and-reconnect cycles (e.g. when both replaceUrl and reconnect fire in the same render). */\n private _isReconnecting = false;\n\n /** True when max retry attempts exceeded; stops automatic reconnection until manual retry. */\n private _maxRetriesExceeded = false;\n\n /**\n * Queue of non-subscribe messages sent while the socket was not open.\n * Flushed when the connection opens. Subscribe messages are NOT cached — they trigger connect only.\n */\n private cachedMessages: SendMessage<string, string, any>[] = [];\n\n /** The WebsocketClient instance */\n private _client: WebsocketClient;\n\n // ─── Constructor ────────────────────────────────────────────────────\n\n /**\n * Creates a new WebSocket connection instance.\n *\n * The connection is not established until a listener is added via {@link addListener}.\n *\n * @param url - The WebSocket URL to connect to\n * @param client - The {@link WebsocketClient} for configuration (reconnection, heartbeat, etc.)\n */\n constructor(url: string, client: WebsocketClient) {\n this._url = url;\n this._client = client;\n }\n\n // ─── Public Getters ─────────────────────────────────────────────────\n\n /**\n * Gets the current ready state of the WebSocket connection.\n *\n * @returns The WebSocket ready state (CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3) or undefined if no socket exists.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState | WebSocket.readyState}\n */\n public get readyState() {\n return this._socket?.readyState;\n }\n\n /**\n * Gets the WebSocket URL for this connection.\n *\n * @returns The WebSocket URL string\n */\n public get url() {\n return this._url;\n }\n\n /**\n * Gets the underlying WebSocket instance.\n *\n * @returns The WebSocket instance if connected, or undefined if the connection hasn't been established yet or has been closed.\n */\n public getSocket = (): WebSocket | undefined => {\n return this._socket;\n };\n\n // ─── Public API: URI Management ─────────────────────────────────────\n\n /**\n * Registers a listener (subscription or message API) with this connection.\n *\n * Initiates the WebSocket connection if not already connected. Sets up the send callback\n * so the listener can transmit messages through this connection.\n *\n * If the socket is already open, immediately notifies subscription listeners via `onOpen`.\n *\n * @param listener - The {@link WebsocketListener} to register\n * @returns The registered listener\n */\n public addListener = (listener: WebsocketListener) => {\n listener.setSendToConnection(this.handleSendMessage);\n this.connect();\n this._listeners.set(listener.key, listener);\n clearTimeout(this.closeConnectionTimeOut);\n\n if (this._socket?.readyState === WebSocket.OPEN && listener.onOpen) {\n listener.onOpen();\n }\n return listener;\n };\n\n /**\n * Unregisters a listener and schedules connection cleanup if no listeners remain.\n *\n * Disconnects the listener's send callback and removes it from the routing map.\n * The WebSocket connection will be closed after {@link CONNECTION_CLEANUP_DELAY_MS} if no other\n * listeners are registered.\n *\n * @param listener - The listener instance to unregister\n */\n public removeListener = (listener: WebsocketListener) => {\n const existing = this._listeners.get(listener.key);\n if (existing) {\n existing.setSendToConnection(null);\n this._listeners.delete(existing.key);\n }\n clearTimeout(this.closeConnectionTimeOut);\n this.scheduleConnectionCleanup();\n };\n\n /** Schedules connection close after configured delay when no listeners remain. */\n private scheduleConnectionCleanup = () => {\n const { connectionCleanupDelayMs } = this._client;\n\n this.closeConnectionTimeOut = setTimeout(() => {\n if (this._listeners.size === 0) {\n this._socket?.close();\n this._client.removeConnection(this.url);\n }\n }, connectionCleanupDelayMs);\n };\n\n // ─── Public API: Connection Control ─────────────────────────────────\n\n /**\n * Replaces the WebSocket URL and re-establishes the connection.\n *\n * Closes the current connection, resets all listeners, and reconnects using the new URL\n * after a short delay (1 second) to allow cleanup to complete.\n *\n * @param newUrl - The new WebSocket URL to connect to\n */\n public replaceUrl = async (newUrl: string) => {\n if (this._url !== newUrl) {\n this._url = newUrl;\n await this.teardownAndReconnect();\n }\n };\n\n /**\n * Reconnects the WebSocket connection.\n *\n * Tears down the current connection by removing all event listeners, closing the socket,\n * and resetting all registered URI APIs. After a short delay (1 second) to allow cleanup,\n * re-establishes the connection. Typically triggered by {@link websocketConnectionsReconnect}\n * when {@link useReconnectWebsocketConnections} (from @mono-fleet/common-components) detects\n * the user's authentication context (region/role) change.\n *\n * Guarded by {@link _isReconnecting} in {@link teardownAndReconnect} — if a `replaceUrl`\n * layout effect already started a reconnect cycle in the same render, this call is a no-op.\n */\n public reconnect = async () => {\n await this.teardownAndReconnect();\n };\n\n /**\n * Resets the retry counter and re-establishes the connection.\n *\n * Used when the user manually retries after hitting {@link RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS}.\n * Clears the max-retries-exceeded state and initiates a fresh connection attempt.\n */\n public resetRetriesAndReconnect = (): void => {\n this.reconnectTries = 0;\n this._maxRetriesExceeded = false;\n this.connect();\n };\n\n // ─── Connection Lifecycle (Private) ─────────────────────────────────\n\n /**\n * Establishes the WebSocket connection if not already connecting or connected.\n * Only creates a socket if at least one registered listener (subscription or message API) is enabled.\n * Sets up all event listeners and logs the connection attempt via the custom logger if configured.\n */\n private connect = () => {\n const hasEnabledListener = Array.from(this._listeners.values()).some(\n (listener) => listener.isEnabled\n );\n if (isConnectionReady(this._socket) || !hasEnabledListener) {\n return;\n }\n this._client.connectionEvent?.({\n type: \"connect\",\n url: this._url,\n retries: this.reconnectTries,\n uriApis: getSubscriptionUris(this._listeners),\n });\n this._socket = new WebSocket(this._url);\n this._socket.addEventListener(\"close\", this.handleClose);\n this._socket.addEventListener(\"message\", this.handleMessage);\n this._socket.addEventListener(\"open\", this.handleOpen);\n this._socket.addEventListener(\"error\", this.handleError);\n };\n\n /**\n * Tears down the current socket: clears all timers, removes all event listeners,\n * closes the socket, and resets the socket reference.\n */\n private teardownSocket = () => {\n this.clearAllTimers();\n this.removeListeners();\n this._socket?.close();\n this._socket = undefined;\n };\n\n /**\n * Tears down the current connection, resets all listeners, waits for cleanup to complete,\n * and re-establishes the connection. Shared by {@link replaceUrl} and {@link reconnect}.\n *\n * Guarded by {@link _isReconnecting} to prevent concurrent cycles. When\n * `selectedRegionRole` changes, both the hook's `replaceUrl` layout effect and\n * `useReconnectWebsocketConnections`'s reconnect effect may fire. Because layout effects run\n * before regular effects, `replaceUrl` wins and updates the URL first; the reconnect call\n * is safely skipped.\n */\n private teardownAndReconnect = async () => {\n if (this._isReconnecting) return;\n this._isReconnecting = true;\n try {\n this.teardownSocket();\n this._listeners.forEach((listener) => listener.reset());\n this.reconnectTries = 0;\n this._maxRetriesExceeded = false;\n await wait(TEARDOWN_RECONNECT_DELAY_MS);\n this.connect();\n } finally {\n this._isReconnecting = false;\n }\n };\n\n /**\n * Cleans up the WebSocket connection when no listeners are registered.\n */\n private cleanupConnection = () => {\n if (this._listeners.size === 0) {\n this._client.connectionEvent?.({\n type: \"cleanup\",\n url: this._url,\n });\n this.removeListeners();\n this._socket = undefined;\n }\n };\n\n /**\n * Clears all active timers (ping heartbeat, pong timeout, and connection cleanup).\n */\n private clearAllTimers = () => {\n clearTimeout(this.pingTimeOut);\n clearTimeout(this.pongTimeOut);\n clearTimeout(this.closeConnectionTimeOut);\n };\n\n /**\n * Removes all event listeners from the WebSocket and window objects.\n * Used during cleanup and reconnection processes.\n */\n private removeListeners = () => {\n this._socket?.removeEventListener(\"message\", this.handleMessage);\n this._socket?.removeEventListener(\"close\", this.handleClose);\n this._socket?.removeEventListener(\"open\", this.handleOpen);\n this._socket?.removeEventListener(\"error\", this.handleError);\n\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnline);\n window.removeEventListener(\"online\", this.handleOnlineForReconnection);\n window.removeEventListener(\"offline\", this.handleOffline);\n }\n };\n\n // ─── Reconnection Logic (Private) ──────────────────────────────────\n\n /**\n * Attempts to reconnect the WebSocket connection with exponential backoff.\n * Shows user notifications after the threshold number of attempts.\n * Only attempts reconnection when the browser is online.\n * When closeCode is 1013 (Try Again Later), waits an extra delay before reconnecting.\n * Stops after configured MAX_RETRY_ATTEMPTS and shows a permanent error with a manual retry button.\n *\n * @param closeCode - Optional WebSocket close code; used to apply TRY_AGAIN_LATER delay when 1013\n */\n private attemptReconnection = async (closeCode?: number) => {\n const { maxRetryAttempts, notificationThreshold, tryAgainLaterDelayMs } =\n this._client;\n if (this.reconnectTries >= maxRetryAttempts) {\n this._maxRetriesExceeded = true;\n this._client.connectionEvent?.({\n type: \"max-retries-exceeded\",\n url: this._url,\n retries: this.reconnectTries,\n });\n return;\n }\n\n if (this.deferReconnectionUntilOnline()) {\n return;\n }\n\n if (closeCode === WEBSOCKET_CLOSE_CODES.TRY_AGAIN_LATER) {\n if (this.reconnectTries > notificationThreshold) {\n this._client.connectionEvent?.({\n type: \"reconnecting\",\n url: this._url,\n retries: this.reconnectTries,\n });\n }\n\n await wait(tryAgainLaterDelayMs);\n if (this.deferReconnectionUntilOnline()) {\n return;\n }\n }\n\n this.reconnectTries++;\n\n const waitTime = reconnectWaitTime(\n this.reconnectTries,\n this._client.delays,\n this._client.phaseThresholds\n );\n\n if (this.reconnectTries > notificationThreshold) {\n this._client.connectionEvent?.({\n type: \"reconnecting\",\n url: this._url,\n retries: this.reconnectTries,\n });\n }\n await wait(waitTime);\n\n // Check again after waiting - browser might have gone offline during the wait\n if (this.deferReconnectionUntilOnline()) {\n return;\n }\n\n if (this.reconnectTries > notificationThreshold) {\n this._client.connectionEvent?.({\n type: \"reconnecting\",\n url: this._url,\n retries: this.reconnectTries,\n });\n }\n this.connect();\n };\n\n /**\n * Checks if the browser is offline and, if so, defers reconnection until it comes back online\n * by registering a one-time 'online' event listener.\n *\n * @returns `true` if reconnection was deferred (browser is offline), `false` if browser is online\n */\n private deferReconnectionUntilOnline = (): boolean => {\n if (isBrowserOnline()) {\n return false;\n }\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", this.handleOnlineForReconnection, {\n once: true,\n });\n }\n return true;\n };\n\n // ─── WebSocket Event Handlers ──────────────────────────────────────\n\n /**\n * Handles WebSocket close events.\n *\n * Implements automatic reconnection for any non-intentional close (anything other than\n * 1000 Normal Closure). This includes 1001 Going Away, 1011 Internal Error, 1012 Service\n * Restart, 1013 Try Again Later, 1006 Abnormal Closure, and other server-initiated codes.\n * Reconnection only occurs when listeners are still registered. Shows user notifications\n * after {@link RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD} failed attempts.\n * Cleans up the connection if no listeners remain. Logs the close event via the custom logger if configured.\n *\n * @param event - The WebSocket close event containing code, reason, and whether the close was clean\n */\n private handleClose = async (event: CloseEvent) => {\n this.clearAllTimers();\n\n this._client.connectionEvent?.({\n type: \"close\",\n url: this._url,\n code: event.code,\n reason: event.reason,\n wasClean: event.wasClean,\n subscriptions: this._listeners.size,\n });\n\n const shouldReconnect = isReconnectableCloseCode(event.code);\n const hasRegisteredApis = this._listeners.size > 0;\n\n if (shouldReconnect && hasRegisteredApis) {\n await this.attemptReconnection(event.code);\n }\n\n this.cleanupConnection();\n };\n\n /**\n * Handles WebSocket open/connected events.\n *\n * Sets up offline detection, dismisses reconnection notifications, shows success message\n * for recovered connections (only if {@link RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD}\n * was exceeded), resets reconnection counter, notifies all listeners, flushes cached\n * messages, and initiates the heartbeat ping sequence.\n */\n private handleOpen = () => {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"offline\", this.handleOffline);\n }\n\n this.reconnectTries = 0;\n\n const socket = this._socket;\n if (socket) {\n this._listeners.forEach((listener) => listener.onOpen?.());\n\n this._client.connectionEvent?.({\n type: \"open\",\n url: this._url,\n retries: this.reconnectTries,\n uriApis: getSubscriptionUris(this._listeners),\n });\n this.cachedMessages.forEach((message) =>\n socket.send(this.serializeMessage(message))\n );\n }\n this.cachedMessages = [];\n if (this._client.heartbeat.enabled) {\n this.schedulePing();\n }\n };\n\n /**\n * Handles incoming WebSocket messages.\n *\n * Routes messages to matching listeners: subscription APIs by URI, message APIs by pending request URI.\n * Special handling for 'ping' messages to maintain heartbeat.\n * Dispatches error-method messages to listener error handlers.\n *\n * @param event - The WebSocket message event containing JSON data\n */\n private handleMessage = (event: MessageEvent<string>) => {\n try {\n const parsed: unknown = JSON.parse(event.data);\n\n if (!isValidIncomingMessage(parsed)) {\n this._client.connectionEvent?.({\n type: \"invalid-message\",\n url: this._url,\n uriApis: getSubscriptionUris(this._listeners),\n message: parsed,\n });\n this._listeners.forEach((listener) =>\n listener.onError({ type: \"transport\", event })\n );\n return;\n }\n\n if (parsed.uri === \"ping\") {\n this.clearPongTimeout();\n if (this._client.heartbeat.enabled) {\n this.schedulePing();\n }\n return;\n }\n\n if (isErrorMethod(parsed.method)) {\n this._client.connectionEvent?.({\n type: \"message-error\",\n url: this._url,\n uri: parsed.uri,\n uriApis: getSubscriptionUris(this._listeners),\n message: parsed,\n });\n this.forEachMatchingListener(parsed.uri, (listener) =>\n listener.onMessageError!({ type: \"server\", message: parsed })\n );\n return;\n }\n\n this.forEachMatchingListener(parsed.uri, (listener) => {\n if (listener.uri === parsed.uri) {\n listener.onMessage?.(parsed.body);\n } else {\n listener.deliverMessage?.(parsed.uri, parsed.body);\n }\n });\n } catch (error) {\n this._client.connectionEvent?.({\n type: \"parse-error\",\n url: this._url,\n uriApis: getSubscriptionUris(this._listeners),\n message: event.data,\n error: error,\n });\n this._listeners.forEach((listener) =>\n listener.onError({ type: \"transport\", event })\n );\n }\n };\n\n /**\n * Handles WebSocket error events.\n * Logs the error via the custom logger if configured and notifies all registered listeners.\n *\n * @param event - The WebSocket error event\n */\n private handleError = (event: Event) => {\n this._listeners.forEach((listener) =>\n listener.onError({ type: \"transport\", event })\n );\n\n this._client.connectionEvent?.({\n type: \"error\",\n url: this._url,\n uriApis: getSubscriptionUris(this._listeners),\n event: event,\n });\n };\n\n // ─── Browser Online/Offline Handlers ───────────────────────────────\n\n /**\n * Handles browser coming back online during offline detection.\n * Removes the online listener and re-establishes the connection.\n */\n private handleOnline = () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnline);\n }\n this.connect();\n };\n\n /**\n * Handles browser coming back online during reconnection attempts.\n * Removes the online listener and resumes reconnection with a decremented counter\n * to avoid adding extra wait time from being offline.\n */\n private handleOnlineForReconnection = () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnlineForReconnection);\n }\n this.reconnectTries--;\n this.attemptReconnection();\n };\n\n /**\n * Handles browser going offline.\n *\n * Notifies all listeners of the closure, tears down the socket, and sets up\n * a listener to reconnect when the browser comes back online.\n */\n private handleOffline = () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"offline\", this.handleOffline);\n }\n if (this._socket) {\n this._listeners.forEach((listener) =>\n listener.onClose(new CloseEvent(\"offline\"))\n );\n }\n this.teardownSocket();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", this.handleOnline);\n }\n };\n\n // ─── Message Utilities (Private) ───────────────────────────────────\n\n /**\n * Handles outgoing messages from listeners.\n *\n * - If socket is OPEN: serializes with correlation ID and sends immediately.\n * - If socket is not open: subscribe messages trigger connect only; other messages are\n * cached and sent when the connection opens.\n *\n * Passed to each listener via {@link WebsocketListener.setSendToConnection}.\n */\n private handleSendMessage = (message: SendMessage<string, string, any>) => {\n if (this._socket?.readyState === WebSocket.OPEN) {\n this._client.connectionEvent?.({\n type: \"send-message\",\n url: this._url,\n uri: message.uri,\n body: message.body,\n method: message.method,\n });\n this._socket.send(this.serializeMessage(message));\n return;\n }\n\n if (message.method !== \"subscribe\") {\n this.cachedMessages.push(message);\n }\n this.connect();\n };\n\n /**\n * Sends a heartbeat ping message to keep the connection alive and detect disconnections.\n * Sets a pong timeout; if no pong arrives within HEARTBEAT_CONFIG.PONG_TIMEOUT_MS,\n * the connection is force-closed to trigger reconnection.\n */\n private sendPing = () => {\n if (!isSocketOnline(this._socket)) return;\n this._socket?.send(this.serializeMessage(createPingMessage()));\n this.schedulePongTimeout();\n };\n\n /**\n * Clears the pong timeout (e.g. when a pong is received).\n */\n private clearPongTimeout = () => {\n clearTimeout(this.pongTimeOut);\n this.pongTimeOut = undefined;\n };\n\n /**\n * Schedules a timeout to detect missing pong. If no pong arrives within\n * configured pong timeout, force-closes the socket to trigger reconnection.\n */\n private schedulePongTimeout = () => {\n this.clearPongTimeout();\n const pongTimeoutMs = this._client.heartbeat.pongTimeoutMs;\n this.pongTimeOut = setTimeout(() => {\n this._client.connectionEvent?.({\n type: \"pong-timeout\",\n url: this._url,\n });\n this.teardownSocket();\n this.attemptReconnection();\n }, pongTimeoutMs);\n };\n\n /**\n * Schedules the next heartbeat ping after the configured interval (40 seconds).\n * @see {@link getPingTime}\n */\n private schedulePing = () => {\n this.pingTimeOut = setTimeout(() => {\n this.sendPing();\n }, getPingTime());\n };\n\n /**\n * Serializes a message with a unique correlation ID for WebSocket transmission.\n * @param message - The message to serialize\n * @returns JSON string for WebSocket send\n */\n private serializeMessage = (\n message: SendMessage<string, string, any>\n ): string => {\n const transformMessagePayload = this._client.transformMessagePayload;\n if (transformMessagePayload) {\n message = transformMessagePayload(message);\n }\n return JSON.stringify(message);\n };\n\n /**\n * Executes a callback for each listener that matches the given URI.\n *\n * - **Subscription listeners**: Match when `listener.uri === uri`\n * - **Message listeners**: Match when `listener.hasWaitingUri(uri)` (pending request/response)\n *\n * A single message can be delivered to multiple listeners if both a subscription\n * and a message API are waiting for the same URI.\n *\n * @param uri - The URI from the incoming message\n * @param callback - Callback invoked for each matching listener\n */\n private forEachMatchingListener = (\n uri: string,\n callback: (listener: WebsocketListener) => void\n ) => {\n this._listeners.forEach((listener) => {\n if (listener.uri === uri || listener.hasWaitingUri?.(uri)) {\n callback(listener);\n }\n });\n };\n}\n","/**\n * @fileoverview Global WebSocket configuration for all WebsocketConnection instances.\n *\n * Provides a single source of truth for connection behavior. Instantiate with\n * {@link WebsocketClientOverrides} at app startup to customize defaults.\n * Use the connectionEvent callback to configure event logging.\n *\n * @module WebsocketClient\n */\n\nimport { Store } from '@tanstack/react-store';\nimport {\n CONNECTION_CLEANUP_DELAY_MS,\n DEFAULT_HEARTBEAT_CONFIG,\n DEFAULT_MESSAGE_RESPONSE_TIMEOUT_MS,\n RECONNECTION_CONFIG\n} from './constants';\nimport type { HeartbeatConfig, SendMessage, WebsocketClientOverrides, WebsocketListener, WebsocketLoggerConnectionEvent } from './types';\nimport { WebsocketConnection } from './WebsocketConnection';\nimport type { WebsocketMessageApi } from './WebsocketMessageApi';\nimport type { WebsocketSubscriptionApi } from './WebsocketSubscriptionApi';\n\n/**\n * Global WebSocket configuration used by all WebsocketConnection instances.\n *\n * Instantiate with {@link WebsocketClientOverrides} to customize behavior.\n * All overrides are merged with defaults; partial overrides are supported.\n *\n * @example\n * ```typescript\n * const client = new WebsocketClient({\n * maxRetryAttempts: 10,\n * heartbeat: { enabled: false },\n * messageResponseTimeoutMs: 5000\n * });\n * ```\n */\nexport class WebsocketClient {\n /**\n * Global map of active WebSocket connections, keyed by URL.\n *\n * One connection per key. Managed by {@link addConnection} and {@link removeConnection}.\n */\n private _connections = new Store<Map<string, WebsocketConnection>>(new Map());\n\n /**\n * Global map of active WebSocket listeners (subscription and message APIs), keyed by API key.\n *\n * One listener per key. Subscription APIs have `uri`; message APIs have `hasWaitingUri`.\n * Managed by {@link createWebsocketSubscriptionApi}, {@link createWebsocketMessageApi},\n * and {@link removeWebsocketListenerFromConnection}.\n */\n private _listeners = new Store<Map<string, WebsocketListener>>(new Map());\n\n /** Maximum reconnection attempts before stopping. */\n public maxRetryAttempts: number;\n /** Attempts before showing user notifications. */\n public notificationThreshold: number;\n /** Delay (ms) when server closes with 1013 Try Again Later. */\n public tryAgainLaterDelayMs: number;\n /** Delay durations (ms) for each reconnection phase. */\n public delays: {\n firstPhase: number;\n secondPhase: number;\n thirdPhase: number;\n };\n public phaseThresholds: {\n first: number;\n second: number;\n };\n /** Delay (ms) before closing connection when no listeners remain. */\n public connectionCleanupDelayMs: number;\n /** Default timeout (ms) for message API responses. */\n public messageResponseTimeoutMs: number;\n /** Heartbeat (ping/pong) configuration. */\n public heartbeat: HeartbeatConfig;\n /** Optional transform for outgoing message payloads. */\n public transformMessagePayload: ((payload: SendMessage<string, string, unknown>) => SendMessage<string, string, unknown>) | undefined;\n /** Optional callback for connection event logging. */\n public connectionEvent: ((event: WebsocketLoggerConnectionEvent) => void) | undefined;\n\n /**\n * Creates a new WebsocketClient with optional overrides.\n *\n * All overrides are merged with defaults from {@link RECONNECTION_CONFIG},\n * {@link CONNECTION_CLEANUP_DELAY_MS}, and {@link DEFAULT_HEARTBEAT_CONFIG}.\n *\n * @param overrides - Partial configuration overrides. Omitted values use defaults.\n */\n constructor({\n maxRetryAttempts,\n notificationThreshold,\n tryAgainLaterDelayMs,\n delays,\n phaseThresholds,\n connectionCleanupDelayMs,\n messageResponseTimeoutMs,\n heartbeat,\n transformMessagePayload,\n connectionEvent\n }: WebsocketClientOverrides) {\n this.maxRetryAttempts = maxRetryAttempts ?? RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS;\n this.notificationThreshold = notificationThreshold ?? RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD;\n this.tryAgainLaterDelayMs = tryAgainLaterDelayMs ?? RECONNECTION_CONFIG.TRY_AGAIN_LATER_DELAY_MS;\n this.delays = {\n firstPhase: delays?.firstPhase ?? RECONNECTION_CONFIG.DELAYS.FIRST_PHASE,\n secondPhase: delays?.secondPhase ?? RECONNECTION_CONFIG.DELAYS.SECOND_PHASE,\n thirdPhase: delays?.thirdPhase ?? RECONNECTION_CONFIG.DELAYS.THIRD_PHASE\n };\n this.phaseThresholds = {\n first: phaseThresholds?.first ?? RECONNECTION_CONFIG.PHASE_THRESHOLDS.FIRST,\n second: phaseThresholds?.second ?? RECONNECTION_CONFIG.PHASE_THRESHOLDS.SECOND\n };\n this.connectionCleanupDelayMs = connectionCleanupDelayMs ?? CONNECTION_CLEANUP_DELAY_MS;\n this.messageResponseTimeoutMs = messageResponseTimeoutMs ?? DEFAULT_MESSAGE_RESPONSE_TIMEOUT_MS;\n this.heartbeat = {\n enabled: heartbeat?.enabled ?? DEFAULT_HEARTBEAT_CONFIG.enabled,\n pongTimeoutMs: heartbeat?.pongTimeoutMs ?? DEFAULT_HEARTBEAT_CONFIG.pongTimeoutMs\n };\n this.transformMessagePayload = transformMessagePayload ?? undefined;\n\n this.connectionEvent = connectionEvent ?? undefined;\n }\n\n /** Reconnects all active WebSocket connections. Use after auth/region change. */\n public reconnectAllConnections = () => {\n this._connections.state.forEach((connection) => {\n connection.reconnect();\n });\n };\n\n /** Registers a listener (subscription or message API) in the client. */\n public addListener = (listener: WebsocketListener) => {\n this._listeners.setState((prev) => {\n const next = new Map(prev);\n next.set(listener.key, listener);\n return next;\n });\n };\n\n /** Unregisters a listener from the client. */\n public removeListener = (listener: WebsocketListener) => {\n this._listeners.setState((prev) => {\n const next = new Map(prev);\n next.delete(listener.key);\n return next;\n });\n };\n\n /**\n * Returns a listener by key and type.\n *\n * @param key - The listener's unique key\n * @param type - `'subscription'` or `'message'`\n * @returns The listener if found, otherwise undefined\n */\n public getListener<TData = unknown, TBody = unknown>(\n key: string,\n type: 'subscription'\n ): WebsocketSubscriptionApi<TData, TBody> | undefined;\n public getListener(key: string, type: 'message'): WebsocketMessageApi | undefined;\n public getListener<TData = unknown, TBody = unknown>(\n key: string,\n type: 'subscription' | 'message'\n ): WebsocketSubscriptionApi<TData, TBody> | WebsocketMessageApi | undefined {\n const listener = this._listeners.state.get(key);\n if (listener && listener.type === type) {\n return listener as WebsocketSubscriptionApi<TData, TBody> | WebsocketMessageApi;\n }\n return undefined;\n }\n\n /** Returns the WebSocket connection for the given URL key, or undefined. */\n public getConnection = (key: string): WebsocketConnection | undefined => {\n return this._connections.state.get(key);\n };\n\n /**\n * Adds or returns an existing WebSocket connection for the given URL.\n *\n * @param key - The key used to identify the connection (typically the URL)\n * @param url - The WebSocket URL to connect to\n * @returns The existing or newly created connection\n */\n public addConnection = (key: string, url: string) => {\n const existingConnection = this._connections.state.get(key);\n if (existingConnection) {\n return existingConnection;\n }\n const connection = new WebsocketConnection(url, this);\n this._connections.setState((prev) => {\n const next = new Map(prev);\n next.set(key, connection);\n return next;\n });\n return connection;\n };\n\n /**\n * Removes a connection from the client.\n *\n * @param url - The WebSocket URL used as the key when calling {@link addConnection}.\n */\n public removeConnection = (url: string) => {\n this._connections.setState((prev) => {\n const next = new Map(prev);\n next.delete(url);\n return next;\n });\n };\n}\n","/**\n * React context provider for WebSocket client.\n *\n * @module WebsocketProvider\n */\n\nimport { createContext, FunctionComponent, PropsWithChildren, useContext } from 'react';\nimport { WebsocketClient } from './WebsocketClient';\n\nconst WebsocketClientContext = createContext<WebsocketClient | undefined>(undefined);\n\n/**\n * Returns the {@link WebsocketClient} from the nearest {@link WebsocketClientProvider}.\n *\n * Must be used within a `WebsocketClientProvider`; throws otherwise.\n *\n * @returns The WebsocketClient instance\n * @throws Error if used outside WebsocketClientProvider\n *\n * @example\n * ```typescript\n * const client = useWebsocketClient();\n * const api = useWebsocketSubscription({ key: 'my-sub', url: '...', uri: '...' });\n * ```\n */\nexport const useWebsocketClient = (): WebsocketClient => {\n const client = useContext(WebsocketClientContext);\n if (!client) {\n throw new Error('useWebsocketClient must be used within a WebsocketClientProvider');\n }\n return client;\n};\n\n/** Props for {@link WebsocketClientProvider}. */\ninterface WebsocketClientProviderProps {\n /** The WebsocketClient instance to provide to descendants. */\n client: WebsocketClient;\n}\n\n/**\n * Provides a {@link WebsocketClient} to the component tree.\n *\n * Wrap your app (or the part that uses WebSocket hooks) with this provider.\n * Create the client once (e.g. at app startup) and pass it here.\n *\n * @example\n * ```typescript\n * const client = new WebsocketClient({ maxRetryAttempts: 10 });\n * <WebsocketClientProvider client={client}>\n * <App />\n * </WebsocketClientProvider>\n * ```\n */\nexport const WebsocketClientProvider: FunctionComponent<PropsWithChildren<WebsocketClientProviderProps>> = ({ children, client }) => {\n return <WebsocketClientContext.Provider value={client}>{children}</WebsocketClientContext.Provider>;\n};\n","/**\n * @fileoverview WebSocket Message API for request/response style messaging.\n *\n * Send to any URI; optionally await a response. No subscription support.\n * Used by {@link useWebsocketMessage}. See {@link WebsocketSubscriptionApi} for\n * streaming subscriptions.\n *\n * @module WebsocketMessageApi\n */\n\nimport { WebsocketClient } from './WebsocketClient';\nimport { INITIATOR_REMOVAL_DELAY_MS } from './constants';\nimport {\n SendMessage,\n SendMessageOptions,\n SendToConnectionFn,\n WebsocketListener,\n WebsocketMessageOptions,\n WebsocketServerError,\n WebsocketTransportError\n} from './types';\n\ninterface PendingRequest<TData = unknown> {\n resolve: (value: TData) => void;\n reject: (reason: unknown) => void;\n timeoutId: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Manages WebSocket request/response messaging without subscription.\n *\n * Use for one-off commands (validate, modify, mark read) rather than streaming.\n * Send to any URI; optionally await a response. Tracks URIs only while waiting.\n *\n * ## Key Features\n *\n * - **Any URI**: Not bound to a single URI like {@link WebsocketSubscriptionApi}\n * - **Request/Response**: `sendMessage` returns a Promise; optional per-call timeout\n * - **Fire-and-forget**: `sendMessageNoWait` for commands that don't need a response\n * - **No Subscription**: Use WebsocketSubscriptionApi for streaming data\n *\n * ## Edge Cases\n *\n * - **Overwrite**: Sending to the same URI while a request is pending cancels the previous\n * request — the previous Promise rejects with \"WebSocket request overwritten for URI\".\n * - **Disabled**: When `enabled=false`, `sendMessage` rejects; `sendMessageNoWait` is a no-op.\n * - **Connection closed**: All pending requests reject with \"WebSocket connection closed\".\n * - **Queued messages**: If the connection is not yet open, messages are queued and sent\n * when the connection opens (via `setSendToConnection`).\n *\n * ## Cleanup\n *\n * {@link reset} is called by WebsocketConnection when the URL changes or during reconnection.\n * When the last hook unmounts, {@link unregisterHook} triggers removal after\n * {@link INITIATOR_REMOVAL_DELAY_MS}.\n *\n * @template TData - The type of data received in the response\n * @template TBody - The type of message body sent to the WebSocket\n *\n * @example\n * ```typescript\n * const api = new WebsocketMessageApi<MyResponse, MyRequest>({\n * url: 'wss://example.com',\n * key: 'my-message-api',\n * responseTimeoutMs: 5000\n * });\n * connection.addListener(api);\n *\n * const response = await api.sendMessage('/api/command', 'post', { action: 'refresh' });\n * ```\n */\nexport class WebsocketMessageApi implements WebsocketListener {\n private _options: WebsocketMessageOptions;\n private _sendToConnection: SendToConnectionFn | null = null;\n private _pendingByUri: Map<string, PendingRequest> = new Map();\n private _pendingMessages: SendMessage<string, string, unknown>[] = [];\n private _registeredHooks: Set<string> = new Set();\n private _hookRemovalTimeout: ReturnType<typeof setTimeout> | undefined;\n private _client: WebsocketClient;\n public readonly type = 'message';\n\n /**\n * Creates a new WebsocketMessageApi.\n *\n * @param options - Configuration options (url, key, callbacks, etc.)\n * @param client - The {@link WebsocketClient} for timeout defaults and connection management\n */\n constructor(options: WebsocketMessageOptions, client: WebsocketClient) {\n this._client = client;\n const defaultTimeout = client.messageResponseTimeoutMs;\n this._options = {\n enabled: true,\n responseTimeoutMs: defaultTimeout,\n ...options\n };\n }\n\n /** Unique key identifier for this Message API. */\n public get key(): string {\n return this._options.key;\n }\n\n /** WebSocket URL for Datadog tracking. */\n public get url(): string {\n return this._options.url;\n }\n\n /** Whether this Message API is enabled. */\n public get isEnabled(): boolean {\n return this._options.enabled ?? true;\n }\n\n /**\n * Returns whether this API is waiting for a response for the given URI.\n *\n * Used by {@link WebsocketConnection} to route incoming messages to the correct\n * listener. Message API receives messages only for URIs with pending requests.\n *\n * @param uri - The URI to check\n * @returns `true` if a request is pending for this URI\n */\n public hasWaitingUri = (uri: string): boolean => {\n return this._pendingByUri.has(uri);\n };\n\n /**\n * Registers a hook (component) that is using this Message API.\n *\n * Tracks the hook ID so the API is only removed from the connection when\n * the last hook unmounts.\n *\n * @param id - Unique identifier for the registering hook\n */\n public registerHook = (id: string): void => {\n this._clearHookRemovalTimeout();\n this._registeredHooks.add(id);\n };\n\n /**\n * Unregisters a hook from this Message API.\n *\n * After {@link INITIATOR_REMOVAL_DELAY_MS}, if no hooks remain, invokes the\n * cleanup callback to remove this API from the connection. The delay prevents\n * rapid subscribe/unsubscribe cycles during React re-renders.\n *\n * @param id - The hook ID to unregister\n * @param onRemove - Callback invoked when the last hook is removed (after delay)\n */\n public unregisterHook = (id: string, onRemove: () => void): void => {\n this._registeredHooks.delete(id);\n this._scheduleHookRemoval(onRemove);\n };\n\n /**\n * Disconnects this Message API from the parent WebSocket connection.\n *\n * Called when the hook is disabled. After a delay, invokes the cleanup callback.\n * Clears any pending hook-removal timeout to avoid duplicate cleanup.\n *\n * @param onRemoveFromSocket - Callback invoked after delay to remove from connection\n */\n public disconnect = (onRemoveFromSocket: () => void): void => {\n this._clearHookRemovalTimeout();\n this._hookRemovalTimeout = setTimeout(() => {\n this._hookRemovalTimeout = undefined;\n onRemoveFromSocket();\n }, INITIATOR_REMOVAL_DELAY_MS);\n };\n\n /**\n * Sets or clears the callback used to send messages through the parent WebSocket connection.\n *\n * When setting a callback, flushes any queued messages. When clearing, cancels all\n * pending requests and clears the hook removal timeout to avoid redundant cleanup.\n *\n * @param callback - The send function, or null to disconnect\n */\n public setSendToConnection = (callback: SendToConnectionFn | null): void => {\n this._sendToConnection = callback;\n\n if (callback) {\n this._flushPendingMessages(callback);\n } else {\n this._clearHookRemovalTimeout();\n this._pendingMessages = [];\n this._cancelAllPending();\n }\n };\n\n /**\n * Delivers an incoming message for a URI we're waiting on.\n *\n * Called by WebsocketConnection when a message arrives for a URI with a pending request.\n *\n * @param uri - The URI the response is for\n * @param data - The response data\n */\n public deliverMessage = (uri: string, data: unknown): void => {\n const pending = this._pendingByUri.get(uri);\n if (!pending) return;\n\n clearTimeout(pending.timeoutId);\n this._pendingByUri.delete(uri);\n pending.resolve(data);\n };\n\n /**\n * Sends a message to the given URI and optionally waits for a response.\n *\n * **Overwrite behavior**: If a request is already pending for this URI, it is\n * cancelled (rejected with \"WebSocket request overwritten for URI\") and replaced.\n *\n * @param uri - The URI to send the message to\n * @param bodyOrMethod - Message body (short form) or HTTP method (full form)\n * @param bodyOrOptions - Message body or options (full form)\n * @param options - Per-call options when using full signature\n * @returns Promise that resolves with the response data; rejects on timeout, overwrite, or disabled\n *\n * @example\n * await api.sendMessage('/api/command', 'post', { action: 'refresh' });\n * await api.sendMessage('/api/command', 'post', { action: 'refresh' }, { timeout: 5000 });\n */\n public sendMessage<TData = unknown, TBody = unknown>(\n uri: string,\n method: string,\n body?: TBody,\n options?: SendMessageOptions\n ): Promise<TData> {\n if (!this.isEnabled) {\n return Promise.reject(new Error('WebsocketMessageApi is disabled'));\n }\n\n this._cancelPendingForUri(uri);\n\n const timeoutMs = options?.timeout ?? this._options.responseTimeoutMs ?? this._client.messageResponseTimeoutMs;\n\n return new Promise<TData>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n if (this._pendingByUri.get(uri)?.timeoutId === timeoutId) {\n this._pendingByUri.delete(uri);\n reject(new Error(`WebSocket response timeout for URI: ${uri}`));\n }\n }, timeoutMs);\n\n this._pendingByUri.set(uri, {\n resolve: (v: unknown) => resolve(v as TData),\n reject,\n timeoutId\n });\n\n const message: SendMessage<string, string, TBody> = { uri, method, body };\n this._sendOrQueue<TBody>(message);\n });\n }\n\n /**\n * Sends a message without waiting for a response (fire-and-forget).\n *\n * @param uri - The URI to send the message to\n * @param methodOrBody - HTTP method (full form) or message body (short form)\n * @param body - Message body when using full form\n *\n * @example\n * api.sendMessageNoWait('/api/log', 'post', { event: 'click' });\n */\n public sendMessageNoWait<TBody = unknown>(uri: string, method: string, body?: TBody): void {\n if (!this.isEnabled) return;\n\n const message: SendMessage<string, string, TBody> = { uri, method, body };\n this._sendOrQueue<TBody>(message);\n }\n\n /** @inheritdoc */\n public onError = (error: WebsocketTransportError): void => {\n this._options.onError?.(error);\n };\n\n /** @inheritdoc */\n public onMessageError = (error: WebsocketServerError): void => {\n this._options.onMessageError?.(error);\n };\n\n /** @inheritdoc */\n public onClose = (event: CloseEvent): void => {\n this._cancelAllPending();\n this._options.onClose?.(event);\n };\n\n /**\n * Resets this Message API, cancelling all pending requests.\n *\n * Called by WebsocketConnection when the URL changes or during reconnection.\n * Clears the hook removal timeout to prevent stale cleanup callbacks.\n */\n public reset = (): void => {\n this._clearHookRemovalTimeout();\n this._cancelAllPending();\n };\n\n private _clearHookRemovalTimeout(): void {\n if (this._hookRemovalTimeout !== undefined) {\n clearTimeout(this._hookRemovalTimeout);\n this._hookRemovalTimeout = undefined;\n }\n }\n\n private _scheduleHookRemoval(onRemove: () => void): void {\n this._clearHookRemovalTimeout();\n this._hookRemovalTimeout = setTimeout(() => {\n this._hookRemovalTimeout = undefined;\n if (this._registeredHooks.size === 0) {\n onRemove();\n }\n }, INITIATOR_REMOVAL_DELAY_MS);\n }\n\n private _flushPendingMessages(callback: SendToConnectionFn): void {\n if (this._pendingMessages.length === 0) return;\n this._pendingMessages.forEach((msg) => callback(msg));\n this._pendingMessages = [];\n }\n\n private _sendOrQueue<TBody = unknown>(message: SendMessage<string, string, TBody>): void {\n if (this._sendToConnection) {\n this._sendToConnection(message);\n } else {\n this._pendingMessages.push(message);\n }\n }\n\n private _cancelPendingForUri(uri: string): void {\n const pending = this._pendingByUri.get(uri);\n if (pending) {\n clearTimeout(pending.timeoutId);\n this._pendingByUri.delete(uri);\n pending.reject(new Error(`WebSocket request overwritten for URI: ${uri}`));\n }\n }\n\n private _cancelAllPending(): void {\n this._pendingByUri.forEach((pending) => {\n clearTimeout(pending.timeoutId);\n pending.reject(new Error('WebSocket connection closed'));\n });\n this._pendingByUri.clear();\n }\n}\n","/**\n * @fileoverview WebSocket subscription API for streaming data over a single URI.\n *\n * Manages subscribe/unsubscribe lifecycle, reactive store updates, and hook tracking.\n * Used by {@link useWebsocketSubscription}. See {@link WebsocketMessageApi} for\n * request/response style messaging.\n *\n * @module WebsocketSubscriptionApi\n */\n\nimport { Store } from '@tanstack/react-store';\nimport { deepEqual } from 'fast-equals';\nimport { DEFAULT_URI_OPTIONS, INITIATOR_REMOVAL_DELAY_MS } from './constants';\nimport {\n createInitialWebsocketSubscriptionStore,\n SendMessage,\n SendToConnectionFn,\n WebsocketListener,\n WebsocketServerError,\n WebsocketSubscriptionOptions,\n WebsocketSubscriptionStore,\n WebsocketTransportError\n} from './types';\n\n/**\n * Manages a single WebSocket URI endpoint with subscription lifecycle and message handling.\n *\n * Use for streaming data (voyage list, notifications). Provides a TanStack Store for\n * reactive updates. Multiple components share one instance via a unique key.\n *\n * ## Key Features\n *\n * - **Reactive Store**: TanStack Store updates when messages are received\n * - **pendingSubscription**: `true` from subscribe until first message (use for loading states)\n * - **Auto-subscribe**: Subscribes when the WebSocket connection opens\n * - **Hook Tracking**: Tracks components; unsubscribes when the last hook unmounts\n *\n * ## Edge Cases\n *\n * - **Multiple initiators**: Using the same key in multiple components emits a console warning;\n * multiple initiators can cause unexpected behavior.\n * - **Body change in subscribe-on-open**: When `options.body` changes, re-subscribes automatically.\n * - **enabled=false**: Unsubscribes and disconnects; re-enabling triggers subscribe.\n * - **reset**: Called by connection on URL change/reconnect; clears store state.\n *\n * ## Cleanup\n *\n * {@link reset} is called by WebsocketConnection on URL change or reconnection.\n * {@link unregisterHook} triggers removal after {@link INITIATOR_REMOVAL_DELAY_MS}.\n *\n * @template TData - The type of data received from the WebSocket\n * @template TBody - The type of message body sent\n *\n * @example\n * ```typescript\n * const api = new WebsocketSubscriptionApi<MyData, MyBody>({\n * url: 'wss://example.com',\n * uri: '/api/stream',\n * key: 'my-stream-key',\n * body: { filter: 'active' },\n * onMessage: (data) => console.log('Received:', data)\n * });\n *\n * const data = useSelector(api.store, (s) => s.message);\n * const isPending = useSelector(api.store, (s) => s.pendingSubscription);\n * api.sendMessage({ method: 'refresh', body: { force: true } });\n * ```\n *\n * @see {@link useWebsocketSubscription} - React hook\n * @see {@link WebsocketConnection} - Connection manager\n */\nexport class WebsocketSubscriptionApi<TData = unknown, TBody = unknown> implements WebsocketListener {\n private _options: WebsocketSubscriptionOptions<TData, TBody>;\n private _state: Store<WebsocketSubscriptionStore<TData>> = new Store<WebsocketSubscriptionStore<TData>>(\n createInitialWebsocketSubscriptionStore<TData>()\n );\n private _registeredHooks: Set<string> = new Set();\n private _disconnectTimeout: ReturnType<typeof setTimeout> | undefined;\n private _hookRemovalTimeout: ReturnType<typeof setTimeout> | undefined;\n private _sendToConnection: SendToConnectionFn | null = null;\n private _pendingMessages: SendMessage<string, string, TBody>[] = [];\n public readonly type = 'subscription';\n\n /**\n * Creates a new WebsocketSubscriptionApi.\n *\n * @param options - Configuration options (url, uri, key, callbacks, etc.)\n */\n constructor(options: WebsocketSubscriptionOptions<TData, TBody>) {\n this._options = { ...DEFAULT_URI_OPTIONS, ...options };\n }\n\n /** Unique key identifier for this WebSocket URI API. */\n public get key(): string {\n return this._options.key;\n }\n\n /** URI path for this WebSocket subscription. */\n public get uri(): string {\n return this._options.uri;\n }\n\n /** WebSocket URL for Datadog tracking. */\n public get url(): string {\n return this._options.url;\n }\n\n /** Configuration options for this WebSocket URI. */\n public get options(): WebsocketSubscriptionOptions<TData, TBody> {\n return this._options;\n }\n\n /**\n * Current data from the store.\n *\n * **Do not use in React components** — it does not trigger re-renders. Use\n * `useSelector(api.store, (s) => s.message)` for reactive updates.\n */\n public get data(): TData | undefined {\n return this._state.state.message;\n }\n\n /** TanStack store containing subscription state (message, subscribed, connected, pendingSubscription, etc.). */\n public get store(): Store<WebsocketSubscriptionStore<TData>> {\n return this._state;\n }\n\n /** Whether this WebSocket URI is enabled. */\n public get isEnabled(): boolean {\n return this._options.enabled ?? true;\n }\n\n /**\n * Updates the configuration options for this subscription.\n *\n * Handles lifecycle changes:\n * - **Body change** (subscribe-on-open): Re-subscribes with new body\n * - **Enabled: false → true**: Subscribes\n * - **Enabled: true → false**: Unsubscribes\n *\n * Uses deep equality to skip no-op updates.\n *\n * @param options - New options (merged with existing)\n */\n public set options(options: WebsocketSubscriptionOptions<TData, TBody>) {\n const updatedOptions: WebsocketSubscriptionOptions<TData, TBody> = {\n ...DEFAULT_URI_OPTIONS,\n ...this._options,\n ...options\n };\n\n if (deepEqual(this._options, updatedOptions)) return;\n\n const previousOptions = this._options;\n this._options = updatedOptions;\n\n this._handleSubscriptionUpdates(previousOptions, updatedOptions);\n this._handleUnsubscribeOnDisable(previousOptions, updatedOptions);\n }\n\n /**\n * Sets or clears the callback used to send messages through the parent WebSocket connection.\n *\n * When clearing, flushes pending messages and clears removal timeouts to avoid redundant cleanup.\n *\n * @param callback - The send function, or null to disconnect\n */\n public setSendToConnection = (callback: SendToConnectionFn | null): void => {\n this._sendToConnection = callback;\n\n if (callback) {\n this._flushPendingMessages(callback);\n } else {\n this._clearPendingTimeouts();\n this._pendingMessages = [];\n }\n };\n\n /**\n * Registers a hook (component) that is using this subscription.\n *\n * Clears pending removal/disconnect timeouts and tracks the hook ID.\n * Emits a console warning if more than one hook is registered (multiple initiators).\n *\n * @param id - Unique identifier for the registering hook\n */\n public registerHook = (id: string): void => {\n this._clearPendingTimeouts();\n this._registeredHooks.add(id);\n if (this._registeredHooks.size > 1) {\n console.warn(`the uri ${this.uri} has more than one initiator, multiple initiators could cause unexpected behavior`);\n }\n };\n\n /**\n * Unregisters a hook from this subscription.\n *\n * After {@link INITIATOR_REMOVAL_DELAY_MS}, if no hooks remain, unsubscribes\n * and invokes the cleanup callback. The delay prevents rapid subscribe/unsubscribe\n * during React re-renders.\n *\n * @param id - The hook ID to unregister\n * @param onRemove - Callback invoked when the last hook is removed (after delay)\n */\n public unregisterHook = (id: string, onRemove: () => void): void => {\n this._registeredHooks.delete(id);\n this._scheduleHookRemoval(onRemove);\n };\n\n /**\n * Disconnects this subscription from the parent WebSocket connection.\n *\n * Immediately unsubscribes, then after {@link INITIATOR_REMOVAL_DELAY_MS} invokes\n * the cleanup callback. Called when the hook is disabled (`enabled=false`).\n *\n * @param onRemoveFromSocket - Callback invoked after delay to remove from connection\n */\n public disconnect = (onRemoveFromSocket: () => void): void => {\n this._clearPendingTimeouts();\n this.unsubscribe();\n this._disconnectTimeout = setTimeout(() => {\n this._disconnectTimeout = undefined;\n this._state.setState((prev) => ({ ...prev, connected: false, subscribed: false, pendingSubscription: false }));\n onRemoveFromSocket();\n }, INITIATOR_REMOVAL_DELAY_MS);\n };\n\n /**\n * Resets this subscription to its initial state.\n *\n * Clears connection/subscription state, resets store data, and cancels pending timeouts.\n * Only runs when currently connected. Called by WebsocketConnection on URL change\n * or reconnection.\n */\n public reset = (): void => {\n if (!this._state.state.connected) return;\n\n this._state.setState((prev) => ({\n ...prev,\n connected: false,\n subscribed: false,\n pendingSubscription: false,\n message: undefined\n }));\n this._clearPendingTimeouts();\n };\n\n /**\n * Sends a custom message through the WebSocket for this URI.\n *\n * Automatically appends the URI and method. Queues if connection not yet set.\n *\n * @param message - The message to send (uri and method may be overridden)\n */\n public sendMessage = (message: SendMessage<string, string, TBody>): void => {\n if (!this.isEnabled) return;\n\n this._clearPendingTimeouts();\n const messageWithUri = { ...message, uri: this.uri, method: message.method ?? this._options.method ?? 'post' };\n this._sendOrQueue(messageWithUri);\n };\n\n /**\n * Subscribes to this WebSocket URI to start receiving messages.\n *\n * Only subscribes when enabled. Sends a 'subscribe' message through the parent connection.\n *\n * @param body - Optional body to send with the subscription\n */\n public subscribe = (body?: TBody): void => {\n if (!this.isEnabled) return;\n\n this._clearPendingTimeouts();\n this._state.setState((prev) => ({\n ...prev,\n subscribed: true,\n pendingSubscription: true,\n subscribedAt: Date.now()\n }));\n this._sendOrQueue({ body, uri: this.uri, method: 'subscribe' });\n this._options.onSubscribe?.({ uri: this.uri, body: this._options.body, uriApi: this });\n };\n\n /**\n * Unsubscribes from this WebSocket URI to stop receiving messages.\n *\n * Only unsubscribes when currently subscribed.\n */\n public unsubscribe = (): void => {\n if (!this._state.state.subscribed) return;\n this._state.setState((prev) => ({ ...prev, subscribed: false, pendingSubscription: false, message: undefined }));\n\n this._sendOrQueue({ uri: this.uri, method: 'unsubscribe' });\n };\n\n /**\n * Called by WebsocketConnection when the WebSocket connection opens.\n *\n * Subscribes with the configured body.\n */\n public onOpen = (): void => {\n if (this._state.state.connected) return;\n this._state.setState((prev) => ({ ...prev, connected: true }));\n this.subscribe(this._options.body);\n };\n\n /**\n * Called by WebsocketConnection when a message is received for this URI.\n *\n * @param data - The message data\n */\n public onMessage = (data: TData): void => {\n this._state.setState((prev) => ({\n ...prev,\n message: data,\n pendingSubscription: false,\n receivedAt: Date.now()\n }));\n this._options.onMessage?.({ data, uriApi: this });\n };\n\n /** @inheritdoc */\n public onError = (error: WebsocketTransportError): void => {\n this._state.setState((prev) => ({ ...prev, pendingSubscription: false }));\n this._options.onError?.(error);\n };\n\n /**\n * Called by WebsocketConnection when a server error message is received.\n *\n * @param error - Server error with parsed message body\n */\n public onMessageError = (error: WebsocketServerError<TBody>): void => {\n this._state.setState((prev) => ({ ...prev, pendingSubscription: false }));\n this._options.onMessageError?.(error);\n };\n\n /**\n * Called by WebsocketConnection when the WebSocket connection closes.\n *\n * Resets subscription state to ensure a fresh subscription on reconnect.\n */\n public onClose = (event: CloseEvent): void => {\n this._state.setState((prev) => ({ ...prev, subscribed: false, pendingSubscription: false }));\n this._options.onClose?.(event);\n };\n\n private _clearPendingTimeouts(): void {\n if (this._disconnectTimeout !== undefined) {\n clearTimeout(this._disconnectTimeout);\n this._disconnectTimeout = undefined;\n }\n if (this._hookRemovalTimeout !== undefined) {\n clearTimeout(this._hookRemovalTimeout);\n this._hookRemovalTimeout = undefined;\n }\n }\n\n private _scheduleHookRemoval(onRemove: () => void): void {\n this._clearPendingTimeouts();\n this._hookRemovalTimeout = setTimeout(() => {\n this._hookRemovalTimeout = undefined;\n if (this._registeredHooks.size === 0) {\n this._state.setState((prev) => ({ ...prev, connected: false }));\n this.unsubscribe();\n onRemove();\n }\n }, INITIATOR_REMOVAL_DELAY_MS);\n }\n\n private _flushPendingMessages(callback: SendToConnectionFn): void {\n if (this._pendingMessages.length === 0) return;\n this._pendingMessages.forEach((msg) => callback({ ...msg, uri: this.uri, method: msg.method ?? this._options.method ?? 'post' }));\n this._pendingMessages = [];\n }\n\n private _sendOrQueue(message: SendMessage<string, string, TBody>): void {\n if (this._sendToConnection) {\n this._sendToConnection(message);\n } else {\n this._pendingMessages.push(message);\n }\n }\n\n private _handleSubscriptionUpdates(\n previousOptions: WebsocketSubscriptionOptions<TData, TBody>,\n updatedOptions: WebsocketSubscriptionOptions<TData, TBody>\n ): void {\n const bodyChanged = !deepEqual(previousOptions.body, updatedOptions.body);\n const becameEnabled = !previousOptions.enabled && updatedOptions.enabled;\n\n if (bodyChanged || becameEnabled) {\n this.subscribe(updatedOptions.body);\n }\n }\n\n private _handleUnsubscribeOnDisable(\n previousOptions: WebsocketSubscriptionOptions<TData, TBody>,\n updatedOptions: WebsocketSubscriptionOptions<TData, TBody>\n ): void {\n const isDisabled = !updatedOptions.enabled;\n const wasEnabled = previousOptions.enabled;\n\n if (isDisabled && wasEnabled && this._state.state.subscribed) {\n this.unsubscribe();\n }\n }\n}\n","/**\n * @fileoverview Helper functions for WebSocket connection and listener management.\n *\n * These functions implement the singleton patterns for connections (per URL key)\n * and listeners (per API key) via {@link WebsocketClient}. Used by the React hooks\n * in {@link WebsocketHook}.\n *\n * @module websocketClient.helpers\n */\n\nimport { WebsocketListener, WebsocketMessageOptions, WebsocketSubscriptionOptions } from './types';\nimport { WebsocketClient } from './WebsocketClient';\nimport { WebsocketMessageApi } from './WebsocketMessageApi';\nimport { WebsocketSubscriptionApi } from './WebsocketSubscriptionApi';\n\n/**\n * Creates a WebSocket subscription API or returns the existing one for the given key.\n *\n * Singleton per key: multiple components with the same key share one instance.\n * The instance is stored in {@link WebsocketClient} and registered with a connection\n * via {@link WebsocketConnection.addListener}.\n *\n * @param client - The {@link WebsocketClient} instance\n * @template TData - The type of data received from the WebSocket\n * @template TBody - The type of message body sent to the WebSocket\n * @param key - Unique key for this subscription API\n * @param options - Configuration options\n * @returns Existing or newly created {@link WebsocketSubscriptionApi}\n *\n * @see {@link WebsocketClient.getListener} - Check for existing instance\n */\nexport const createWebsocketSubscriptionApi = <TData = unknown, TBody = unknown>(\n client: WebsocketClient,\n key: string,\n options: WebsocketSubscriptionOptions<TData, any>\n): WebsocketSubscriptionApi<TData, any> => {\n const listener = client.getListener<TData, TBody>(key, 'subscription');\n if (listener) {\n return listener;\n }\n const uriApi = new WebsocketSubscriptionApi(options);\n client.addListener(uriApi);\n return uriApi;\n};\n\n/**\n * Creates a WebSocket Message API or returns the existing one for the given key.\n *\n * Singleton per key: multiple components with the same key share one instance.\n *\n * @param client - The {@link WebsocketClient} instance\n * @param key - Unique key for this Message API\n * @param options - Configuration options\n * @returns Existing or newly created {@link WebsocketMessageApi}\n */\nexport const createWebsocketMessageApi = (client: WebsocketClient, key: string, options: WebsocketMessageOptions): WebsocketMessageApi => {\n const listener = client.getListener(key, 'message');\n if (listener) {\n return listener;\n }\n const messageApi = new WebsocketMessageApi(options, client);\n client.addListener(messageApi);\n return messageApi;\n};\n\n/**\n * Removes a WebSocket listener from its connection and from the client.\n *\n * Calls {@link WebsocketConnection.removeListener} and removes the listener from\n * {@link WebsocketClient}. Call when the last hook unmounts or when the listener\n * is disabled (via `enabled=false`).\n *\n * @param client - The {@link WebsocketClient} instance\n * @param listener - The listener (subscription or message API) to remove\n */\nexport const removeWebsocketListenerFromConnection = (client: WebsocketClient, listener: WebsocketListener): void => {\n const connection = client.getConnection(listener.url);\n connection?.removeListener(listener);\n client.removeListener(listener);\n};\n","import { useStore } from \"@tanstack/react-store\";\nimport { Store } from \"@tanstack/store\";\nimport { deepEqual } from \"fast-equals\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { useIsomorphicLayoutEffect } from \"usehooks-ts\";\nimport { WebsocketMessageApi } from \"./WebsocketMessageApi\";\nimport { useWebsocketClient } from \"./WebsocketProvider\";\nimport { WebsocketSubscriptionApi } from \"./WebsocketSubscriptionApi\";\nimport {\n createInitialWebsocketSubscriptionStore,\n WebsocketListener,\n WebsocketMessageApiPublic,\n WebsocketMessageOptions,\n WebsocketSubscriptionApiPublic,\n WebsocketSubscriptionOptions,\n WebsocketSubscriptionStore,\n} from \"./types\";\nimport {\n createWebsocketMessageApi,\n createWebsocketSubscriptionApi,\n removeWebsocketListenerFromConnection,\n} from \"./websocketClient.helpers\";\n\n/**\n * WebSocket React hooks for the shared connection architecture.\n *\n * This module provides hooks that integrate with {@link WebsocketConnection}.\n * from a path and optional secret (for region-based auth from `@mono-fleet/iam-provider`).\n * Call `useWebsocketConnectionConfig` and `useReconnectWebsocketConnections` from\n * `@mono-fleet/common-components` at app root for logging and reconnection on region change.\n *\n * ## Hook Overview\n *\n * | Hook | Use Case |\n * |------|----------|\n * | `useWebsocketSubscription` | Subscribe to a URI and receive streaming data via a reactive store |\n * | `useWebsocketMessage` | Send request/response messages to any URI (no subscription) |\n * | `useWebsocketSubscriptionByKey` | Access the store of a subscription created elsewhere (e.g. parent) |\n *\n * ## Choosing the Right Hook\n *\n * - **Streaming data** (voyage list, notifications): `useWebsocketSubscription`\n * - **One-off commands** (validate, modify, mark read): `useWebsocketMessage`\n * - **Child needs parent's subscription data**: `useWebsocketSubscriptionByKey` with same `key`\n *\n * ## Edge Cases\n *\n * - **Same key, multiple components**: Subscription and Message APIs are singletons per key.\n * Multiple hooks with the same key share one instance; `useWebsocketSubscriptionByKey` returns\n * a fallback store if the subscription does not exist yet (parent not mounted).\n * - **Options object identity**: Options are deep-compared; avoid passing new object literals\n * in dependency arrays to prevent unnecessary effect re-runs.\n * - **enabled=false**: Disconnects the listener and removes it from the connection after a delay.\n *\n * @module WebsocketHook\n */\n\n/**\n * Returns a referentially stable version of `value` that only updates when its\n * content changes according to deep equality.\n *\n * Prevents effect re-runs when dependency arrays contain object literals that\n * are structurally identical across renders (e.g. `{ uri: '/api', body: {} }`).\n *\n * @param value - The value to memoize (object or array)\n * @returns A referentially stable reference; updates only when deep equality changes\n *\n * @internal\n */\nfunction useDeepCompareMemoize<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!deepEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\n/**\n * Internal interface for listeners that support hook lifecycle management.\n *\n * Extends {@link WebsocketListener} with methods for registering/unregistering\n * hook instances and disconnecting. Both {@link WebsocketSubscriptionApi} and\n * {@link WebsocketMessageApi} implement this interface, enabling the shared\n * {@link useWebsocketLifecycle} hook.\n *\n * @internal\n */\ninterface HookableListener extends WebsocketListener {\n registerHook(id: string): void;\n unregisterHook(id: string, onRemove: () => void): void;\n disconnect(onRemoveFromSocket: () => void): void;\n}\n\n/**\n * Shared hook that manages connection registration, URL replacement, and hook\n * lifecycle tracking for both subscription and message listeners.\n *\n * Extracted from `useWebsocketCore` and `useWebsocketMessage` to eliminate\n * duplicated effect logic.\n *\n * @param listener - The listener instance (subscription or message API)\n * @param url - The WebSocket URL used for connection lookup\n * @param enabled - When `false`, disconnects the listener; when `true` or `undefined`, registers it\n *\n * @internal\n */\nfunction useWebsocketLifecycle(\n listener: HookableListener,\n url: string,\n enabled: boolean | undefined\n): void {\n const id = useId();\n const client = useWebsocketClient();\n\n useIsomorphicLayoutEffect(() => {\n if (enabled !== false) {\n const connection = client.addConnection(listener.url, url);\n connection.addListener(listener);\n } else {\n listener.disconnect(() =>\n removeWebsocketListenerFromConnection(client, listener)\n );\n }\n }, [enabled, listener, client]);\n\n useIsomorphicLayoutEffect(() => {\n const connection = client.getConnection(url);\n connection?.replaceUrl(url);\n }, [url, client]);\n\n useEffect(() => {\n const initiatorId = id;\n if (enabled !== false) {\n listener.registerHook(id);\n }\n return () => {\n listener.unregisterHook(initiatorId, () =>\n removeWebsocketListenerFromConnection(client, listener)\n );\n };\n }, [client, enabled, id, listener]);\n}\n\n/**\n * React hook that manages a WebSocket subscription for a specific Subscription endpoint.\n *\n * This hook provides a reactive interface to the WebSocket connection system. It establishes\n * the connection architecture by linking three key components:\n *\n * ## Architecture Overview\n *\n * The hook integrates with a two-layer class architecture:\n *\n * 1. **WebsocketConnection** (singleton per URL)\n * - Manages the underlying WebSocket connection lifecycle\n * - Handles reconnection, heartbeat, and connection state\n * - Routes incoming messages to the appropriate Subscription handlers\n * - Retrieved via `WebsocketClient.addConnection()` which ensures only one\n * connection exists per WebSocket URL\n *\n * 2. **WebsocketSubscriptionApi** (one per subscription per connection)\n * - Manages subscription lifecycle for a specific URI endpoint\n * - Provides a TanStack Store for reactive data updates\n * - Handles subscribe/unsubscribe operations\n * - Registered via `connection.addListener(subscriptionApi)` which routes messages by URI\n *\n * ## How the Hook Links to Classes\n *\n * ```\n * useWebsocketSubscription\n * │\n * ├─→ createWebsocketSubscriptionApi(key, options)\n * │ └─→ Returns/creates WebsocketSubscriptionApi singleton (per key)\n * │ ├─→ Manages subscription for this specific URI\n * │ ├─→ Provides reactive store for data updates\n * │ └─→ Handles subscribe/unsubscribe lifecycle\n * │\n * └─→ client.addConnection(url, url)\n * └─→ Returns/creates WebsocketConnection singleton (per URL)\n * ├─→ Manages WebSocket connection (connect, reconnect, heartbeat)\n * ├─→ Routes messages to registered listeners\n * └─→ connection.addListener(subscriptionApi) registers the listener\n * ```\n *\n * ## Lifecycle Management\n *\n * - **URI API**: Created once via `useState` initializer (singleton per key via\n * `createWebsocketUriApi`). Multiple components can share the same URI API,\n * tracked via registered hook IDs.\n *\n * - **Connection**: Found or created in a `useIsomorphicLayoutEffect` that watches\n * `enabled`. The connection is a singleton per key, shared across all hooks using\n * the same base URL path.\n *\n * - **Options Updates**: `useIsomorphicLayoutEffect` synchronously updates URI API options\n * via the `options` setter when they change (deep-compared via `useDeepCompareMemoize`),\n * preventing rendering with stale configuration.\n *\n * - **URL Replacement**: A separate `useIsomorphicLayoutEffect` watches `wsUrl` and calls\n * `connection.replaceUrl()` when the URL changes (e.g. due to auth context changes).\n *\n * - **Cleanup**: `useEffect` registers this hook instance as a hook and provides cleanup\n * that removes it. When the last hook is removed, the URI API automatically unsubscribes\n * and is removed from the connection.\n *\n * @template TData - The type of data received from the WebSocket for this URI\n * @template TBody - The type of message body sent to the WebSocket for this URI\n *\n * @param options - Configuration options including:\n * - `url`: The WebSocket URL\n * - `uri`: The specific URI endpoint for this subscription\n * - `key`: Unique identifier for this subscription (used to retrieve it elsewhere via `useWebsocketSubscriptionByKey`)\n * - `enabled`: Whether this subscription is enabled (default: true)\n * - `body`: Optional payload for subscription or initial message\n * - `onMessage`, `onSubscribe`, `onError`, `onMessageError`, `onClose`: Optional callbacks\n * @returns The {@link WebsocketSubscriptionApiPublic} instance. Use `useSelector(api.store, (s) => s.message)` to read data reactively.\n *\n * @example\n * ```typescript\n * // Create subscription and read data via TanStack Store\n * const voyageApi = useWebsocketSubscription<Voyage[], VoyageFilters>({\n * key: 'voyages-list',\n * url: '/api',\n * uri: '/api/voyages',\n * body: { status: 'active' }\n * });\n * const voyages = useSelector(voyageApi.store, (s) => s.message);\n *\n * // Or use useWebsocketSubscriptionByKey in children to access the same store\n * const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>('voyages-list');\n * const voyages = useSelector(voyagesStore, (s) => s.message);\n * ```\n *\n * ## Edge Cases\n *\n * - **Multiple initiators**: Using the same `key` in multiple components registers multiple hooks.\n * A console warning is emitted; multiple initiators can cause unexpected behavior.\n * - **pendingSubscription**: Use `store.pendingSubscription` for loading states — it is `true`\n * from subscribe until the first message is received.\n *\n * @see {@link useWebsocketSubscriptionByKey} - Access the store when the subscription is created in a parent\n * @see {@link WebsocketSubscriptionStore} - Store shape: `{ message, subscribed, connected, ... }`\n */\n\n// Implementation\nexport function useWebsocketSubscription<TData = unknown, TBody = unknown>(\n options: WebsocketSubscriptionOptions<TData, TBody>\n): WebsocketSubscriptionApiPublic<TData, TBody> {\n const client = useWebsocketClient();\n const [subscriptionApi] = useState<WebsocketSubscriptionApi<TData, TBody>>(\n () => createWebsocketSubscriptionApi(client, options.key, options)\n );\n\n useWebsocketLifecycle(subscriptionApi, options.url, options.enabled);\n\n const stableOptions = useDeepCompareMemoize(options);\n\n useIsomorphicLayoutEffect(() => {\n subscriptionApi.options = stableOptions;\n }, [stableOptions, subscriptionApi]);\n\n return subscriptionApi;\n}\n\n/**\n * React hook that returns the store of a WebSocket subscription by key.\n *\n * Use when a parent creates the subscription via `useWebsocketSubscription` and\n * children need to read the data. The `key` must match the one used when creating\n * the subscription.\n *\n * **Edge case**: Returns a fallback store (initial empty state) if the subscription\n * does not exist yet (e.g. parent hasn't mounted). This avoids null checks but means\n * children may briefly see empty data before the parent mounts and subscribes.\n *\n * @template TData - The type of data in the store's `message` field\n * @param key - Unique key (must match `useWebsocketSubscription` options.key)\n * @returns TanStack {@link Store} with shape {@link WebsocketSubscriptionStore}\n *\n * @example\n * ```typescript\n * // Parent creates subscription\n * useWebsocketSubscription<Voyage[]>({ key: 'voyages-list', url: '...', uri: '...' });\n *\n * // Child reads store by key\n * const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>('voyages-list');\n * const voyages = useSelector(voyagesStore, (s) => s.message);\n * ```\n *\n * @see {@link WebsocketSubscriptionStore} - Store shape\n */\nexport const useWebsocketSubscriptionByKey = <TData = unknown>(key: string) => {\n const client = useWebsocketClient();\n const subscription = client.getListener<TData, any>(key, \"subscription\");\n\n const [fallbackStore] = useState<Store<WebsocketSubscriptionStore<TData>>>(\n () =>\n new Store<WebsocketSubscriptionStore<TData>>(\n createInitialWebsocketSubscriptionStore<TData>()\n )\n );\n return subscription?.store ?? fallbackStore;\n};\n\n/**\n * React hook that manages a WebSocket Message API for request/response style messaging.\n *\n * Use this for one-off commands (validate, modify, mark read) rather than streaming\n * subscriptions. Send to any URI; optionally await a response.\n *\n * ## Key Features\n *\n * - **Request/Response**: `sendMessage(uri, method, body?, options?)` returns a Promise that resolves with the response\n * - **Fire-and-forget**: `sendMessageNoWait(uri, method, body?)` for commands that don't need a response\n * - **Any URI**: Not bound to a single URI like subscription APIs\n * - **Shared Instance**: Multiple components with the same `key` share the same Message API\n * - **Automatic Cleanup**: Removes from connection when the last hook unmounts\n *\n * @template TData - The type of data received in the response\n * @template TBody - The type of message body sent to the WebSocket\n *\n * @param options - Configuration options including:\n * - `url`: The WebSocket URL\n * - `key`: Unique identifier (components with same key share the API)\n * - `enabled`: Whether this API is enabled (default: true)\n * - `responseTimeoutMs`: Default timeout for `sendMessage` (default: 10000)\n * - `onError`, `onMessageError`, `onClose`: Optional callbacks\n * @returns {@link WebsocketMessageApiPublic} with `sendMessage`, `sendMessageNoWait`, `reset`, `url`, `key`, `isEnabled`\n *\n * @example\n * ```typescript\n * const api = useWebsocketMessage<ModifyVoyageUim, ModifyVoyageUim>({\n * key: 'voyages/modify',\n * url: '/api',\n * responseTimeoutMs: 10000\n * });\n *\n * // Await response (full form: uri, method, body?, options?)\n * const result = await api.sendMessage('voyages/modify/validate', 'post', formValues);\n *\n * // Fire-and-forget\n * api.sendMessageNoWait(`notifications/${id}/read`, 'post');\n * ```\n *\n * ## Edge Cases\n *\n * - **Overwrite**: Sending to the same URI while a request is pending cancels the previous\n * request (rejects with \"WebSocket request overwritten for URI\").\n * - **Disabled**: When `enabled=false`, `sendMessage` rejects; `sendMessageNoWait` is a no-op.\n * - **Connection closed**: Pending requests are rejected with \"WebSocket connection closed\".\n *\n * @see {@link WebsocketMessageApiPublic} - Public API surface\n */\nexport const useWebsocketMessage = (\n options: WebsocketMessageOptions\n): WebsocketMessageApiPublic => {\n const client = useWebsocketClient();\n const [messageApi] = useState<WebsocketMessageApi>(() =>\n createWebsocketMessageApi(client, options.key, options)\n );\n\n useWebsocketLifecycle(messageApi, options.url, options.enabled);\n\n return messageApi;\n};\n\n/**\n * Selects a value from a WebSocket subscription store with reactive updates.\n *\n * The store type is inferred from the first argument, so the selector\n * receives properly typed state (including `message: TData`) without explicit generics.\n *\n * Use this to subscribe to specific slices of subscription state and avoid re-renders when\n * unrelated fields change. The selector runs on every store update; return a primitive or\n * memoized value for optimal performance.\n *\n * @template TStore - The store state type (extends {@link WebsocketSubscriptionStore})\n * @template TResult - The type of the selected value\n * @param store - The TanStack Store from {@link WebsocketSubscriptionApi.store} or {@link useWebsocketSubscriptionByKey}\n * @param selector - Function that maps store state to the desired value. Receives typed state with `message`, `subscribed`, `pendingSubscription`, `connected`, etc.\n * @returns The selected value; triggers re-renders when the selected value changes (shallow comparison)\n *\n * @example\n * ```tsx\n * const voyageApi = useWebsocketSubscription<Voyage>({\n * key: 'voyages',\n * url: 'wss://api.example.com',\n * uri: '/api/voyages'\n * });\n *\n * // Select only message — re-renders when message changes, not when connected/subscribed change\n * const voyage = useSelector(voyageApi.store, (s) => s.message);\n *\n * // Select derived state\n * const isLoading = useSelector(voyageApi.store, (s) => s.pendingSubscription || !s.connected);\n *\n * // Select multiple fields (returns new object each time — consider useMemo if used as dependency)\n * const status = useSelector(voyageApi.store, (s) => ({\n * hasData: s.message !== undefined,\n * error: s.serverError ?? s.messageError\n * }));\n * ```\n *\n * @see {@link WebsocketSubscriptionStore} - Store shape and field descriptions\n * @see {@link useWebsocketSubscription} - Creates a subscription and returns the store\n * @see {@link useWebsocketSubscriptionByKey} - Access a subscription store by key\n */\nexport const useSelector = <\n TStore extends WebsocketSubscriptionStore<unknown>,\n TResult = unknown\n>(\n store: Store<TStore>,\n selector: (state: TStore) => TResult\n) => useStore(store, selector);\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/lib/types.ts","../src/lib/constants.ts","../src/lib/utils.ts","../src/lib/WebsocketConnection.helpers.ts","../src/lib/WebsocketConnection.ts","../src/lib/WebsocketClient.ts","../src/lib/WebsocketProvider.tsx","../src/lib/WebsocketMessageApi.ts","../src/lib/WebsocketSubscriptionApi.ts","../src/lib/websocketClient.helpers.ts","../src/lib/WebsocketHook.ts"],"sourcesContent":["import { RECONNECTION_CONFIG } from './constants';\nimport { WebsocketMessageApi } from './WebsocketMessageApi';\nimport { WebsocketSubscriptionApi } from './WebsocketSubscriptionApi';\n\n/**\n * Type definitions for the WebSocket connection system.\n *\n * @module types\n */\n\n/**\n * WebSocket connection ready states.\n *\n * Values match the WebSocket API readyState constants, with an additional\n * UNINSTANTIATED state for connections that haven't been created yet.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState\n */\nexport enum ReadyState {\n /** Connection has not been instantiated yet */\n UNINSTANTIATED = -1,\n /** Connection is being established */\n CONNECTING = 0,\n /** Connection is open and ready to communicate */\n OPEN = 1,\n /** Connection is in the process of closing */\n CLOSING = 2,\n /** Connection is closed or couldn't be opened */\n CLOSED = 3\n}\n\n/**\n * Structure for outgoing WebSocket messages.\n *\n * Messages are sent with a method (HTTP-like), URI for routing, optional body,\n * and an automatically generated correlation ID for tracking.\n *\n * @template TMethod - The type of the HTTP method (e.g., 'subscribe', 'unsubscribe', 'post')\n * @template TUri - The type of the URI string\n * @template TBody - The type of the message body payload\n */\nexport interface SendMessage<TMethod = string, TUri = string, TBody = unknown> {\n /** HTTP-like method for the message (e.g., 'subscribe', 'unsubscribe', 'post') */\n method?: TMethod;\n /** URI path for routing the message to the correct handler */\n uri?: TUri;\n /** Optional message body/payload */\n body?: TBody;\n}\n\n/**\n * Callback function for sending messages from a {@link WebsocketSubscriptionApi} to its parent {@link WebsocketConnection}.\n *\n * This callback is injected by the connection when a URI API is registered,\n * replacing the previous EventTarget/CustomEvent indirection with a direct,\n * type-safe function call.\n */\nexport type SendToConnectionFn = (message: SendMessage<string, string, unknown>) => void;\n\n/**\n * Structure of incoming WebSocket messages.\n *\n * Messages must have a URI for routing to the correct handler and can include\n * an optional body with the actual message data.\n *\n * @template TBody - The type of the message body payload\n */\nexport interface IncomingWebsocketMessage<TBody = unknown> {\n /** URI path that identifies which handler should process this message */\n uri: string;\n /** Optional message body/payload */\n body?: TBody;\n /** HTTP-like method for the message (e.g., 'subscribe', 'unsubscribe', 'post') */\n method?: string;\n}\n\n/**\n * Error sent by the server via a message with method 'error', 'conflict', or 'exception'.\n * Contains the parsed message body for application-level error handling.\n *\n * @template TBody - The type of the error body payload\n */\nexport interface WebsocketServerError<TBody = unknown> {\n readonly type: 'server';\n readonly message: IncomingWebsocketMessage<TBody>;\n}\n\n/**\n * Error from the WebSocket transport layer (connection failure, network issues, etc.).\n * Contains the raw Event from the WebSocket 'error' handler.\n */\nexport interface WebsocketTransportError {\n readonly type: 'transport';\n readonly event: Event;\n}\n\n/**\n * Configuration options for WebSocket URI APIs.\n *\n * Subscriptions automatically subscribe when the WebSocket connection opens.\n *\n * @template TData - The type of data received from the WebSocket\n * @template TBody - The type of message body sent to the WebSocket\n */\nexport interface WebsocketSubscriptionOptions<TData = unknown, TBody = unknown> {\n /** The base URL of the WebSocket connection. */\n url: string;\n /** The URI path for this subscription. */\n uri: string;\n /**\n * Unique key for the URI API.\n *\n * Used to identify the URI API in the connection.\n */\n key: string;\n /** Whether this URI API is enabled (default: true). When disabled, messages are not sent. */\n enabled?: boolean;\n /** Optional body payload to send with subscription or initial message */\n body?: TBody;\n\n /** Optional HTTP method for custom messages sent via sendMessage */\n method?: string;\n /**\n * Callback invoked when subscription is successful.\n *\n * @param uri - The URI path that was subscribed to\n * @param body - The body that was sent with the subscription\n */\n onSubscribe?: (props: { uri: string; body?: TBody; uriApi: WebsocketSubscriptionApi<TData, TBody> }) => void;\n /**\n * Callback invoked when a message is received for this URI.\n *\n * @param data - The message data received from the WebSocket\n * @param uriApi - The URI API instance that received the message\n */\n onMessage?: (props: { data: TData; uriApi: WebsocketSubscriptionApi<TData, TBody> }) => void;\n /**\n * Callback invoked when a WebSocket error occurs.\n *\n * @param error - Discriminated error: use `error.type === 'server'` for server-sent error messages\n * (parsed body in `error.message`), or `error.type === 'transport'` for connection failures.\n */\n onError?: (error: WebsocketTransportError) => void;\n\n /**\n * Callback invoked when a server error message is received for this subscription.\n *\n * @param error - Server error with parsed message body (`error.type === 'server'`, `error.message` contains the incoming message)\n */\n onMessageError?: (error: WebsocketServerError<TBody>) => void;\n /**\n * Callback invoked when the WebSocket connection closes.\n *\n * @param event - The close event from the WebSocket connection\n */\n onClose?: (event: CloseEvent) => void;\n}\n\n/**\n * Configuration options for WebSocket Message API.\n *\n * Message API is for request/response style communication: send a message to any URI\n * and optionally wait for a response. No subscription support.\n *\n * @template TData - The type of data received in the response\n * @template TBody - The type of message body sent to the WebSocket\n */\nexport interface WebsocketMessageOptions {\n /** The base URL of the WebSocket connection. */\n url: string;\n /**\n * Unique key for the Message API.\n *\n * Used to identify the API in the connection.\n */\n key: string;\n /** Whether this Message API is enabled (default: true). When disabled, messages are not sent. */\n enabled?: boolean;\n /**\n * Default timeout in ms when waiting for a response.\n *\n * Can be overridden per sendMessage call.\n */\n responseTimeoutMs?: number;\n /**\n * Callback invoked when a WebSocket transport error occurs.\n */\n onError?: (error: WebsocketTransportError) => void;\n /**\n * Callback invoked when a server error message is received.\n */\n onMessageError?: (error: WebsocketServerError) => void;\n /**\n * Callback invoked when the WebSocket connection closes.\n */\n onClose?: (event: CloseEvent) => void;\n}\n\n/**\n * Options for WebsocketMessageApi.sendMessage.\n */\nexport interface SendMessageOptions {\n /** Timeout in ms when waiting for a response. Overrides the default from options. */\n timeout?: number;\n}\n\n/**\n * Common interface for WebSocket listeners registered with {@link WebsocketConnection}.\n *\n * Both {@link WebsocketSubscriptionApi} and {@link WebsocketMessageApi} implement this interface,\n * allowing the connection to treat them uniformly via {@link addListener} / {@link removeListener}.\n *\n * - **Subscription listeners**: Have `uri`, `onOpen`, `onMessage` — route by URI match\n * - **Message listeners**: Have `hasWaitingUri`, `deliverMessage` — route by pending URI\n */\nexport interface WebsocketListener {\n readonly key: string;\n readonly url: string;\n readonly isEnabled: boolean;\n setSendToConnection(callback: SendToConnectionFn | null): void;\n onError(error: WebsocketTransportError): void;\n onMessageError(error: WebsocketServerError<unknown>): void;\n onClose(event: CloseEvent): void;\n reset(): void;\n /** Subscription listeners: fixed URI for this endpoint */\n readonly uri?: string;\n /** Subscription listeners: called when connection opens */\n onOpen?(): void;\n /** Subscription listeners: called when a message is received for this URI */\n onMessage?(data: unknown): void;\n /** Message listeners: returns true if waiting for a response for the given URI */\n hasWaitingUri?(uri: string): boolean;\n /** Message listeners: delivers a response for a pending request */\n deliverMessage?(uri: string, data: unknown): void;\n readonly type: 'subscription' | 'message';\n}\n\nexport type WebsocketMessageApiPublic = Pick<\n WebsocketMessageApi,\n 'sendMessage' | 'sendMessageNoWait' | 'reset' | 'url' | 'key' | 'isEnabled'\n>;\n\nexport type WebsocketSubscriptionApiPublic<TData = unknown, TBody = unknown> = Pick<\n WebsocketSubscriptionApi<TData, TBody>,\n 'reset' | 'url' | 'key' | 'isEnabled' | 'store'\n>;\n\nexport interface WebsocketSubscriptionStore<TData = unknown> {\n message: TData | undefined;\n subscribed: boolean;\n /**\n * Whether a subscription has been sent but no response received yet.\n *\n * - `true`: A subscribe message was sent and we are waiting for the first (or next) message.\n * - `false`: No subscription is active, the connection is closed, or we have already received a response.\n *\n * Use this to show loading/placeholder UI while waiting for initial data after subscribing.\n */\n pendingSubscription: boolean;\n subscribedAt: number | undefined;\n receivedAt: number | undefined;\n connected: boolean;\n messageError: WebsocketTransportError | undefined;\n serverError: WebsocketServerError<unknown> | undefined;\n}\n\n/**\n * Creates the initial state for a {@link WebsocketSubscriptionStore}.\n *\n * @template TData - The type of data in the store's `message` field\n * @returns A new store with default values (message: undefined, subscribed: false, etc.)\n */\nexport function createInitialWebsocketSubscriptionStore<TData = unknown>(): WebsocketSubscriptionStore<TData> {\n return {\n message: undefined,\n subscribed: false,\n pendingSubscription: false,\n subscribedAt: undefined,\n receivedAt: undefined,\n connected: false,\n messageError: undefined,\n serverError: undefined\n };\n}\n\n/**\n * Optional custom logger for WebSocket connection events.\n * Set via {@link WebsocketConfig.setCustomLogger}.\n */\nexport interface WebsocketLogger {\n /** Logs connection events (e.g. ws-connect, ws-close, ws-error, ws-reconnect) */\n // log?(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void;\n /** Called when max retry attempts exceeded; use to trigger token refresh or other recovery */\n connectionEvents?: (event: WebsocketLoggerConnectionEvent) => void;\n}\n\n/** Union of all connection event types passed to {@link WebsocketClientOverrides.connectionEvent}. */\n\nexport type WebsocketLoggerConnectionEvent =\n | WebsocketLoggerCloseEvent\n | WebsocketLoggerOpenEvent\n | WebsocketLoggerMessageEvent\n | WebsocketLoggerErrorEvent\n | WebsocketLoggerReconnectingEvent\n | WebsocketLoggerPongTimeoutEvent\n | WebsocketLoggerInvalidMessageEvent\n | WebsocketLoggerMessageErrorEvent\n | WebsocketLoggerParseErrorEvent\n | WebsocketLoggerSendMessageEvent\n | WebsocketLoggerCleanupEvent;\n\n/** @internal */\ninterface WebsocketLoggerCloseEvent {\n /** WebSocket connection closed */\n type: 'close';\n url: string;\n code: number;\n reason: string;\n wasClean: boolean;\n subscriptions: number;\n}\n/** @internal */\ninterface WebsocketLoggerCleanupEvent {\n /** Connection cleaned up (no listeners remain) */\n type: 'cleanup';\n url: string;\n}\n\n/** @internal */\ninterface WebsocketLoggerOpenEvent {\n /** WebSocket connection opened or connecting */\n type: 'open' | 'connect';\n url: string;\n retries: number;\n uriApis: string[];\n}\n/** @internal */\ninterface WebsocketLoggerMessageEvent {\n /** Incoming message received */\n type: 'message';\n uri: string;\n url: string;\n body: unknown;\n method: string;\n}\n/** @internal */\ninterface WebsocketLoggerSendMessageEvent {\n /** Outgoing message sent */\n type: 'send-message';\n uri?: string;\n url: string;\n body: unknown;\n method?: string;\n}\n\n/** @internal */\ninterface WebsocketLoggerErrorEvent {\n /** WebSocket transport error */\n type: 'error';\n event: unknown;\n url: string;\n uriApis: string[];\n}\n/** @internal */\ninterface WebsocketLoggerParseErrorEvent {\n /** Failed to parse incoming message JSON */\n type: 'parse-error';\n error: unknown;\n url: string;\n uriApis: string[];\n message: unknown;\n}\n\n/** @internal */\ninterface WebsocketLoggerMessageErrorEvent {\n /** Server sent error message (method: error, conflict, or exception) */\n type: 'message-error';\n uri: string;\n url: string;\n uriApis: string[];\n message: unknown;\n}\n/** @internal */\ninterface WebsocketLoggerReconnectingEvent {\n /** Reconnection attempt or max retries exceeded */\n type: 'reconnecting' | 'max-retries-exceeded';\n retries: number;\n url: string;\n}\n\n/** @internal */\ninterface WebsocketLoggerInvalidMessageEvent {\n /** Incoming message missing required structure (e.g. uri) */\n type: 'invalid-message';\n url: string;\n uriApis: string[];\n message: unknown;\n}\n\n/** @internal */\ninterface WebsocketLoggerPongTimeoutEvent {\n /** No pong received within heartbeat timeout */\n type: 'pong-timeout';\n url: string;\n}\n\nexport type ReconnectionConfig = typeof RECONNECTION_CONFIG;\n\n/** Heartbeat (ping/pong) configuration */\nexport interface HeartbeatConfig {\n /** Whether to send ping messages and expect pong responses. Default: true */\n enabled: boolean;\n /** Time in ms to wait for a pong before considering the connection dead. Default: 10000 */\n pongTimeoutMs: number;\n}\n\n/** Overrides for the global WebSocket configuration. All fields are optional. */\nexport interface WebsocketClientOverrides {\n /** Maximum number of reconnection attempts before stopping and showing a permanent error. Prevents infinite retries on dead endpoints (CPU wake-ups, battery drain). ~10 attempts ≈ 12 minutes at phase 3 (90s interval). User can retry manually. */\n maxRetryAttempts?: number;\n /** Number of failed reconnection attempts before showing user notifications. Prevents notification spam during brief network interruptions. */\n notificationThreshold?: number;\n /** Initial delay (in ms) when server closes with 1013 Try Again Later. The server explicitly asks to wait before reconnecting. */\n tryAgainLaterDelayMs?: number;\n /** Delay durations (in milliseconds) for each reconnection phase. */\n delays?: {\n firstPhase?: number;\n secondPhase?: number;\n thirdPhase?: number;\n };\n /** Threshold values that determine when to transition between reconnection phases. */\n phaseThresholds?: {\n first?: number;\n second?: number;\n };\n /** Override connection cleanup delay when no listeners remain */\n connectionCleanupDelayMs?: number;\n /** Default timeout in ms when waiting for a message response. Used by WebsocketMessageApi */\n messageResponseTimeoutMs?: number;\n /** Override ping/pong heartbeat behavior */\n heartbeat?: Partial<HeartbeatConfig>;\n\n transformMessagePayload?: (payload: SendMessage<string, string, unknown>) => SendMessage<string, string, unknown>;\n /**\n * Callback for connection event logging. Receives events such as:\n * - `{ type: 'open' | 'connect', url, retries, uriApis }`\n * - `{ type: 'close', url, code, reason, wasClean, subscriptions }`\n * - `{ type: 'reconnecting' | 'max-retries-exceeded', url, retries }`\n * - `{ type: 'message-error', url, uri, uriApis, message }`\n * - `{ type: 'invalid-message', url, uriApis, message }`\n * - `{ type: 'parse-error', url, uriApis, message, error }`\n * - `{ type: 'send-message', url, uri?, body, method? }`\n * - `{ type: 'cleanup', url }`\n * - `{ type: 'pong-timeout', url }`\n *\n * @param event - The connection event\n */\n connectionEvent?: (event: WebsocketLoggerConnectionEvent) => void;\n}\n","import { HeartbeatConfig } from \"./types\";\n\n/**\n * WebSocket constants and configuration.\n *\n * @module constants\n */\n\n/**\n * WebSocket close codes used for connection state detection.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n */\nexport const WEBSOCKET_CLOSE_CODES = {\n /** Clean intentional closure — do not reconnect */\n NORMAL_CLOSURE: 1000,\n /** Endpoint going away (e.g. server restarting) — reconnect */\n GOING_AWAY: 1001,\n /** Server internal error — reconnect */\n INTERNAL_ERROR: 1011,\n /** Service restart — reconnect */\n SERVICE_RESTART: 1012,\n /** Try again later — reconnect */\n TRY_AGAIN_LATER: 1013,\n /**\n * Abnormal closure (1006) indicates the connection was closed without\n * receiving a close frame. Typically occurs due to network issues,\n * server crashes, or unexpected termination — reconnect.\n */\n ABNORMAL_CLOSURE: 1006\n} as const;\n\n/**\n * Configuration for WebSocket reconnection behavior with exponential backoff.\n *\n * The reconnection strategy uses three phases:\n * - First phase (attempts 0-4): 4 second delay\n * - Second phase (attempts 5-9): 30 second delay\n * - Third phase (attempts 10+): 90 second delay\n *\n * User notifications are only shown after the notification threshold is exceeded\n * to avoid spamming users during brief network interruptions.\n *\n * After MAX_RETRY_ATTEMPTS, reconnection stops to avoid infinite retries on dead\n * endpoints (e.g. ~10 attempts ≈ 12 minutes); the user can manually retry via the\n * notification action.\n */\nexport const RECONNECTION_CONFIG = {\n /**\n * Maximum number of reconnection attempts before stopping and showing a permanent\n * error. Prevents infinite retries on dead endpoints (CPU wake-ups, battery drain).\n * ~10 attempts ≈ 12 minutes at phase 3 (90s interval). User can retry manually.\n */\n MAX_RETRY_ATTEMPTS: 20,\n /**\n * Number of failed reconnection attempts before showing user notifications.\n * Prevents notification spam during brief network interruptions.\n */\n NOTIFICATION_THRESHOLD: 10,\n /**\n * Initial delay (in ms) when server closes with 1013 Try Again Later.\n * The server explicitly asks to wait before reconnecting.\n */\n TRY_AGAIN_LATER_DELAY_MS: 30000,\n /**\n * Delay durations (in milliseconds) for each reconnection phase.\n */\n DELAYS: {\n /** First phase delay: 4 seconds for the first 5 attempts */\n FIRST_PHASE: 4000,\n /** Second phase delay: 30 seconds for attempts 6-10 */\n SECOND_PHASE: 30000,\n /** Third phase delay: 90 seconds for attempts 11+ */\n THIRD_PHASE: 90000\n },\n /**\n * Threshold values that determine when to transition between reconnection phases.\n */\n PHASE_THRESHOLDS: {\n /** Maximum attempts in the first phase (0-4) */\n FIRST: 5,\n /** Maximum attempts in the second phase (5-9) */\n SECOND: 10\n }\n} as const;\n\n/**\n * Delay configuration for WebSocket connection cleanup.\n *\n * When all URI APIs are removed, the connection is closed after a delay to allow\n * for efficient resource cleanup while avoiding unnecessary reconnections for\n * quick re-registrations. Different delays are used for production vs test environments.\n */\n// export const CONNECTION_CLEANUP_DELAY = {\n// /** Production delay: 3 seconds to allow for quick re-registrations */\n// PRODUCTION_MS: 3000,\n// /** Test delay: 10ms for faster test execution */\n// TEST_MS: 10\n// } as const;\nexport const CONNECTION_CLEANUP_DELAY_MS = 3000;\n\n/**\n * Delay in milliseconds before reconnecting after teardown.\n *\n * Used in {@link WebsocketConnection.teardownAndReconnect} to allow cleanup\n * to complete before establishing a new connection.\n */\nexport const TEARDOWN_RECONNECT_DELAY_MS = 1000;\n\n/**\n * Delay in milliseconds before removing a WebSocket URI API initiator.\n *\n * When an initiator (component/hook) is removed, there's a short delay before\n * unsubscribing and cleaning up. This prevents rapid subscribe/unsubscribe cycles\n * that could occur during React component re-renders or fast user interactions.\n */\nexport const INITIATOR_REMOVAL_DELAY_MS = 200;\n\n/**\n * Configuration for WebSocket heartbeat (ping/pong) mechanism.\n *\n * After sending a ping, we expect a pong within PONG_TIMEOUT_MS. If no pong arrives,\n * the connection is considered dead and we force-close to trigger reconnection.\n */\nexport const HEARTBEAT_CONFIG = {\n /** Time in ms to wait for a pong before considering the connection dead. Default: 10 seconds */\n PONG_TIMEOUT_MS: 10000\n} as const;\n\n/**\n * Default options for WebSocket URI APIs.\n *\n * These defaults are used when creating a new URI API instance if options\n * are not explicitly provided. Subscriptions automatically subscribe when\n * the WebSocket connection opens.\n *\n * @see {@link WebsocketUriOptions} - The type definition for URI options\n * @see {@link WebsocketSubscriptionApi} - The class that uses these defaults\n */\nexport const DEFAULT_URI_OPTIONS: {\n enabled: boolean;\n} = {\n enabled: true\n};\n\n/**\n * Default timeout in milliseconds when waiting for a WebSocket message response.\n *\n * Used by {@link WebsocketMessageApi} when no explicit timeout is provided.\n */\nexport const DEFAULT_MESSAGE_RESPONSE_TIMEOUT_MS = 10000;\n\n\n/**\n * Default heartbeat configuration for WebSocket connections.\n *\n * Enables ping/pong with the default timeout from {@link HEARTBEAT_CONFIG}.\n */\nexport const DEFAULT_HEARTBEAT_CONFIG: HeartbeatConfig = {\n enabled: true,\n pongTimeoutMs: HEARTBEAT_CONFIG.PONG_TIMEOUT_MS\n};","import { useEffect, useLayoutEffect } from \"react\";\n\nexport const wait = (ms: number) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const useIsomorphicLayoutEffect =\n typeof window !== 'undefined' ? useLayoutEffect : useEffect;","/**\n * @fileoverview Pure helper functions for WebSocket connection management.\n *\n * These utilities support {@link WebsocketConnection} with reconnection timing,\n * message validation, heartbeat, and user notifications. All functions are\n * stateless and side-effect free except the notification helpers.\n *\n * @module WebsocketConnection.helpers\n */\n\nimport { RECONNECTION_CONFIG, WEBSOCKET_CLOSE_CODES } from \"./constants\";\nimport {\n IncomingWebsocketMessage,\n ReconnectionConfig,\n SendMessage,\n WebsocketClientOverrides,\n WebsocketListener,\n} from \"./types\";\n\n/**\n * Extracts URIs from subscription listeners for connection event logging.\n *\n * @param listeners - Map of listeners keyed by their unique key\n * @returns Array of URIs from subscription listeners (message APIs are excluded)\n * @internal\n */\nexport const getSubscriptionUris = (\n listeners: Map<string, WebsocketListener>\n): string[] =>\n Array.from(listeners)\n .filter(([, listener]) => \"uri\" in listener)\n .map(([, listener]) => (listener as { uri: string }).uri);\n\n/**\n * Calculates the wait time before attempting to reconnect based on the number of failed attempts.\n *\n * Uses a three-phase exponential backoff strategy to avoid hammering a failing server:\n * - **First phase** (attempts 0–4): 4 seconds — quick recovery for transient issues\n * - **Second phase** (attempts 5–9): 30 seconds — moderate backoff for persistent issues\n * - **Third phase** (attempts 10+): 90 seconds — long backoff to reduce load on dead endpoints\n *\n * @param tries - The number of reconnection attempts made so far\n * @param reconnectionConfig - Optional reconnection config (defaults to global config)\n * @returns Wait time in milliseconds before next reconnection attempt\n *\n * @see {@link RECONNECTION_CONFIG} - Phase thresholds and delay values\n * @internal\n */\nexport const reconnectWaitTime = (\n tries: number,\n delays: NonNullable<Required<WebsocketClientOverrides[\"delays\"]>>,\n phaseThresholds: NonNullable<\n Required<WebsocketClientOverrides[\"phaseThresholds\"]>\n >\n) => {\n if (tries < phaseThresholds.first) {\n return delays.firstPhase;\n }\n if (tries < phaseThresholds.second) {\n return delays.secondPhase;\n }\n return delays.thirdPhase;\n};\n\n/**\n * Gets the ping interval time in milliseconds for keeping WebSocket connections alive.\n *\n * The heartbeat sends a ping every 40 seconds. If no pong arrives within\n * {@link HEARTBEAT_CONFIG.PONG_TIMEOUT_MS}, the connection is force-closed to trigger reconnection.\n *\n * @returns The ping interval in milliseconds (40 seconds)\n *\n * @see {@link HEARTBEAT_CONFIG.PONG_TIMEOUT_MS} - Time to wait for pong before considering connection dead\n * @internal\n */\nexport const getPingTime = (): number => 40 * 1000;\n\n/**\n * Type guard to validate that a parsed value is a valid incoming WebSocket message.\n *\n * Valid messages must be an object with a string `uri` property. Messages without\n * a valid structure are rejected and trigger {@link WebsocketListener.onError} with\n * type `'transport'`.\n *\n * @param value - The value to check (typically from `JSON.parse`)\n * @returns `true` if the value is a valid {@link IncomingWebsocketMessage}\n *\n * @internal\n */\nexport const isValidIncomingMessage = (\n value: unknown\n): value is IncomingWebsocketMessage => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"uri\" in value &&\n typeof (value as Record<string, unknown>).uri === \"string\"\n );\n};\n\n/**\n * Checks if the method indicates a server-side error message.\n *\n * Server errors use methods `'error'`, `'conflict'`, or `'exception'`. These are\n * routed to {@link WebsocketListener.onMessageError} instead of `onMessage`.\n *\n * @param method - The message method to check (optional)\n * @returns `true` if the method is an error method; `false` if undefined or not an error\n *\n * @internal\n */\nexport const isErrorMethod = (method?: string): boolean => {\n if (!method) return false;\n const errorMethods = [\"error\", \"conflict\", \"exception\"];\n return errorMethods.includes(method);\n};\n\n/**\n * Checks if the browser reports an online network state.\n *\n * Uses `window.navigator.onLine`. Note: this can be unreliable — it may report\n * `true` when the user is on a network but has no internet (e.g. captive portal).\n *\n * @returns `true` if the browser is online; `false` in SSR or when offline\n *\n * @internal\n */\nexport const isBrowserOnline = (): boolean => {\n return typeof window !== \"undefined\" && window.navigator.onLine;\n};\n\n/**\n * Checks if the WebSocket is ready to send and receive messages.\n *\n * Requires both browser online state and socket in {@link WebSocket.OPEN} state.\n * Use this before sending messages (e.g. heartbeat ping).\n *\n * @param socket - The WebSocket instance to check\n * @returns `true` if browser is online and socket is OPEN\n *\n * @see {@link isConnectionReady} - Less strict: also allows CONNECTING\n * @internal\n */\nexport const isSocketOnline = (socket?: WebSocket): boolean => {\n return (\n typeof window !== \"undefined\" &&\n window.navigator.onLine &&\n socket !== undefined &&\n socket.readyState === WebSocket.OPEN\n );\n};\n\n/**\n * Creates a ping message for the WebSocket heartbeat mechanism.\n *\n * Format: `{ method: 'post', uri: 'ping', body: timestamp, correlation: uuid }`.\n * The server should respond with a pong; missing pong triggers reconnection.\n *\n * @returns JSON string of the ping message\n *\n * @internal\n */\nexport const createPingMessage = (): SendMessage<string, string, number> => {\n return {\n method: \"post\",\n uri: \"ping\",\n body: Date.now(),\n };\n};\n\n/**\n * Checks if the WebSocket connection is in a valid state (open or connecting).\n *\n * Used to avoid creating duplicate connections. Unlike {@link isSocketOnline},\n * this returns `true` for CONNECTING state — useful when deciding whether to\n * call `connect()`.\n *\n * @param socket - The WebSocket instance to check\n * @returns `true` if socket is OPEN or CONNECTING; `false` if undefined, CLOSING, or CLOSED\n *\n * @see {@link isSocketOnline} - Stricter: requires OPEN and browser online\n * @internal\n */\nexport const isConnectionReady = (socket?: WebSocket): boolean => {\n return (\n socket?.readyState === WebSocket.OPEN ||\n socket?.readyState === WebSocket.CONNECTING\n );\n};\n\n/**\n * Determines whether a WebSocket close event warrants an automatic reconnection attempt.\n *\n * **Only code 1000 (Normal Closure) does NOT trigger reconnection** — it indicates a\n * clean, intentional shutdown. All other codes trigger reconnection when listeners\n * are still registered, including:\n * - 1001 Going Away, 1011 Internal Error, 1012 Service Restart, 1013 Try Again Later\n * - 1006 Abnormal Closure (no close frame received — network/server crash)\n *\n * @param closeCode - The close event code from the WebSocket CloseEvent\n * @returns `true` if reconnection should be attempted; `false` for 1000 only\n *\n * @see {@link WEBSOCKET_CLOSE_CODES} - Close code constants\n */\nexport const isReconnectableCloseCode = (closeCode: number): boolean => {\n return closeCode !== WEBSOCKET_CLOSE_CODES.NORMAL_CLOSURE;\n};\n","/**\n * @fileoverview WebSocket connection management with automatic reconnection and URI-based routing.\n *\n * This module provides a robust WebSocket connection manager that handles:\n * - Connection lifecycle (connect, disconnect, reconnect)\n * - Automatic reconnection with three-phase exponential backoff\n * - Heartbeat/ping-pong to detect and recover from stale connections\n * - URI-based message routing to multiple listeners over a single connection\n * - Browser online/offline detection and deferred reconnection\n * - Singleton connection per URL key (via {@link WebsocketClient.addConnection})\n * - User notifications for connection status (with configurable threshold)\n *\n * ## Architecture\n *\n * Connections are created via {@link WebsocketClient.addConnection} which ensures one\n * connection per key. Listeners ({@link WebsocketSubscriptionApi} or {@link WebsocketMessageApi})\n * register via {@link addListener} and receive messages routed by URI.\n *\n * ## Edge Cases\n *\n * - **Cached messages**: Only non-subscribe messages are cached when the socket is not open;\n * subscribe messages trigger connect but are not queued.\n * - **replaceUrl vs reconnect**: Both use `teardownAndReconnect`; a guard prevents concurrent\n * cycles when both fire in the same render (e.g. auth context change).\n * - **Close code 1000**: Only this code does NOT trigger reconnection (intentional shutdown).\n * - **Max retries**: After {@link RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS}, automatic reconnection\n * stops; user must click Retry in the notification.\n *\n * @module WebsocketConnection\n */\n\nimport { wait } from \"./utils\";\nimport {\n TEARDOWN_RECONNECT_DELAY_MS,\n WEBSOCKET_CLOSE_CODES,\n} from \"./constants\";\nimport { SendMessage, WebsocketListener } from \"./types\";\nimport { WebsocketClient } from \"./WebsocketClient\";\nimport {\n createPingMessage,\n getPingTime,\n getSubscriptionUris,\n isBrowserOnline,\n isConnectionReady,\n isErrorMethod,\n isReconnectableCloseCode,\n isSocketOnline,\n isValidIncomingMessage,\n reconnectWaitTime,\n} from \"./WebsocketConnection.helpers\";\n\n/**\n * Manages a WebSocket connection with automatic reconnection, heartbeat monitoring, and URI-based message routing.\n *\n * This class provides:\n * - Automatic reconnection with exponential backoff on connection loss\n * - Heartbeat/ping mechanism to keep connections alive\n * - Multiple URI API registration for routing messages to different handlers\n * - Online/offline detection and handling\n * - Custom logger support for monitoring (configure via {@link WebsocketClient} connectionEvent)\n * - User notifications for connection status\n *\n * @example\n * ```typescript\n * const connection = new WebsocketConnection('ws://example.com/api');\n * const uriApi = new WebsocketSubscriptionApi({\n * key: 'messages',\n * url: '/api',\n * uri: '/messages',\n * onMessage: ({ data }) => console.log('Received:', data),\n * onError: (error) => console.log('Error:', error),\n * onClose: (event) => console.log('Closed:', event)\n * });\n * connection.addListener(uriApi);\n * ```\n *\n * @see {@link WebsocketClient.reconnectAllConnections} - Reconnect all connections (e.g. on auth change)\n */\nexport class WebsocketConnection {\n // ─── Properties ─────────────────────────────────────────────────────\n /** The underlying WebSocket instance */\n private _socket?: WebSocket;\n\n /** Map of all listeners (subscription and message APIs) keyed by their unique key */\n private _listeners: Map<string, WebsocketListener> = new Map();\n\n /** The WebSocket URL */\n private _url: string;\n\n /** Timeout for the next ping message */\n private pingTimeOut: ReturnType<typeof setTimeout> | undefined;\n\n /** Timeout for detecting missing pong after ping (dead-connection detection) */\n private pongTimeOut: ReturnType<typeof setTimeout> | undefined;\n\n /** Timeout for closing the connection when no URIs are registered */\n private closeConnectionTimeOut: ReturnType<typeof setTimeout> | undefined;\n\n /** Counter for reconnection attempts */\n private reconnectTries = 0;\n\n /** Guard flag that prevents concurrent teardown-and-reconnect cycles (e.g. when both replaceUrl and reconnect fire in the same render). */\n private _isReconnecting = false;\n\n /** True when max retry attempts exceeded; stops automatic reconnection until manual retry. */\n private _maxRetriesExceeded = false;\n\n /**\n * Queue of non-subscribe messages sent while the socket was not open.\n * Flushed when the connection opens. Subscribe messages are NOT cached — they trigger connect only.\n */\n private cachedMessages: SendMessage<string, string, any>[] = [];\n\n /** The WebsocketClient instance */\n private _client: WebsocketClient;\n\n // ─── Constructor ────────────────────────────────────────────────────\n\n /**\n * Creates a new WebSocket connection instance.\n *\n * The connection is not established until a listener is added via {@link addListener}.\n *\n * @param url - The WebSocket URL to connect to\n * @param client - The {@link WebsocketClient} for configuration (reconnection, heartbeat, etc.)\n */\n constructor(url: string, client: WebsocketClient) {\n this._url = url;\n this._client = client;\n }\n\n // ─── Public Getters ─────────────────────────────────────────────────\n\n /**\n * Gets the current ready state of the WebSocket connection.\n *\n * @returns The WebSocket ready state (CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3) or undefined if no socket exists.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState | WebSocket.readyState}\n */\n public get readyState() {\n return this._socket?.readyState;\n }\n\n /**\n * Gets the WebSocket URL for this connection.\n *\n * @returns The WebSocket URL string\n */\n public get url() {\n return this._url;\n }\n\n /**\n * Gets the underlying WebSocket instance.\n *\n * @returns The WebSocket instance if connected, or undefined if the connection hasn't been established yet or has been closed.\n */\n public getSocket = (): WebSocket | undefined => {\n return this._socket;\n };\n\n // ─── Public API: URI Management ─────────────────────────────────────\n\n /**\n * Registers a listener (subscription or message API) with this connection.\n *\n * Initiates the WebSocket connection if not already connected. Sets up the send callback\n * so the listener can transmit messages through this connection.\n *\n * If the socket is already open, immediately notifies subscription listeners via `onOpen`.\n *\n * @param listener - The {@link WebsocketListener} to register\n * @returns The registered listener\n */\n public addListener = (listener: WebsocketListener) => {\n listener.setSendToConnection(this.handleSendMessage);\n this.connect();\n this._listeners.set(listener.key, listener);\n clearTimeout(this.closeConnectionTimeOut);\n\n if (this._socket?.readyState === WebSocket.OPEN && listener.onOpen) {\n listener.onOpen();\n }\n return listener;\n };\n\n /**\n * Unregisters a listener and schedules connection cleanup if no listeners remain.\n *\n * Disconnects the listener's send callback and removes it from the routing map.\n * The WebSocket connection will be closed after {@link CONNECTION_CLEANUP_DELAY_MS} if no other\n * listeners are registered.\n *\n * @param listener - The listener instance to unregister\n */\n public removeListener = (listener: WebsocketListener) => {\n const existing = this._listeners.get(listener.key);\n if (existing) {\n existing.setSendToConnection(null);\n this._listeners.delete(existing.key);\n }\n clearTimeout(this.closeConnectionTimeOut);\n this.scheduleConnectionCleanup();\n };\n\n /** Schedules connection close after configured delay when no listeners remain. */\n private scheduleConnectionCleanup = () => {\n const { connectionCleanupDelayMs } = this._client;\n\n this.closeConnectionTimeOut = setTimeout(() => {\n if (this._listeners.size === 0) {\n this._socket?.close();\n this._client.removeConnection(this.url);\n }\n }, connectionCleanupDelayMs);\n };\n\n // ─── Public API: Connection Control ─────────────────────────────────\n\n /**\n * Replaces the WebSocket URL and re-establishes the connection.\n *\n * Closes the current connection, resets all listeners, and reconnects using the new URL\n * after a short delay (1 second) to allow cleanup to complete.\n *\n * @param newUrl - The new WebSocket URL to connect to\n */\n public replaceUrl = async (newUrl: string) => {\n if (this._url !== newUrl) {\n this._url = newUrl;\n await this.teardownAndReconnect();\n }\n };\n\n /**\n * Reconnects the WebSocket connection.\n *\n * Tears down the current connection by removing all event listeners, closing the socket,\n * and resetting all registered URI APIs. After a short delay (1 second) to allow cleanup,\n * re-establishes the connection. Typically triggered by {@link websocketConnectionsReconnect}\n * when {@link useReconnectWebsocketConnections} (from @mono-fleet/common-components) detects\n * the user's authentication context (region/role) change.\n *\n * Guarded by {@link _isReconnecting} in {@link teardownAndReconnect} — if a `replaceUrl`\n * layout effect already started a reconnect cycle in the same render, this call is a no-op.\n */\n public reconnect = async () => {\n await this.teardownAndReconnect();\n };\n\n /**\n * Resets the retry counter and re-establishes the connection.\n *\n * Used when the user manually retries after hitting {@link RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS}.\n * Clears the max-retries-exceeded state and initiates a fresh connection attempt.\n */\n public resetRetriesAndReconnect = (): void => {\n this.reconnectTries = 0;\n this._maxRetriesExceeded = false;\n this.connect();\n };\n\n // ─── Connection Lifecycle (Private) ─────────────────────────────────\n\n /**\n * Establishes the WebSocket connection if not already connecting or connected.\n * Only creates a socket if at least one registered listener (subscription or message API) is enabled.\n * Sets up all event listeners and logs the connection attempt via the custom logger if configured.\n */\n private connect = () => {\n const hasEnabledListener = Array.from(this._listeners.values()).some(\n (listener) => listener.isEnabled\n );\n if (isConnectionReady(this._socket) || !hasEnabledListener) {\n return;\n }\n this._client.connectionEvent?.({\n type: \"connect\",\n url: this._url,\n retries: this.reconnectTries,\n uriApis: getSubscriptionUris(this._listeners),\n });\n this._socket = new WebSocket(this._url);\n this._socket.addEventListener(\"close\", this.handleClose);\n this._socket.addEventListener(\"message\", this.handleMessage);\n this._socket.addEventListener(\"open\", this.handleOpen);\n this._socket.addEventListener(\"error\", this.handleError);\n };\n\n /**\n * Tears down the current socket: clears all timers, removes all event listeners,\n * closes the socket, and resets the socket reference.\n */\n private teardownSocket = () => {\n this.clearAllTimers();\n this.removeListeners();\n this._socket?.close();\n this._socket = undefined;\n };\n\n /**\n * Tears down the current connection, resets all listeners, waits for cleanup to complete,\n * and re-establishes the connection. Shared by {@link replaceUrl} and {@link reconnect}.\n *\n * Guarded by {@link _isReconnecting} to prevent concurrent cycles. When\n * `selectedRegionRole` changes, both the hook's `replaceUrl` layout effect and\n * `useReconnectWebsocketConnections`'s reconnect effect may fire. Because layout effects run\n * before regular effects, `replaceUrl` wins and updates the URL first; the reconnect call\n * is safely skipped.\n */\n private teardownAndReconnect = async () => {\n if (this._isReconnecting) return;\n this._isReconnecting = true;\n try {\n this.teardownSocket();\n this._listeners.forEach((listener) => listener.reset());\n this.reconnectTries = 0;\n this._maxRetriesExceeded = false;\n await wait(TEARDOWN_RECONNECT_DELAY_MS);\n this.connect();\n } finally {\n this._isReconnecting = false;\n }\n };\n\n /**\n * Cleans up the WebSocket connection when no listeners are registered.\n */\n private cleanupConnection = () => {\n if (this._listeners.size === 0) {\n this._client.connectionEvent?.({\n type: \"cleanup\",\n url: this._url,\n });\n this.removeListeners();\n this._socket = undefined;\n }\n };\n\n /**\n * Clears all active timers (ping heartbeat, pong timeout, and connection cleanup).\n */\n private clearAllTimers = () => {\n clearTimeout(this.pingTimeOut);\n clearTimeout(this.pongTimeOut);\n clearTimeout(this.closeConnectionTimeOut);\n };\n\n /**\n * Removes all event listeners from the WebSocket and window objects.\n * Used during cleanup and reconnection processes.\n */\n private removeListeners = () => {\n this._socket?.removeEventListener(\"message\", this.handleMessage);\n this._socket?.removeEventListener(\"close\", this.handleClose);\n this._socket?.removeEventListener(\"open\", this.handleOpen);\n this._socket?.removeEventListener(\"error\", this.handleError);\n\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnline);\n window.removeEventListener(\"online\", this.handleOnlineForReconnection);\n window.removeEventListener(\"offline\", this.handleOffline);\n }\n };\n\n // ─── Reconnection Logic (Private) ──────────────────────────────────\n\n /**\n * Attempts to reconnect the WebSocket connection with exponential backoff.\n * Shows user notifications after the threshold number of attempts.\n * Only attempts reconnection when the browser is online.\n * When closeCode is 1013 (Try Again Later), waits an extra delay before reconnecting.\n * Stops after configured MAX_RETRY_ATTEMPTS and shows a permanent error with a manual retry button.\n *\n * @param closeCode - Optional WebSocket close code; used to apply TRY_AGAIN_LATER delay when 1013\n */\n private attemptReconnection = async (closeCode?: number) => {\n const { maxRetryAttempts, notificationThreshold, tryAgainLaterDelayMs } =\n this._client;\n if (this.reconnectTries >= maxRetryAttempts) {\n this._maxRetriesExceeded = true;\n this._client.connectionEvent?.({\n type: \"max-retries-exceeded\",\n url: this._url,\n retries: this.reconnectTries,\n });\n return;\n }\n\n if (this.deferReconnectionUntilOnline()) {\n return;\n }\n\n if (closeCode === WEBSOCKET_CLOSE_CODES.TRY_AGAIN_LATER) {\n if (this.reconnectTries > notificationThreshold) {\n this._client.connectionEvent?.({\n type: \"reconnecting\",\n url: this._url,\n retries: this.reconnectTries,\n });\n }\n\n await wait(tryAgainLaterDelayMs);\n if (this.deferReconnectionUntilOnline()) {\n return;\n }\n }\n\n this.reconnectTries++;\n\n const waitTime = reconnectWaitTime(\n this.reconnectTries,\n this._client.delays,\n this._client.phaseThresholds\n );\n\n if (this.reconnectTries > notificationThreshold) {\n this._client.connectionEvent?.({\n type: \"reconnecting\",\n url: this._url,\n retries: this.reconnectTries,\n });\n }\n await wait(waitTime);\n\n // Check again after waiting - browser might have gone offline during the wait\n if (this.deferReconnectionUntilOnline()) {\n return;\n }\n\n if (this.reconnectTries > notificationThreshold) {\n this._client.connectionEvent?.({\n type: \"reconnecting\",\n url: this._url,\n retries: this.reconnectTries,\n });\n }\n this.connect();\n };\n\n /**\n * Checks if the browser is offline and, if so, defers reconnection until it comes back online\n * by registering a one-time 'online' event listener.\n *\n * @returns `true` if reconnection was deferred (browser is offline), `false` if browser is online\n */\n private deferReconnectionUntilOnline = (): boolean => {\n if (isBrowserOnline()) {\n return false;\n }\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", this.handleOnlineForReconnection, {\n once: true,\n });\n }\n return true;\n };\n\n // ─── WebSocket Event Handlers ──────────────────────────────────────\n\n /**\n * Handles WebSocket close events.\n *\n * Implements automatic reconnection for any non-intentional close (anything other than\n * 1000 Normal Closure). This includes 1001 Going Away, 1011 Internal Error, 1012 Service\n * Restart, 1013 Try Again Later, 1006 Abnormal Closure, and other server-initiated codes.\n * Reconnection only occurs when listeners are still registered. Shows user notifications\n * after {@link RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD} failed attempts.\n * Cleans up the connection if no listeners remain. Logs the close event via the custom logger if configured.\n *\n * @param event - The WebSocket close event containing code, reason, and whether the close was clean\n */\n private handleClose = async (event: CloseEvent) => {\n this.clearAllTimers();\n\n this._client.connectionEvent?.({\n type: \"close\",\n url: this._url,\n code: event.code,\n reason: event.reason,\n wasClean: event.wasClean,\n subscriptions: this._listeners.size,\n });\n\n const shouldReconnect = isReconnectableCloseCode(event.code);\n const hasRegisteredApis = this._listeners.size > 0;\n\n if (shouldReconnect && hasRegisteredApis) {\n await this.attemptReconnection(event.code);\n }\n\n this.cleanupConnection();\n };\n\n /**\n * Handles WebSocket open/connected events.\n *\n * Sets up offline detection, dismisses reconnection notifications, shows success message\n * for recovered connections (only if {@link RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD}\n * was exceeded), resets reconnection counter, notifies all listeners, flushes cached\n * messages, and initiates the heartbeat ping sequence.\n */\n private handleOpen = () => {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"offline\", this.handleOffline);\n }\n\n this.reconnectTries = 0;\n\n const socket = this._socket;\n if (socket) {\n this._listeners.forEach((listener) => listener.onOpen?.());\n\n this._client.connectionEvent?.({\n type: \"open\",\n url: this._url,\n retries: this.reconnectTries,\n uriApis: getSubscriptionUris(this._listeners),\n });\n this.cachedMessages.forEach((message) =>\n socket.send(this.serializeMessage(message))\n );\n }\n this.cachedMessages = [];\n if (this._client.heartbeat.enabled) {\n this.schedulePing();\n }\n };\n\n /**\n * Handles incoming WebSocket messages.\n *\n * Routes messages to matching listeners: subscription APIs by URI, message APIs by pending request URI.\n * Special handling for 'ping' messages to maintain heartbeat.\n * Dispatches error-method messages to listener error handlers.\n *\n * @param event - The WebSocket message event containing JSON data\n */\n private handleMessage = (event: MessageEvent<string>) => {\n try {\n const parsed: unknown = JSON.parse(event.data);\n\n if (!isValidIncomingMessage(parsed)) {\n this._client.connectionEvent?.({\n type: \"invalid-message\",\n url: this._url,\n uriApis: getSubscriptionUris(this._listeners),\n message: parsed,\n });\n this._listeners.forEach((listener) =>\n listener.onError({ type: \"transport\", event })\n );\n return;\n }\n\n if (parsed.uri === \"ping\") {\n this.clearPongTimeout();\n if (this._client.heartbeat.enabled) {\n this.schedulePing();\n }\n return;\n }\n\n if (isErrorMethod(parsed.method)) {\n this._client.connectionEvent?.({\n type: \"message-error\",\n url: this._url,\n uri: parsed.uri,\n uriApis: getSubscriptionUris(this._listeners),\n message: parsed,\n });\n this.forEachMatchingListener(parsed.uri, (listener) =>\n listener.onMessageError!({ type: \"server\", message: parsed })\n );\n return;\n }\n\n this.forEachMatchingListener(parsed.uri, (listener) => {\n if (listener.uri === parsed.uri) {\n listener.onMessage?.(parsed.body);\n } else {\n listener.deliverMessage?.(parsed.uri, parsed.body);\n }\n });\n } catch (error) {\n this._client.connectionEvent?.({\n type: \"parse-error\",\n url: this._url,\n uriApis: getSubscriptionUris(this._listeners),\n message: event.data,\n error: error,\n });\n this._listeners.forEach((listener) =>\n listener.onError({ type: \"transport\", event })\n );\n }\n };\n\n /**\n * Handles WebSocket error events.\n * Logs the error via the custom logger if configured and notifies all registered listeners.\n *\n * @param event - The WebSocket error event\n */\n private handleError = (event: Event) => {\n this._listeners.forEach((listener) =>\n listener.onError({ type: \"transport\", event })\n );\n\n this._client.connectionEvent?.({\n type: \"error\",\n url: this._url,\n uriApis: getSubscriptionUris(this._listeners),\n event: event,\n });\n };\n\n // ─── Browser Online/Offline Handlers ───────────────────────────────\n\n /**\n * Handles browser coming back online during offline detection.\n * Removes the online listener and re-establishes the connection.\n */\n private handleOnline = () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnline);\n }\n this.connect();\n };\n\n /**\n * Handles browser coming back online during reconnection attempts.\n * Removes the online listener and resumes reconnection with a decremented counter\n * to avoid adding extra wait time from being offline.\n */\n private handleOnlineForReconnection = () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", this.handleOnlineForReconnection);\n }\n this.reconnectTries--;\n this.attemptReconnection();\n };\n\n /**\n * Handles browser going offline.\n *\n * Notifies all listeners of the closure, tears down the socket, and sets up\n * a listener to reconnect when the browser comes back online.\n */\n private handleOffline = () => {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"offline\", this.handleOffline);\n }\n if (this._socket) {\n this._listeners.forEach((listener) =>\n listener.onClose(new CloseEvent(\"offline\"))\n );\n }\n this.teardownSocket();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", this.handleOnline);\n }\n };\n\n // ─── Message Utilities (Private) ───────────────────────────────────\n\n /**\n * Handles outgoing messages from listeners.\n *\n * - If socket is OPEN: serializes with correlation ID and sends immediately.\n * - If socket is not open: subscribe messages trigger connect only; other messages are\n * cached and sent when the connection opens.\n *\n * Passed to each listener via {@link WebsocketListener.setSendToConnection}.\n */\n private handleSendMessage = (message: SendMessage<string, string, any>) => {\n if (this._socket?.readyState === WebSocket.OPEN) {\n this._client.connectionEvent?.({\n type: \"send-message\",\n url: this._url,\n uri: message.uri,\n body: message.body,\n method: message.method,\n });\n this._socket.send(this.serializeMessage(message));\n return;\n }\n\n if (message.method !== \"subscribe\") {\n this.cachedMessages.push(message);\n }\n this.connect();\n };\n\n /**\n * Sends a heartbeat ping message to keep the connection alive and detect disconnections.\n * Sets a pong timeout; if no pong arrives within HEARTBEAT_CONFIG.PONG_TIMEOUT_MS,\n * the connection is force-closed to trigger reconnection.\n */\n private sendPing = () => {\n if (!isSocketOnline(this._socket)) return;\n this._socket?.send(this.serializeMessage(createPingMessage()));\n this.schedulePongTimeout();\n };\n\n /**\n * Clears the pong timeout (e.g. when a pong is received).\n */\n private clearPongTimeout = () => {\n clearTimeout(this.pongTimeOut);\n this.pongTimeOut = undefined;\n };\n\n /**\n * Schedules a timeout to detect missing pong. If no pong arrives within\n * configured pong timeout, force-closes the socket to trigger reconnection.\n */\n private schedulePongTimeout = () => {\n this.clearPongTimeout();\n const pongTimeoutMs = this._client.heartbeat.pongTimeoutMs;\n this.pongTimeOut = setTimeout(() => {\n this._client.connectionEvent?.({\n type: \"pong-timeout\",\n url: this._url,\n });\n this.teardownSocket();\n this.attemptReconnection();\n }, pongTimeoutMs);\n };\n\n /**\n * Schedules the next heartbeat ping after the configured interval (40 seconds).\n * @see {@link getPingTime}\n */\n private schedulePing = () => {\n this.pingTimeOut = setTimeout(() => {\n this.sendPing();\n }, getPingTime());\n };\n\n /**\n * Serializes a message with a unique correlation ID for WebSocket transmission.\n * @param message - The message to serialize\n * @returns JSON string for WebSocket send\n */\n private serializeMessage = (\n message: SendMessage<string, string, any>\n ): string => {\n const transformMessagePayload = this._client.transformMessagePayload;\n if (transformMessagePayload) {\n message = transformMessagePayload(message);\n }\n return JSON.stringify(message);\n };\n\n /**\n * Executes a callback for each listener that matches the given URI.\n *\n * - **Subscription listeners**: Match when `listener.uri === uri`\n * - **Message listeners**: Match when `listener.hasWaitingUri(uri)` (pending request/response)\n *\n * A single message can be delivered to multiple listeners if both a subscription\n * and a message API are waiting for the same URI.\n *\n * @param uri - The URI from the incoming message\n * @param callback - Callback invoked for each matching listener\n */\n private forEachMatchingListener = (\n uri: string,\n callback: (listener: WebsocketListener) => void\n ) => {\n this._listeners.forEach((listener) => {\n if (listener.uri === uri || listener.hasWaitingUri?.(uri)) {\n callback(listener);\n }\n });\n };\n}\n","/**\n * @fileoverview Global WebSocket configuration for all WebsocketConnection instances.\n *\n * Provides a single source of truth for connection behavior. Instantiate with\n * {@link WebsocketClientOverrides} at app startup to customize defaults.\n * Use the connectionEvent callback to configure event logging.\n *\n * @module WebsocketClient\n */\n\nimport { Store } from '@tanstack/react-store';\nimport {\n CONNECTION_CLEANUP_DELAY_MS,\n DEFAULT_HEARTBEAT_CONFIG,\n DEFAULT_MESSAGE_RESPONSE_TIMEOUT_MS,\n RECONNECTION_CONFIG\n} from './constants';\nimport type { HeartbeatConfig, SendMessage, WebsocketClientOverrides, WebsocketListener, WebsocketLoggerConnectionEvent } from './types';\nimport { WebsocketConnection } from './WebsocketConnection';\nimport type { WebsocketMessageApi } from './WebsocketMessageApi';\nimport type { WebsocketSubscriptionApi } from './WebsocketSubscriptionApi';\n\n/**\n * Global WebSocket configuration used by all WebsocketConnection instances.\n *\n * Instantiate with {@link WebsocketClientOverrides} to customize behavior.\n * All overrides are merged with defaults; partial overrides are supported.\n *\n * @example\n * ```typescript\n * const client = new WebsocketClient({\n * maxRetryAttempts: 10,\n * heartbeat: { enabled: false },\n * messageResponseTimeoutMs: 5000\n * });\n * ```\n */\nexport class WebsocketClient {\n /**\n * Global map of active WebSocket connections, keyed by URL.\n *\n * One connection per key. Managed by {@link addConnection} and {@link removeConnection}.\n */\n private _connections = new Store<Map<string, WebsocketConnection>>(new Map());\n\n /**\n * Global map of active WebSocket listeners (subscription and message APIs), keyed by API key.\n *\n * One listener per key. Subscription APIs have `uri`; message APIs have `hasWaitingUri`.\n * Managed by {@link createWebsocketSubscriptionApi}, {@link createWebsocketMessageApi},\n * and {@link removeWebsocketListenerFromConnection}.\n */\n private _listeners = new Store<Map<string, WebsocketListener>>(new Map());\n\n /** Maximum reconnection attempts before stopping. */\n public maxRetryAttempts: number;\n /** Attempts before showing user notifications. */\n public notificationThreshold: number;\n /** Delay (ms) when server closes with 1013 Try Again Later. */\n public tryAgainLaterDelayMs: number;\n /** Delay durations (ms) for each reconnection phase. */\n public delays: {\n firstPhase: number;\n secondPhase: number;\n thirdPhase: number;\n };\n public phaseThresholds: {\n first: number;\n second: number;\n };\n /** Delay (ms) before closing connection when no listeners remain. */\n public connectionCleanupDelayMs: number;\n /** Default timeout (ms) for message API responses. */\n public messageResponseTimeoutMs: number;\n /** Heartbeat (ping/pong) configuration. */\n public heartbeat: HeartbeatConfig;\n /** Optional transform for outgoing message payloads. */\n public transformMessagePayload: ((payload: SendMessage<string, string, unknown>) => SendMessage<string, string, unknown>) | undefined;\n /** Optional callback for connection event logging. */\n public connectionEvent: ((event: WebsocketLoggerConnectionEvent) => void) | undefined;\n\n /**\n * Creates a new WebsocketClient with optional overrides.\n *\n * All overrides are merged with defaults from {@link RECONNECTION_CONFIG},\n * {@link CONNECTION_CLEANUP_DELAY_MS}, and {@link DEFAULT_HEARTBEAT_CONFIG}.\n *\n * @param overrides - Partial configuration overrides. Omitted values use defaults.\n */\n constructor({\n maxRetryAttempts,\n notificationThreshold,\n tryAgainLaterDelayMs,\n delays,\n phaseThresholds,\n connectionCleanupDelayMs,\n messageResponseTimeoutMs,\n heartbeat,\n transformMessagePayload,\n connectionEvent\n }: WebsocketClientOverrides) {\n this.maxRetryAttempts = maxRetryAttempts ?? RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS;\n this.notificationThreshold = notificationThreshold ?? RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD;\n this.tryAgainLaterDelayMs = tryAgainLaterDelayMs ?? RECONNECTION_CONFIG.TRY_AGAIN_LATER_DELAY_MS;\n this.delays = {\n firstPhase: delays?.firstPhase ?? RECONNECTION_CONFIG.DELAYS.FIRST_PHASE,\n secondPhase: delays?.secondPhase ?? RECONNECTION_CONFIG.DELAYS.SECOND_PHASE,\n thirdPhase: delays?.thirdPhase ?? RECONNECTION_CONFIG.DELAYS.THIRD_PHASE\n };\n this.phaseThresholds = {\n first: phaseThresholds?.first ?? RECONNECTION_CONFIG.PHASE_THRESHOLDS.FIRST,\n second: phaseThresholds?.second ?? RECONNECTION_CONFIG.PHASE_THRESHOLDS.SECOND\n };\n this.connectionCleanupDelayMs = connectionCleanupDelayMs ?? CONNECTION_CLEANUP_DELAY_MS;\n this.messageResponseTimeoutMs = messageResponseTimeoutMs ?? DEFAULT_MESSAGE_RESPONSE_TIMEOUT_MS;\n this.heartbeat = {\n enabled: heartbeat?.enabled ?? DEFAULT_HEARTBEAT_CONFIG.enabled,\n pongTimeoutMs: heartbeat?.pongTimeoutMs ?? DEFAULT_HEARTBEAT_CONFIG.pongTimeoutMs\n };\n this.transformMessagePayload = transformMessagePayload ?? undefined;\n\n this.connectionEvent = connectionEvent ?? undefined;\n }\n\n /** Reconnects all active WebSocket connections. Use after auth/region change. */\n public reconnectAllConnections = () => {\n this._connections.state.forEach((connection) => {\n connection.reconnect();\n });\n };\n\n /** Registers a listener (subscription or message API) in the client. */\n public addListener = (listener: WebsocketListener) => {\n this._listeners.setState((prev) => {\n const next = new Map(prev);\n next.set(listener.key, listener);\n return next;\n });\n };\n\n /** Unregisters a listener from the client. */\n public removeListener = (listener: WebsocketListener) => {\n this._listeners.setState((prev) => {\n const next = new Map(prev);\n next.delete(listener.key);\n return next;\n });\n };\n\n /**\n * Returns a listener by key and type.\n *\n * @param key - The listener's unique key\n * @param type - `'subscription'` or `'message'`\n * @returns The listener if found, otherwise undefined\n */\n public getListener<TData = unknown, TBody = unknown>(\n key: string,\n type: 'subscription'\n ): WebsocketSubscriptionApi<TData, TBody> | undefined;\n public getListener(key: string, type: 'message'): WebsocketMessageApi | undefined;\n public getListener<TData = unknown, TBody = unknown>(\n key: string,\n type: 'subscription' | 'message'\n ): WebsocketSubscriptionApi<TData, TBody> | WebsocketMessageApi | undefined {\n const listener = this._listeners.state.get(key);\n if (listener && listener.type === type) {\n return listener as WebsocketSubscriptionApi<TData, TBody> | WebsocketMessageApi;\n }\n return undefined;\n }\n\n /** Returns the WebSocket connection for the given URL key, or undefined. */\n public getConnection = (key: string): WebsocketConnection | undefined => {\n return this._connections.state.get(key);\n };\n\n /**\n * Adds or returns an existing WebSocket connection for the given URL.\n *\n * @param key - The key used to identify the connection (typically the URL)\n * @param url - The WebSocket URL to connect to\n * @returns The existing or newly created connection\n */\n public addConnection = (key: string, url: string) => {\n const existingConnection = this._connections.state.get(key);\n if (existingConnection) {\n return existingConnection;\n }\n const connection = new WebsocketConnection(url, this);\n this._connections.setState((prev) => {\n const next = new Map(prev);\n next.set(key, connection);\n return next;\n });\n return connection;\n };\n\n /**\n * Removes a connection from the client.\n *\n * @param url - The WebSocket URL used as the key when calling {@link addConnection}.\n */\n public removeConnection = (url: string) => {\n this._connections.setState((prev) => {\n const next = new Map(prev);\n next.delete(url);\n return next;\n });\n };\n}\n","/**\n * React context provider for WebSocket client.\n *\n * @module WebsocketProvider\n */\n\nimport { createContext, FunctionComponent, PropsWithChildren, useContext } from 'react';\nimport { WebsocketClient } from './WebsocketClient';\n\nconst WebsocketClientContext = createContext<WebsocketClient | undefined>(undefined);\n\n/**\n * Returns the {@link WebsocketClient} from the nearest {@link WebsocketClientProvider}.\n *\n * Must be used within a `WebsocketClientProvider`; throws otherwise.\n *\n * @returns The WebsocketClient instance\n * @throws Error if used outside WebsocketClientProvider\n *\n * @example\n * ```typescript\n * const client = useWebsocketClient();\n * const api = useWebsocketSubscription({ key: 'my-sub', url: '...', uri: '...' });\n * ```\n */\nexport const useWebsocketClient = (): WebsocketClient => {\n const client = useContext(WebsocketClientContext);\n if (!client) {\n throw new Error('useWebsocketClient must be used within a WebsocketClientProvider');\n }\n return client;\n};\n\n/** Props for {@link WebsocketClientProvider}. */\ninterface WebsocketClientProviderProps {\n /** The WebsocketClient instance to provide to descendants. */\n client: WebsocketClient;\n}\n\n/**\n * Provides a {@link WebsocketClient} to the component tree.\n *\n * Wrap your app (or the part that uses WebSocket hooks) with this provider.\n * Create the client once (e.g. at app startup) and pass it here.\n *\n * @example\n * ```typescript\n * const client = new WebsocketClient({ maxRetryAttempts: 10 });\n * <WebsocketClientProvider client={client}>\n * <App />\n * </WebsocketClientProvider>\n * ```\n */\nexport const WebsocketClientProvider: FunctionComponent<PropsWithChildren<WebsocketClientProviderProps>> = ({ children, client }) => {\n return <WebsocketClientContext.Provider value={client}>{children}</WebsocketClientContext.Provider>;\n};\n","/**\n * @fileoverview WebSocket Message API for request/response style messaging.\n *\n * Send to any URI; optionally await a response. No subscription support.\n * Used by {@link useWebsocketMessage}. See {@link WebsocketSubscriptionApi} for\n * streaming subscriptions.\n *\n * @module WebsocketMessageApi\n */\n\nimport { WebsocketClient } from './WebsocketClient';\nimport { INITIATOR_REMOVAL_DELAY_MS } from './constants';\nimport {\n SendMessage,\n SendMessageOptions,\n SendToConnectionFn,\n WebsocketListener,\n WebsocketMessageOptions,\n WebsocketServerError,\n WebsocketTransportError\n} from './types';\n\ninterface PendingRequest<TData = unknown> {\n resolve: (value: TData) => void;\n reject: (reason: unknown) => void;\n timeoutId: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Manages WebSocket request/response messaging without subscription.\n *\n * Use for one-off commands (validate, modify, mark read) rather than streaming.\n * Send to any URI; optionally await a response. Tracks URIs only while waiting.\n *\n * ## Key Features\n *\n * - **Any URI**: Not bound to a single URI like {@link WebsocketSubscriptionApi}\n * - **Request/Response**: `sendMessage` returns a Promise; optional per-call timeout\n * - **Fire-and-forget**: `sendMessageNoWait` for commands that don't need a response\n * - **No Subscription**: Use WebsocketSubscriptionApi for streaming data\n *\n * ## Edge Cases\n *\n * - **Overwrite**: Sending to the same URI while a request is pending cancels the previous\n * request — the previous Promise rejects with \"WebSocket request overwritten for URI\".\n * - **Disabled**: When `enabled=false`, `sendMessage` rejects; `sendMessageNoWait` is a no-op.\n * - **Connection closed**: All pending requests reject with \"WebSocket connection closed\".\n * - **Queued messages**: If the connection is not yet open, messages are queued and sent\n * when the connection opens (via `setSendToConnection`).\n *\n * ## Cleanup\n *\n * {@link reset} is called by WebsocketConnection when the URL changes or during reconnection.\n * When the last hook unmounts, {@link unregisterHook} triggers removal after\n * {@link INITIATOR_REMOVAL_DELAY_MS}.\n *\n * @template TData - The type of data received in the response\n * @template TBody - The type of message body sent to the WebSocket\n *\n * @example\n * ```typescript\n * const api = new WebsocketMessageApi<MyResponse, MyRequest>({\n * url: 'wss://example.com',\n * key: 'my-message-api',\n * responseTimeoutMs: 5000\n * });\n * connection.addListener(api);\n *\n * const response = await api.sendMessage('/api/command', 'post', { action: 'refresh' });\n * ```\n */\nexport class WebsocketMessageApi implements WebsocketListener {\n private _options: WebsocketMessageOptions;\n private _sendToConnection: SendToConnectionFn | null = null;\n private _pendingByUri: Map<string, PendingRequest> = new Map();\n private _pendingMessages: SendMessage<string, string, unknown>[] = [];\n private _registeredHooks: Set<string> = new Set();\n private _hookRemovalTimeout: ReturnType<typeof setTimeout> | undefined;\n private _client: WebsocketClient;\n public readonly type = 'message';\n\n /**\n * Creates a new WebsocketMessageApi.\n *\n * @param options - Configuration options (url, key, callbacks, etc.)\n * @param client - The {@link WebsocketClient} for timeout defaults and connection management\n */\n constructor(options: WebsocketMessageOptions, client: WebsocketClient) {\n this._client = client;\n const defaultTimeout = client.messageResponseTimeoutMs;\n this._options = {\n enabled: true,\n responseTimeoutMs: defaultTimeout,\n ...options\n };\n }\n\n /** Unique key identifier for this Message API. */\n public get key(): string {\n return this._options.key;\n }\n\n /** WebSocket URL for Datadog tracking. */\n public get url(): string {\n return this._options.url;\n }\n\n /** Whether this Message API is enabled. */\n public get isEnabled(): boolean {\n return this._options.enabled ?? true;\n }\n\n /**\n * Returns whether this API is waiting for a response for the given URI.\n *\n * Used by {@link WebsocketConnection} to route incoming messages to the correct\n * listener. Message API receives messages only for URIs with pending requests.\n *\n * @param uri - The URI to check\n * @returns `true` if a request is pending for this URI\n */\n public hasWaitingUri = (uri: string): boolean => {\n return this._pendingByUri.has(uri);\n };\n\n /**\n * Registers a hook (component) that is using this Message API.\n *\n * Tracks the hook ID so the API is only removed from the connection when\n * the last hook unmounts.\n *\n * @param id - Unique identifier for the registering hook\n */\n public registerHook = (id: string): void => {\n this._clearHookRemovalTimeout();\n this._registeredHooks.add(id);\n };\n\n /**\n * Unregisters a hook from this Message API.\n *\n * After {@link INITIATOR_REMOVAL_DELAY_MS}, if no hooks remain, invokes the\n * cleanup callback to remove this API from the connection. The delay prevents\n * rapid subscribe/unsubscribe cycles during React re-renders.\n *\n * @param id - The hook ID to unregister\n * @param onRemove - Callback invoked when the last hook is removed (after delay)\n */\n public unregisterHook = (id: string, onRemove: () => void): void => {\n this._registeredHooks.delete(id);\n this._scheduleHookRemoval(onRemove);\n };\n\n /**\n * Disconnects this Message API from the parent WebSocket connection.\n *\n * Called when the hook is disabled. After a delay, invokes the cleanup callback.\n * Clears any pending hook-removal timeout to avoid duplicate cleanup.\n *\n * @param onRemoveFromSocket - Callback invoked after delay to remove from connection\n */\n public disconnect = (onRemoveFromSocket: () => void): void => {\n this._clearHookRemovalTimeout();\n this._hookRemovalTimeout = setTimeout(() => {\n this._hookRemovalTimeout = undefined;\n onRemoveFromSocket();\n }, INITIATOR_REMOVAL_DELAY_MS);\n };\n\n /**\n * Sets or clears the callback used to send messages through the parent WebSocket connection.\n *\n * When setting a callback, flushes any queued messages. When clearing, cancels all\n * pending requests and clears the hook removal timeout to avoid redundant cleanup.\n *\n * @param callback - The send function, or null to disconnect\n */\n public setSendToConnection = (callback: SendToConnectionFn | null): void => {\n this._sendToConnection = callback;\n\n if (callback) {\n this._flushPendingMessages(callback);\n } else {\n this._clearHookRemovalTimeout();\n this._pendingMessages = [];\n this._cancelAllPending();\n }\n };\n\n /**\n * Delivers an incoming message for a URI we're waiting on.\n *\n * Called by WebsocketConnection when a message arrives for a URI with a pending request.\n *\n * @param uri - The URI the response is for\n * @param data - The response data\n */\n public deliverMessage = (uri: string, data: unknown): void => {\n const pending = this._pendingByUri.get(uri);\n if (!pending) return;\n\n clearTimeout(pending.timeoutId);\n this._pendingByUri.delete(uri);\n pending.resolve(data);\n };\n\n /**\n * Sends a message to the given URI and optionally waits for a response.\n *\n * **Overwrite behavior**: If a request is already pending for this URI, it is\n * cancelled (rejected with \"WebSocket request overwritten for URI\") and replaced.\n *\n * @param uri - The URI to send the message to\n * @param bodyOrMethod - Message body (short form) or HTTP method (full form)\n * @param bodyOrOptions - Message body or options (full form)\n * @param options - Per-call options when using full signature\n * @returns Promise that resolves with the response data; rejects on timeout, overwrite, or disabled\n *\n * @example\n * await api.sendMessage('/api/command', 'post', { action: 'refresh' });\n * await api.sendMessage('/api/command', 'post', { action: 'refresh' }, { timeout: 5000 });\n */\n public sendMessage<TData = unknown, TBody = unknown>(\n uri: string,\n method: string,\n body?: TBody,\n options?: SendMessageOptions\n ): Promise<TData> {\n if (!this.isEnabled) {\n return Promise.reject(new Error('WebsocketMessageApi is disabled'));\n }\n\n this._cancelPendingForUri(uri);\n\n const timeoutMs = options?.timeout ?? this._options.responseTimeoutMs ?? this._client.messageResponseTimeoutMs;\n\n return new Promise<TData>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n if (this._pendingByUri.get(uri)?.timeoutId === timeoutId) {\n this._pendingByUri.delete(uri);\n reject(new Error(`WebSocket response timeout for URI: ${uri}`));\n }\n }, timeoutMs);\n\n this._pendingByUri.set(uri, {\n resolve: (v: unknown) => resolve(v as TData),\n reject,\n timeoutId\n });\n\n const message: SendMessage<string, string, TBody> = { uri, method, body };\n this._sendOrQueue<TBody>(message);\n });\n }\n\n /**\n * Sends a message without waiting for a response (fire-and-forget).\n *\n * @param uri - The URI to send the message to\n * @param methodOrBody - HTTP method (full form) or message body (short form)\n * @param body - Message body when using full form\n *\n * @example\n * api.sendMessageNoWait('/api/log', 'post', { event: 'click' });\n */\n public sendMessageNoWait<TBody = unknown>(uri: string, method: string, body?: TBody): void {\n if (!this.isEnabled) return;\n\n const message: SendMessage<string, string, TBody> = { uri, method, body };\n this._sendOrQueue<TBody>(message);\n }\n\n /** @inheritdoc */\n public onError = (error: WebsocketTransportError): void => {\n this._options.onError?.(error);\n };\n\n /** @inheritdoc */\n public onMessageError = (error: WebsocketServerError): void => {\n this._options.onMessageError?.(error);\n };\n\n /** @inheritdoc */\n public onClose = (event: CloseEvent): void => {\n this._cancelAllPending();\n this._options.onClose?.(event);\n };\n\n /**\n * Resets this Message API, cancelling all pending requests.\n *\n * Called by WebsocketConnection when the URL changes or during reconnection.\n * Clears the hook removal timeout to prevent stale cleanup callbacks.\n */\n public reset = (): void => {\n this._clearHookRemovalTimeout();\n this._cancelAllPending();\n };\n\n private _clearHookRemovalTimeout(): void {\n if (this._hookRemovalTimeout !== undefined) {\n clearTimeout(this._hookRemovalTimeout);\n this._hookRemovalTimeout = undefined;\n }\n }\n\n private _scheduleHookRemoval(onRemove: () => void): void {\n this._clearHookRemovalTimeout();\n this._hookRemovalTimeout = setTimeout(() => {\n this._hookRemovalTimeout = undefined;\n if (this._registeredHooks.size === 0) {\n onRemove();\n }\n }, INITIATOR_REMOVAL_DELAY_MS);\n }\n\n private _flushPendingMessages(callback: SendToConnectionFn): void {\n if (this._pendingMessages.length === 0) return;\n this._pendingMessages.forEach((msg) => callback(msg));\n this._pendingMessages = [];\n }\n\n private _sendOrQueue<TBody = unknown>(message: SendMessage<string, string, TBody>): void {\n if (this._sendToConnection) {\n this._sendToConnection(message);\n } else {\n this._pendingMessages.push(message);\n }\n }\n\n private _cancelPendingForUri(uri: string): void {\n const pending = this._pendingByUri.get(uri);\n if (pending) {\n clearTimeout(pending.timeoutId);\n this._pendingByUri.delete(uri);\n pending.reject(new Error(`WebSocket request overwritten for URI: ${uri}`));\n }\n }\n\n private _cancelAllPending(): void {\n this._pendingByUri.forEach((pending) => {\n clearTimeout(pending.timeoutId);\n pending.reject(new Error('WebSocket connection closed'));\n });\n this._pendingByUri.clear();\n }\n}\n","/**\n * @fileoverview WebSocket subscription API for streaming data over a single URI.\n *\n * Manages subscribe/unsubscribe lifecycle, reactive store updates, and hook tracking.\n * Used by {@link useWebsocketSubscription}. See {@link WebsocketMessageApi} for\n * request/response style messaging.\n *\n * @module WebsocketSubscriptionApi\n */\n\nimport { Store } from '@tanstack/react-store';\nimport { deepEqual } from 'fast-equals';\nimport { DEFAULT_URI_OPTIONS, INITIATOR_REMOVAL_DELAY_MS } from './constants';\nimport {\n createInitialWebsocketSubscriptionStore,\n SendMessage,\n SendToConnectionFn,\n WebsocketListener,\n WebsocketServerError,\n WebsocketSubscriptionOptions,\n WebsocketSubscriptionStore,\n WebsocketTransportError\n} from './types';\n\n/**\n * Manages a single WebSocket URI endpoint with subscription lifecycle and message handling.\n *\n * Use for streaming data (voyage list, notifications). Provides a TanStack Store for\n * reactive updates. Multiple components share one instance via a unique key.\n *\n * ## Key Features\n *\n * - **Reactive Store**: TanStack Store updates when messages are received\n * - **pendingSubscription**: `true` from subscribe until first message (use for loading states)\n * - **Auto-subscribe**: Subscribes when the WebSocket connection opens\n * - **Hook Tracking**: Tracks components; unsubscribes when the last hook unmounts\n *\n * ## Edge Cases\n *\n * - **Multiple initiators**: Using the same key in multiple components emits a console warning;\n * multiple initiators can cause unexpected behavior.\n * - **Body change in subscribe-on-open**: When `options.body` changes, re-subscribes automatically.\n * - **enabled=false**: Unsubscribes and disconnects; re-enabling triggers subscribe.\n * - **reset**: Called by connection on URL change/reconnect; clears store state.\n *\n * ## Cleanup\n *\n * {@link reset} is called by WebsocketConnection on URL change or reconnection.\n * {@link unregisterHook} triggers removal after {@link INITIATOR_REMOVAL_DELAY_MS}.\n *\n * @template TData - The type of data received from the WebSocket\n * @template TBody - The type of message body sent\n *\n * @example\n * ```typescript\n * const api = new WebsocketSubscriptionApi<MyData, MyBody>({\n * url: 'wss://example.com',\n * uri: '/api/stream',\n * key: 'my-stream-key',\n * body: { filter: 'active' },\n * onMessage: (data) => console.log('Received:', data)\n * });\n *\n * const data = useSelector(api.store, (s) => s.message);\n * const isPending = useSelector(api.store, (s) => s.pendingSubscription);\n * api.sendMessage({ method: 'refresh', body: { force: true } });\n * ```\n *\n * @see {@link useWebsocketSubscription} - React hook\n * @see {@link WebsocketConnection} - Connection manager\n */\nexport class WebsocketSubscriptionApi<TData = unknown, TBody = unknown> implements WebsocketListener {\n private _options: WebsocketSubscriptionOptions<TData, TBody>;\n private _state: Store<WebsocketSubscriptionStore<TData>> = new Store<WebsocketSubscriptionStore<TData>>(\n createInitialWebsocketSubscriptionStore<TData>()\n );\n private _registeredHooks: Set<string> = new Set();\n private _disconnectTimeout: ReturnType<typeof setTimeout> | undefined;\n private _hookRemovalTimeout: ReturnType<typeof setTimeout> | undefined;\n private _sendToConnection: SendToConnectionFn | null = null;\n private _pendingMessages: SendMessage<string, string, TBody>[] = [];\n public readonly type = 'subscription';\n\n /**\n * Creates a new WebsocketSubscriptionApi.\n *\n * @param options - Configuration options (url, uri, key, callbacks, etc.)\n */\n constructor(options: WebsocketSubscriptionOptions<TData, TBody>) {\n this._options = { ...DEFAULT_URI_OPTIONS, ...options };\n }\n\n /** Unique key identifier for this WebSocket URI API. */\n public get key(): string {\n return this._options.key;\n }\n\n /** URI path for this WebSocket subscription. */\n public get uri(): string {\n return this._options.uri;\n }\n\n /** WebSocket URL for Datadog tracking. */\n public get url(): string {\n return this._options.url;\n }\n\n /** Configuration options for this WebSocket URI. */\n public get options(): WebsocketSubscriptionOptions<TData, TBody> {\n return this._options;\n }\n\n /**\n * Current data from the store.\n *\n * **Do not use in React components** — it does not trigger re-renders. Use\n * `useSelector(api.store, (s) => s.message)` for reactive updates.\n */\n public get data(): TData | undefined {\n return this._state.state.message;\n }\n\n /** TanStack store containing subscription state (message, subscribed, connected, pendingSubscription, etc.). */\n public get store(): Store<WebsocketSubscriptionStore<TData>> {\n return this._state;\n }\n\n /** Whether this WebSocket URI is enabled. */\n public get isEnabled(): boolean {\n return this._options.enabled ?? true;\n }\n\n /**\n * Updates the configuration options for this subscription.\n *\n * Handles lifecycle changes:\n * - **Body change** (subscribe-on-open): Re-subscribes with new body\n * - **Enabled: false → true**: Subscribes\n * - **Enabled: true → false**: Unsubscribes\n *\n * Uses deep equality to skip no-op updates.\n *\n * @param options - New options (merged with existing)\n */\n public set options(options: WebsocketSubscriptionOptions<TData, TBody>) {\n const updatedOptions: WebsocketSubscriptionOptions<TData, TBody> = {\n ...DEFAULT_URI_OPTIONS,\n ...this._options,\n ...options\n };\n\n if (deepEqual(this._options, updatedOptions)) return;\n\n const previousOptions = this._options;\n this._options = updatedOptions;\n\n this._handleSubscriptionUpdates(previousOptions, updatedOptions);\n this._handleUnsubscribeOnDisable(previousOptions, updatedOptions);\n }\n\n /**\n * Sets or clears the callback used to send messages through the parent WebSocket connection.\n *\n * When clearing, flushes pending messages and clears removal timeouts to avoid redundant cleanup.\n *\n * @param callback - The send function, or null to disconnect\n */\n public setSendToConnection = (callback: SendToConnectionFn | null): void => {\n this._sendToConnection = callback;\n\n if (callback) {\n this._flushPendingMessages(callback);\n } else {\n this._clearPendingTimeouts();\n this._pendingMessages = [];\n }\n };\n\n /**\n * Registers a hook (component) that is using this subscription.\n *\n * Clears pending removal/disconnect timeouts and tracks the hook ID.\n * Emits a console warning if more than one hook is registered (multiple initiators).\n *\n * @param id - Unique identifier for the registering hook\n */\n public registerHook = (id: string): void => {\n this._clearPendingTimeouts();\n this._registeredHooks.add(id);\n if (this._registeredHooks.size > 1) {\n console.warn(`the uri ${this.uri} has more than one initiator, multiple initiators could cause unexpected behavior`);\n }\n };\n\n /**\n * Unregisters a hook from this subscription.\n *\n * After {@link INITIATOR_REMOVAL_DELAY_MS}, if no hooks remain, unsubscribes\n * and invokes the cleanup callback. The delay prevents rapid subscribe/unsubscribe\n * during React re-renders.\n *\n * @param id - The hook ID to unregister\n * @param onRemove - Callback invoked when the last hook is removed (after delay)\n */\n public unregisterHook = (id: string, onRemove: () => void): void => {\n this._registeredHooks.delete(id);\n this._scheduleHookRemoval(onRemove);\n };\n\n /**\n * Disconnects this subscription from the parent WebSocket connection.\n *\n * Immediately unsubscribes, then after {@link INITIATOR_REMOVAL_DELAY_MS} invokes\n * the cleanup callback. Called when the hook is disabled (`enabled=false`).\n *\n * @param onRemoveFromSocket - Callback invoked after delay to remove from connection\n */\n public disconnect = (onRemoveFromSocket: () => void): void => {\n this._clearPendingTimeouts();\n this.unsubscribe();\n this._disconnectTimeout = setTimeout(() => {\n this._disconnectTimeout = undefined;\n this._state.setState((prev) => ({ ...prev, connected: false, subscribed: false, pendingSubscription: false }));\n onRemoveFromSocket();\n }, INITIATOR_REMOVAL_DELAY_MS);\n };\n\n /**\n * Resets this subscription to its initial state.\n *\n * Clears connection/subscription state, resets store data, and cancels pending timeouts.\n * Only runs when currently connected. Called by WebsocketConnection on URL change\n * or reconnection.\n */\n public reset = (): void => {\n if (!this._state.state.connected) return;\n\n this._state.setState((prev) => ({\n ...prev,\n connected: false,\n subscribed: false,\n pendingSubscription: false,\n message: undefined\n }));\n this._clearPendingTimeouts();\n };\n\n /**\n * Sends a custom message through the WebSocket for this URI.\n *\n * Automatically appends the URI and method. Queues if connection not yet set.\n *\n * @param message - The message to send (uri and method may be overridden)\n */\n public sendMessage = (message: SendMessage<string, string, TBody>): void => {\n if (!this.isEnabled) return;\n\n this._clearPendingTimeouts();\n const messageWithUri = { ...message, uri: this.uri, method: message.method ?? this._options.method ?? 'post' };\n this._sendOrQueue(messageWithUri);\n };\n\n /**\n * Subscribes to this WebSocket URI to start receiving messages.\n *\n * Only subscribes when enabled. Sends a 'subscribe' message through the parent connection.\n *\n * @param body - Optional body to send with the subscription\n */\n public subscribe = (body?: TBody): void => {\n if (!this.isEnabled) return;\n\n this._clearPendingTimeouts();\n this._state.setState((prev) => ({\n ...prev,\n subscribed: true,\n pendingSubscription: true,\n subscribedAt: Date.now()\n }));\n this._sendOrQueue({ body, uri: this.uri, method: 'subscribe' });\n this._options.onSubscribe?.({ uri: this.uri, body: this._options.body, uriApi: this });\n };\n\n /**\n * Unsubscribes from this WebSocket URI to stop receiving messages.\n *\n * Only unsubscribes when currently subscribed.\n */\n public unsubscribe = (): void => {\n if (!this._state.state.subscribed) return;\n this._state.setState((prev) => ({ ...prev, subscribed: false, pendingSubscription: false, message: undefined }));\n\n this._sendOrQueue({ uri: this.uri, method: 'unsubscribe' });\n };\n\n /**\n * Called by WebsocketConnection when the WebSocket connection opens.\n *\n * Subscribes with the configured body.\n */\n public onOpen = (): void => {\n if (this._state.state.connected) return;\n this._state.setState((prev) => ({ ...prev, connected: true }));\n this.subscribe(this._options.body);\n };\n\n /**\n * Called by WebsocketConnection when a message is received for this URI.\n *\n * @param data - The message data\n */\n public onMessage = (data: TData): void => {\n this._state.setState((prev) => ({\n ...prev,\n message: data,\n pendingSubscription: false,\n receivedAt: Date.now()\n }));\n this._options.onMessage?.({ data, uriApi: this });\n };\n\n /** @inheritdoc */\n public onError = (error: WebsocketTransportError): void => {\n this._state.setState((prev) => ({ ...prev, pendingSubscription: false }));\n this._options.onError?.(error);\n };\n\n /**\n * Called by WebsocketConnection when a server error message is received.\n *\n * @param error - Server error with parsed message body\n */\n public onMessageError = (error: WebsocketServerError<TBody>): void => {\n this._state.setState((prev) => ({ ...prev, pendingSubscription: false }));\n this._options.onMessageError?.(error);\n };\n\n /**\n * Called by WebsocketConnection when the WebSocket connection closes.\n *\n * Resets subscription state to ensure a fresh subscription on reconnect.\n */\n public onClose = (event: CloseEvent): void => {\n this._state.setState((prev) => ({ ...prev, subscribed: false, pendingSubscription: false }));\n this._options.onClose?.(event);\n };\n\n private _clearPendingTimeouts(): void {\n if (this._disconnectTimeout !== undefined) {\n clearTimeout(this._disconnectTimeout);\n this._disconnectTimeout = undefined;\n }\n if (this._hookRemovalTimeout !== undefined) {\n clearTimeout(this._hookRemovalTimeout);\n this._hookRemovalTimeout = undefined;\n }\n }\n\n private _scheduleHookRemoval(onRemove: () => void): void {\n this._clearPendingTimeouts();\n this._hookRemovalTimeout = setTimeout(() => {\n this._hookRemovalTimeout = undefined;\n if (this._registeredHooks.size === 0) {\n this._state.setState((prev) => ({ ...prev, connected: false }));\n this.unsubscribe();\n onRemove();\n }\n }, INITIATOR_REMOVAL_DELAY_MS);\n }\n\n private _flushPendingMessages(callback: SendToConnectionFn): void {\n if (this._pendingMessages.length === 0) return;\n this._pendingMessages.forEach((msg) => callback({ ...msg, uri: this.uri, method: msg.method ?? this._options.method ?? 'post' }));\n this._pendingMessages = [];\n }\n\n private _sendOrQueue(message: SendMessage<string, string, TBody>): void {\n if (this._sendToConnection) {\n this._sendToConnection(message);\n } else {\n this._pendingMessages.push(message);\n }\n }\n\n private _handleSubscriptionUpdates(\n previousOptions: WebsocketSubscriptionOptions<TData, TBody>,\n updatedOptions: WebsocketSubscriptionOptions<TData, TBody>\n ): void {\n const bodyChanged = !deepEqual(previousOptions.body, updatedOptions.body);\n const becameEnabled = !previousOptions.enabled && updatedOptions.enabled;\n\n if (bodyChanged || becameEnabled) {\n this.subscribe(updatedOptions.body);\n }\n }\n\n private _handleUnsubscribeOnDisable(\n previousOptions: WebsocketSubscriptionOptions<TData, TBody>,\n updatedOptions: WebsocketSubscriptionOptions<TData, TBody>\n ): void {\n const isDisabled = !updatedOptions.enabled;\n const wasEnabled = previousOptions.enabled;\n\n if (isDisabled && wasEnabled && this._state.state.subscribed) {\n this.unsubscribe();\n }\n }\n}\n","/**\n * @fileoverview Helper functions for WebSocket connection and listener management.\n *\n * These functions implement the singleton patterns for connections (per URL key)\n * and listeners (per API key) via {@link WebsocketClient}. Used by the React hooks\n * in {@link WebsocketHook}.\n *\n * @module websocketClient.helpers\n */\n\nimport { WebsocketListener, WebsocketMessageOptions, WebsocketSubscriptionOptions } from './types';\nimport { WebsocketClient } from './WebsocketClient';\nimport { WebsocketMessageApi } from './WebsocketMessageApi';\nimport { WebsocketSubscriptionApi } from './WebsocketSubscriptionApi';\n\n/**\n * Creates a WebSocket subscription API or returns the existing one for the given key.\n *\n * Singleton per key: multiple components with the same key share one instance.\n * The instance is stored in {@link WebsocketClient} and registered with a connection\n * via {@link WebsocketConnection.addListener}.\n *\n * @param client - The {@link WebsocketClient} instance\n * @template TData - The type of data received from the WebSocket\n * @template TBody - The type of message body sent to the WebSocket\n * @param key - Unique key for this subscription API\n * @param options - Configuration options\n * @returns Existing or newly created {@link WebsocketSubscriptionApi}\n *\n * @see {@link WebsocketClient.getListener} - Check for existing instance\n */\nexport const createWebsocketSubscriptionApi = <TData = unknown, TBody = unknown>(\n client: WebsocketClient,\n key: string,\n options: WebsocketSubscriptionOptions<TData, any>\n): WebsocketSubscriptionApi<TData, any> => {\n const listener = client.getListener<TData, TBody>(key, 'subscription');\n if (listener) {\n return listener;\n }\n const uriApi = new WebsocketSubscriptionApi(options);\n client.addListener(uriApi);\n return uriApi;\n};\n\n/**\n * Creates a WebSocket Message API or returns the existing one for the given key.\n *\n * Singleton per key: multiple components with the same key share one instance.\n *\n * @param client - The {@link WebsocketClient} instance\n * @param key - Unique key for this Message API\n * @param options - Configuration options\n * @returns Existing or newly created {@link WebsocketMessageApi}\n */\nexport const createWebsocketMessageApi = (client: WebsocketClient, key: string, options: WebsocketMessageOptions): WebsocketMessageApi => {\n const listener = client.getListener(key, 'message');\n if (listener) {\n return listener;\n }\n const messageApi = new WebsocketMessageApi(options, client);\n client.addListener(messageApi);\n return messageApi;\n};\n\n/**\n * Removes a WebSocket listener from its connection and from the client.\n *\n * Calls {@link WebsocketConnection.removeListener} and removes the listener from\n * {@link WebsocketClient}. Call when the last hook unmounts or when the listener\n * is disabled (via `enabled=false`).\n *\n * @param client - The {@link WebsocketClient} instance\n * @param listener - The listener (subscription or message API) to remove\n */\nexport const removeWebsocketListenerFromConnection = (client: WebsocketClient, listener: WebsocketListener): void => {\n const connection = client.getConnection(listener.url);\n connection?.removeListener(listener);\n client.removeListener(listener);\n};\n","import { useStore } from \"@tanstack/react-store\";\nimport { Store } from \"@tanstack/store\";\nimport { deepEqual } from \"fast-equals\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { WebsocketMessageApi } from \"./WebsocketMessageApi\";\nimport { useWebsocketClient } from \"./WebsocketProvider\";\nimport { WebsocketSubscriptionApi } from \"./WebsocketSubscriptionApi\";\nimport {\n createInitialWebsocketSubscriptionStore,\n WebsocketListener,\n WebsocketMessageApiPublic,\n WebsocketMessageOptions,\n WebsocketSubscriptionApiPublic,\n WebsocketSubscriptionOptions,\n WebsocketSubscriptionStore,\n} from \"./types\";\nimport { useIsomorphicLayoutEffect } from \"./utils\";\nimport {\n createWebsocketMessageApi,\n createWebsocketSubscriptionApi,\n removeWebsocketListenerFromConnection,\n} from \"./websocketClient.helpers\";\n\n/**\n * WebSocket React hooks for the shared connection architecture.\n *\n * This module provides hooks that integrate with {@link WebsocketConnection}.\n * from a path and optional secret (for region-based auth from `@mono-fleet/iam-provider`).\n * Call `useWebsocketConnectionConfig` and `useReconnectWebsocketConnections` from\n * `@mono-fleet/common-components` at app root for logging and reconnection on region change.\n *\n * ## Hook Overview\n *\n * | Hook | Use Case |\n * |------|----------|\n * | `useWebsocketSubscription` | Subscribe to a URI and receive streaming data via a reactive store |\n * | `useWebsocketMessage` | Send request/response messages to any URI (no subscription) |\n * | `useWebsocketSubscriptionByKey` | Access the store of a subscription created elsewhere (e.g. parent) |\n *\n * ## Choosing the Right Hook\n *\n * - **Streaming data** (voyage list, notifications): `useWebsocketSubscription`\n * - **One-off commands** (validate, modify, mark read): `useWebsocketMessage`\n * - **Child needs parent's subscription data**: `useWebsocketSubscriptionByKey` with same `key`\n *\n * ## Edge Cases\n *\n * - **Same key, multiple components**: Subscription and Message APIs are singletons per key.\n * Multiple hooks with the same key share one instance; `useWebsocketSubscriptionByKey` returns\n * a fallback store if the subscription does not exist yet (parent not mounted).\n * - **Options object identity**: Options are deep-compared; avoid passing new object literals\n * in dependency arrays to prevent unnecessary effect re-runs.\n * - **enabled=false**: Disconnects the listener and removes it from the connection after a delay.\n *\n * @module WebsocketHook\n */\n\n/**\n * Returns a referentially stable version of `value` that only updates when its\n * content changes according to deep equality.\n *\n * Prevents effect re-runs when dependency arrays contain object literals that\n * are structurally identical across renders (e.g. `{ uri: '/api', body: {} }`).\n *\n * @param value - The value to memoize (object or array)\n * @returns A referentially stable reference; updates only when deep equality changes\n *\n * @internal\n */\nfunction useDeepCompareMemoize<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!deepEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\n/**\n * Internal interface for listeners that support hook lifecycle management.\n *\n * Extends {@link WebsocketListener} with methods for registering/unregistering\n * hook instances and disconnecting. Both {@link WebsocketSubscriptionApi} and\n * {@link WebsocketMessageApi} implement this interface, enabling the shared\n * {@link useWebsocketLifecycle} hook.\n *\n * @internal\n */\ninterface HookableListener extends WebsocketListener {\n registerHook(id: string): void;\n unregisterHook(id: string, onRemove: () => void): void;\n disconnect(onRemoveFromSocket: () => void): void;\n}\n\n/**\n * Shared hook that manages connection registration, URL replacement, and hook\n * lifecycle tracking for both subscription and message listeners.\n *\n * Extracted from `useWebsocketCore` and `useWebsocketMessage` to eliminate\n * duplicated effect logic.\n *\n * @param listener - The listener instance (subscription or message API)\n * @param url - The WebSocket URL used for connection lookup\n * @param enabled - When `false`, disconnects the listener; when `true` or `undefined`, registers it\n *\n * @internal\n */\nfunction useWebsocketLifecycle(\n listener: HookableListener,\n url: string,\n enabled: boolean | undefined\n): void {\n const id = useId();\n const client = useWebsocketClient();\n\n useIsomorphicLayoutEffect(() => {\n if (enabled !== false) {\n const connection = client.addConnection(listener.url, url);\n connection.addListener(listener);\n } else {\n listener.disconnect(() =>\n removeWebsocketListenerFromConnection(client, listener)\n );\n }\n }, [enabled, listener, client]);\n\n useIsomorphicLayoutEffect(() => {\n const connection = client.getConnection(url);\n connection?.replaceUrl(url);\n }, [url, client]);\n\n useEffect(() => {\n const initiatorId = id;\n if (enabled !== false) {\n listener.registerHook(id);\n }\n return () => {\n listener.unregisterHook(initiatorId, () =>\n removeWebsocketListenerFromConnection(client, listener)\n );\n };\n }, [client, enabled, id, listener]);\n}\n\n/**\n * React hook that manages a WebSocket subscription for a specific Subscription endpoint.\n *\n * This hook provides a reactive interface to the WebSocket connection system. It establishes\n * the connection architecture by linking three key components:\n *\n * ## Architecture Overview\n *\n * The hook integrates with a two-layer class architecture:\n *\n * 1. **WebsocketConnection** (singleton per URL)\n * - Manages the underlying WebSocket connection lifecycle\n * - Handles reconnection, heartbeat, and connection state\n * - Routes incoming messages to the appropriate Subscription handlers\n * - Retrieved via `WebsocketClient.addConnection()` which ensures only one\n * connection exists per WebSocket URL\n *\n * 2. **WebsocketSubscriptionApi** (one per subscription per connection)\n * - Manages subscription lifecycle for a specific URI endpoint\n * - Provides a TanStack Store for reactive data updates\n * - Handles subscribe/unsubscribe operations\n * - Registered via `connection.addListener(subscriptionApi)` which routes messages by URI\n *\n * ## How the Hook Links to Classes\n *\n * ```\n * useWebsocketSubscription\n * │\n * ├─→ createWebsocketSubscriptionApi(key, options)\n * │ └─→ Returns/creates WebsocketSubscriptionApi singleton (per key)\n * │ ├─→ Manages subscription for this specific URI\n * │ ├─→ Provides reactive store for data updates\n * │ └─→ Handles subscribe/unsubscribe lifecycle\n * │\n * └─→ client.addConnection(url, url)\n * └─→ Returns/creates WebsocketConnection singleton (per URL)\n * ├─→ Manages WebSocket connection (connect, reconnect, heartbeat)\n * ├─→ Routes messages to registered listeners\n * └─→ connection.addListener(subscriptionApi) registers the listener\n * ```\n *\n * ## Lifecycle Management\n *\n * - **URI API**: Created once via `useState` initializer (singleton per key via\n * `createWebsocketUriApi`). Multiple components can share the same URI API,\n * tracked via registered hook IDs.\n *\n * - **Connection**: Found or created in a `useIsomorphicLayoutEffect` that watches\n * `enabled`. The connection is a singleton per key, shared across all hooks using\n * the same base URL path.\n *\n * - **Options Updates**: `useIsomorphicLayoutEffect` synchronously updates URI API options\n * via the `options` setter when they change (deep-compared via `useDeepCompareMemoize`),\n * preventing rendering with stale configuration.\n *\n * - **URL Replacement**: A separate `useIsomorphicLayoutEffect` watches `wsUrl` and calls\n * `connection.replaceUrl()` when the URL changes (e.g. due to auth context changes).\n *\n * - **Cleanup**: `useEffect` registers this hook instance as a hook and provides cleanup\n * that removes it. When the last hook is removed, the URI API automatically unsubscribes\n * and is removed from the connection.\n *\n * @template TData - The type of data received from the WebSocket for this URI\n * @template TBody - The type of message body sent to the WebSocket for this URI\n *\n * @param options - Configuration options including:\n * - `url`: The WebSocket URL\n * - `uri`: The specific URI endpoint for this subscription\n * - `key`: Unique identifier for this subscription (used to retrieve it elsewhere via `useWebsocketSubscriptionByKey`)\n * - `enabled`: Whether this subscription is enabled (default: true)\n * - `body`: Optional payload for subscription or initial message\n * - `onMessage`, `onSubscribe`, `onError`, `onMessageError`, `onClose`: Optional callbacks\n * @returns The {@link WebsocketSubscriptionApiPublic} instance. Use `useSelector(api.store, (s) => s.message)` to read data reactively.\n *\n * @example\n * ```typescript\n * // Create subscription and read data via TanStack Store\n * const voyageApi = useWebsocketSubscription<Voyage[], VoyageFilters>({\n * key: 'voyages-list',\n * url: '/api',\n * uri: '/api/voyages',\n * body: { status: 'active' }\n * });\n * const voyages = useSelector(voyageApi.store, (s) => s.message);\n *\n * // Or use useWebsocketSubscriptionByKey in children to access the same store\n * const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>('voyages-list');\n * const voyages = useSelector(voyagesStore, (s) => s.message);\n * ```\n *\n * ## Edge Cases\n *\n * - **Multiple initiators**: Using the same `key` in multiple components registers multiple hooks.\n * A console warning is emitted; multiple initiators can cause unexpected behavior.\n * - **pendingSubscription**: Use `store.pendingSubscription` for loading states — it is `true`\n * from subscribe until the first message is received.\n *\n * @see {@link useWebsocketSubscriptionByKey} - Access the store when the subscription is created in a parent\n * @see {@link WebsocketSubscriptionStore} - Store shape: `{ message, subscribed, connected, ... }`\n */\n\n// Implementation\nexport function useWebsocketSubscription<TData = unknown, TBody = unknown>(\n options: WebsocketSubscriptionOptions<TData, TBody>\n): WebsocketSubscriptionApiPublic<TData, TBody> {\n const client = useWebsocketClient();\n const [subscriptionApi] = useState<WebsocketSubscriptionApi<TData, TBody>>(\n () => createWebsocketSubscriptionApi(client, options.key, options)\n );\n\n useWebsocketLifecycle(subscriptionApi, options.url, options.enabled);\n\n const stableOptions = useDeepCompareMemoize(options);\n\n useIsomorphicLayoutEffect(() => {\n subscriptionApi.options = stableOptions;\n }, [stableOptions, subscriptionApi]);\n\n return subscriptionApi;\n}\n\n/**\n * React hook that returns the store of a WebSocket subscription by key.\n *\n * Use when a parent creates the subscription via `useWebsocketSubscription` and\n * children need to read the data. The `key` must match the one used when creating\n * the subscription.\n *\n * **Edge case**: Returns a fallback store (initial empty state) if the subscription\n * does not exist yet (e.g. parent hasn't mounted). This avoids null checks but means\n * children may briefly see empty data before the parent mounts and subscribes.\n *\n * @template TData - The type of data in the store's `message` field\n * @param key - Unique key (must match `useWebsocketSubscription` options.key)\n * @returns TanStack {@link Store} with shape {@link WebsocketSubscriptionStore}\n *\n * @example\n * ```typescript\n * // Parent creates subscription\n * useWebsocketSubscription<Voyage[]>({ key: 'voyages-list', url: '...', uri: '...' });\n *\n * // Child reads store by key\n * const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>('voyages-list');\n * const voyages = useSelector(voyagesStore, (s) => s.message);\n * ```\n *\n * @see {@link WebsocketSubscriptionStore} - Store shape\n */\nexport const useWebsocketSubscriptionByKey = <TData = unknown>(key: string) => {\n const client = useWebsocketClient();\n const subscription = client.getListener<TData, any>(key, \"subscription\");\n\n const [fallbackStore] = useState<Store<WebsocketSubscriptionStore<TData>>>(\n () =>\n new Store<WebsocketSubscriptionStore<TData>>(\n createInitialWebsocketSubscriptionStore<TData>()\n )\n );\n return subscription?.store ?? fallbackStore;\n};\n\n/**\n * React hook that manages a WebSocket Message API for request/response style messaging.\n *\n * Use this for one-off commands (validate, modify, mark read) rather than streaming\n * subscriptions. Send to any URI; optionally await a response.\n *\n * ## Key Features\n *\n * - **Request/Response**: `sendMessage(uri, method, body?, options?)` returns a Promise that resolves with the response\n * - **Fire-and-forget**: `sendMessageNoWait(uri, method, body?)` for commands that don't need a response\n * - **Any URI**: Not bound to a single URI like subscription APIs\n * - **Shared Instance**: Multiple components with the same `key` share the same Message API\n * - **Automatic Cleanup**: Removes from connection when the last hook unmounts\n *\n * @template TData - The type of data received in the response\n * @template TBody - The type of message body sent to the WebSocket\n *\n * @param options - Configuration options including:\n * - `url`: The WebSocket URL\n * - `key`: Unique identifier (components with same key share the API)\n * - `enabled`: Whether this API is enabled (default: true)\n * - `responseTimeoutMs`: Default timeout for `sendMessage` (default: 10000)\n * - `onError`, `onMessageError`, `onClose`: Optional callbacks\n * @returns {@link WebsocketMessageApiPublic} with `sendMessage`, `sendMessageNoWait`, `reset`, `url`, `key`, `isEnabled`\n *\n * @example\n * ```typescript\n * const api = useWebsocketMessage<ModifyVoyageUim, ModifyVoyageUim>({\n * key: 'voyages/modify',\n * url: '/api',\n * responseTimeoutMs: 10000\n * });\n *\n * // Await response (full form: uri, method, body?, options?)\n * const result = await api.sendMessage('voyages/modify/validate', 'post', formValues);\n *\n * // Fire-and-forget\n * api.sendMessageNoWait(`notifications/${id}/read`, 'post');\n * ```\n *\n * ## Edge Cases\n *\n * - **Overwrite**: Sending to the same URI while a request is pending cancels the previous\n * request (rejects with \"WebSocket request overwritten for URI\").\n * - **Disabled**: When `enabled=false`, `sendMessage` rejects; `sendMessageNoWait` is a no-op.\n * - **Connection closed**: Pending requests are rejected with \"WebSocket connection closed\".\n *\n * @see {@link WebsocketMessageApiPublic} - Public API surface\n */\nexport const useWebsocketMessage = (\n options: WebsocketMessageOptions\n): WebsocketMessageApiPublic => {\n const client = useWebsocketClient();\n const [messageApi] = useState<WebsocketMessageApi>(() =>\n createWebsocketMessageApi(client, options.key, options)\n );\n\n useWebsocketLifecycle(messageApi, options.url, options.enabled);\n\n return messageApi;\n};\n\n/**\n * Selects a value from a WebSocket subscription store with reactive updates.\n *\n * The store type is inferred from the first argument, so the selector\n * receives properly typed state (including `message: TData`) without explicit generics.\n *\n * Use this to subscribe to specific slices of subscription state and avoid re-renders when\n * unrelated fields change. The selector runs on every store update; return a primitive or\n * memoized value for optimal performance.\n *\n * @template TStore - The store state type (extends {@link WebsocketSubscriptionStore})\n * @template TResult - The type of the selected value\n * @param store - The TanStack Store from {@link WebsocketSubscriptionApi.store} or {@link useWebsocketSubscriptionByKey}\n * @param selector - Function that maps store state to the desired value. Receives typed state with `message`, `subscribed`, `pendingSubscription`, `connected`, etc.\n * @returns The selected value; triggers re-renders when the selected value changes (shallow comparison)\n *\n * @example\n * ```tsx\n * const voyageApi = useWebsocketSubscription<Voyage>({\n * key: 'voyages',\n * url: 'wss://api.example.com',\n * uri: '/api/voyages'\n * });\n *\n * // Select only message — re-renders when message changes, not when connected/subscribed change\n * const voyage = useSelector(voyageApi.store, (s) => s.message);\n *\n * // Select derived state\n * const isLoading = useSelector(voyageApi.store, (s) => s.pendingSubscription || !s.connected);\n *\n * // Select multiple fields (returns new object each time — consider useMemo if used as dependency)\n * const status = useSelector(voyageApi.store, (s) => ({\n * hasData: s.message !== undefined,\n * error: s.serverError ?? s.messageError\n * }));\n * ```\n *\n * @see {@link WebsocketSubscriptionStore} - Store shape and field descriptions\n * @see {@link useWebsocketSubscription} - Creates a subscription and returns the store\n * @see {@link useWebsocketSubscriptionByKey} - Access a subscription store by key\n */\nexport const useSelector = <\n TStore extends WebsocketSubscriptionStore<unknown>,\n TResult = unknown\n>(\n store: Store<TStore>,\n selector: (state: TStore) => TResult\n) => useStore(store, selector);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxtroost/use-websocket",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "A robust WebSocket connection management package for React applications with automatic reconnection, heartbeat monitoring, URI-based message routing, and React integration via TanStack Store.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -47,26 +47,25 @@
47
47
  "author": "Max Troost",
48
48
  "license": "MIT",
49
49
  "dependencies": {
50
- "@tanstack/react-store": "^0.9.2",
51
- "@tanstack/store": "^0.9.2",
52
- "fast-equals": "^5.0.0",
53
- "uuid": "^9.0.0",
54
- "usehooks-ts": "3.1.1"
50
+ "@tanstack/react-store": "0.9.2",
51
+ "@tanstack/store": "0.9.2",
52
+ "fast-equals": "6.0.0",
53
+ "uuid": "13.0.0"
55
54
  },
56
55
  "peerDependencies": {
57
56
  "react": ">=18.0.0",
58
57
  "react-dom": ">=18.0.0"
59
58
  },
60
59
  "devDependencies": {
61
- "@types/node": "^22.0.0",
62
- "@testing-library/jest-dom": "^6.6.0",
63
- "@testing-library/react": "^16.0.0",
64
- "@types/react": "^18.2.0",
65
- "@types/react-dom": "^18.2.0",
66
- "@types/uuid": "^9.0.0",
67
- "jsdom": "^25.0.0",
68
- "react": "^18.2.0",
69
- "react-dom": "^18.2.0",
60
+ "@testing-library/jest-dom": "6.9.1",
61
+ "@testing-library/react": "16.0.0",
62
+ "@types/node": "22.12.0",
63
+ "@types/react": "18.2.0",
64
+ "@types/react-dom": "18.2.0",
65
+ "@types/uuid": "9.0.0",
66
+ "jsdom": "25.0.0",
67
+ "react": "18.2.0",
68
+ "react-dom": "18.2.0",
70
69
  "typescript": "5.6.3",
71
70
  "vite": "8.0.0",
72
71
  "vite-plugin-dts": "4.5.4",