@maxtroost/use-websocket 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/index.d.ts +1122 -0
- package/dist/index.js +595 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
import { Store } from '@tanstack/react-store';
|
|
2
|
+
import { Store as Store_2 } from '@tanstack/store';
|
|
3
|
+
|
|
4
|
+
/** Creates the initial state for a WebsocketSubscriptionStore. */
|
|
5
|
+
export declare function createInitialWebsocketSubscriptionStore<TData = unknown>(): WebsocketSubscriptionStore<TData>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Structure of incoming WebSocket messages.
|
|
9
|
+
*
|
|
10
|
+
* Messages must have a URI for routing to the correct handler and can include
|
|
11
|
+
* an optional body with the actual message data.
|
|
12
|
+
*
|
|
13
|
+
* @template TBody - The type of the message body payload
|
|
14
|
+
*/
|
|
15
|
+
export declare interface IncomingWebsocketMessage<TBody = unknown> {
|
|
16
|
+
/** URI path that identifies which handler should process this message */
|
|
17
|
+
uri: string;
|
|
18
|
+
/** Optional message body/payload */
|
|
19
|
+
body?: TBody;
|
|
20
|
+
/** HTTP-like method for the message (e.g., 'subscribe', 'unsubscribe', 'post') */
|
|
21
|
+
method?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* WebSocket connection ready states.
|
|
26
|
+
*
|
|
27
|
+
* Values match the WebSocket API readyState constants, with an additional
|
|
28
|
+
* UNINSTANTIATED state for connections that haven't been created yet.
|
|
29
|
+
*
|
|
30
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
|
31
|
+
*/
|
|
32
|
+
declare enum ReadyState_2 {
|
|
33
|
+
/** Connection has not been instantiated yet */
|
|
34
|
+
UNINSTANTIATED = -1,
|
|
35
|
+
/** Connection is being established */
|
|
36
|
+
CONNECTING = 0,
|
|
37
|
+
/** Connection is open and ready to communicate */
|
|
38
|
+
OPEN = 1,
|
|
39
|
+
/** Connection is in the process of closing */
|
|
40
|
+
CLOSING = 2,
|
|
41
|
+
/** Connection is closed or couldn't be opened */
|
|
42
|
+
CLOSED = 3
|
|
43
|
+
}
|
|
44
|
+
export { ReadyState_2 as ReadyState }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Structure for outgoing WebSocket messages.
|
|
48
|
+
*
|
|
49
|
+
* Messages are sent with a method (HTTP-like), URI for routing, optional body,
|
|
50
|
+
* and an automatically generated correlation ID for tracking.
|
|
51
|
+
*
|
|
52
|
+
* @template TMethod - The type of the HTTP method (e.g., 'subscribe', 'unsubscribe', 'post')
|
|
53
|
+
* @template TUri - The type of the URI string
|
|
54
|
+
* @template TBody - The type of the message body payload
|
|
55
|
+
*/
|
|
56
|
+
export declare interface SendMessage<TMethod = string, TUri = string, TBody = unknown> {
|
|
57
|
+
/** HTTP-like method for the message (e.g., 'subscribe', 'unsubscribe', 'post') */
|
|
58
|
+
method?: TMethod;
|
|
59
|
+
/** URI path for routing the message to the correct handler */
|
|
60
|
+
uri?: TUri;
|
|
61
|
+
/** Optional message body/payload */
|
|
62
|
+
body?: TBody;
|
|
63
|
+
/** Correlation ID for tracking messages (automatically generated) */
|
|
64
|
+
correlation?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Options for WebsocketMessageApi.sendMessage.
|
|
69
|
+
*/
|
|
70
|
+
export declare interface SendMessageOptions {
|
|
71
|
+
/** Timeout in ms when waiting for a response. Overrides the default from options. */
|
|
72
|
+
timeout?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Callback function for sending messages from a {@link WebsocketSubscriptionApi} to its parent {@link WebsocketConnection}.
|
|
77
|
+
*
|
|
78
|
+
* This callback is injected by the connection when a URI API is registered,
|
|
79
|
+
* replacing the previous EventTarget/CustomEvent indirection with a direct,
|
|
80
|
+
* type-safe function call.
|
|
81
|
+
*/
|
|
82
|
+
export declare type SendToConnectionFn = (message: SendMessage<string, string, unknown>) => void;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* React hook that manages a WebSocket Message API for request/response style messaging.
|
|
86
|
+
*
|
|
87
|
+
* Use this for one-off commands (validate, modify, mark read) rather than streaming
|
|
88
|
+
* subscriptions. Send to any URI; optionally await a response.
|
|
89
|
+
*
|
|
90
|
+
* ## Key Features
|
|
91
|
+
*
|
|
92
|
+
* - **Request/Response**: `sendMessage(uri, method, body?, options?)` returns a Promise that resolves with the response
|
|
93
|
+
* - **Fire-and-forget**: `sendMessageNoWait(uri, method, body?)` for commands that don't need a response
|
|
94
|
+
* - **Any URI**: Not bound to a single URI like subscription APIs
|
|
95
|
+
* - **Shared Instance**: Multiple components with the same `key` share the same Message API
|
|
96
|
+
* - **Automatic Cleanup**: Removes from connection when the last hook unmounts
|
|
97
|
+
*
|
|
98
|
+
* @template TData - The type of data received in the response
|
|
99
|
+
* @template TBody - The type of message body sent to the WebSocket
|
|
100
|
+
*
|
|
101
|
+
* @param options - Configuration options including:
|
|
102
|
+
* - `url`: The WebSocket URL
|
|
103
|
+
* - `key`: Unique identifier (components with same key share the API)
|
|
104
|
+
* - `enabled`: Whether this API is enabled (default: true)
|
|
105
|
+
* - `responseTimeoutMs`: Default timeout for `sendMessage` (default: 5000)
|
|
106
|
+
* - `onError`, `onMessageError`, `onClose`: Optional callbacks
|
|
107
|
+
* @returns {@link WebsocketMessageApiPublic} with `sendMessage`, `sendMessageNoWait`, `reset`, `url`, `key`, `isEnabled`
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const api = useWebsocketMessage<ModifyVoyageUim, ModifyVoyageUim>({
|
|
112
|
+
* key: 'voyages/modify',
|
|
113
|
+
* url: '/api',
|
|
114
|
+
* responseTimeoutMs: 5000
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* // Await response (full form: uri, method, body?, options?)
|
|
118
|
+
* const result = await api.sendMessage('voyages/modify/validate', 'post', formValues);
|
|
119
|
+
*
|
|
120
|
+
* // Fire-and-forget
|
|
121
|
+
* api.sendMessageNoWait(`notifications/${id}/read`, 'post');
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* ## Edge Cases
|
|
125
|
+
*
|
|
126
|
+
* - **Overwrite**: Sending to the same URI while a request is pending cancels the previous
|
|
127
|
+
* request (rejects with "WebSocket request overwritten for URI").
|
|
128
|
+
* - **Disabled**: When `enabled=false`, `sendMessage` rejects; `sendMessageNoWait` is a no-op.
|
|
129
|
+
* - **Connection closed**: Pending requests are rejected with "WebSocket connection closed".
|
|
130
|
+
*
|
|
131
|
+
* @see {@link WebsocketMessageApiPublic} - Public API surface
|
|
132
|
+
*/
|
|
133
|
+
export declare const useWebsocketMessage: (options: WebsocketMessageOptions) => WebsocketMessageApiPublic;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* React hook that manages a WebSocket subscription for a specific Subscription endpoint.
|
|
137
|
+
*
|
|
138
|
+
* This hook provides a reactive interface to the WebSocket connection system. It establishes
|
|
139
|
+
* the connection architecture by linking three key components:
|
|
140
|
+
*
|
|
141
|
+
* ## Architecture Overview
|
|
142
|
+
*
|
|
143
|
+
* The hook integrates with a two-layer class architecture:
|
|
144
|
+
*
|
|
145
|
+
* 1. **WebsocketConnection** (singleton per URL)
|
|
146
|
+
* - Manages the underlying WebSocket connection lifecycle
|
|
147
|
+
* - Handles reconnection, heartbeat, and connection state
|
|
148
|
+
* - Routes incoming messages to the appropriate Subscription handlers
|
|
149
|
+
* - Retrieved via `findOrCreateWebsocketConnection()` which ensures only one
|
|
150
|
+
* connection exists per WebSocket URL
|
|
151
|
+
*
|
|
152
|
+
* 2. **WebsocketSubscriptionApi** (one per subscription per connection)
|
|
153
|
+
* - Manages subscription lifecycle for a specific URI endpoint
|
|
154
|
+
* - Provides a TanStack Store for reactive data updates
|
|
155
|
+
* - Handles subscribe/unsubscribe operations
|
|
156
|
+
* - Registered via `connection.addListener(subscriptionApi)` which routes messages by URI
|
|
157
|
+
*
|
|
158
|
+
* ## How the Hook Links to Classes
|
|
159
|
+
*
|
|
160
|
+
* ```
|
|
161
|
+
* useWebsocketSubscription
|
|
162
|
+
* │
|
|
163
|
+
* ├─→ createWebsocketSubscriptionApi(key, options)
|
|
164
|
+
* │ └─→ Returns/creates WebsocketSubscriptionApi singleton (per key)
|
|
165
|
+
* │ ├─→ Manages subscription for this specific URI
|
|
166
|
+
* │ ├─→ Provides reactive store for data updates
|
|
167
|
+
* │ └─→ Handles subscribe/unsubscribe lifecycle
|
|
168
|
+
* │
|
|
169
|
+
* └─→ findOrCreateWebsocketConnection(url, url)
|
|
170
|
+
* └─→ Returns/creates WebsocketConnection singleton (per URL)
|
|
171
|
+
* ├─→ Manages WebSocket connection (connect, reconnect, heartbeat)
|
|
172
|
+
* ├─→ Routes messages to registered listeners
|
|
173
|
+
* └─→ connection.addListener(subscriptionApi) registers the listener
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* ## Lifecycle Management
|
|
177
|
+
*
|
|
178
|
+
* - **URI API**: Created once via `useState` initializer (singleton per key via
|
|
179
|
+
* `createWebsocketUriApi`). Multiple components can share the same URI API,
|
|
180
|
+
* tracked via registered hook IDs.
|
|
181
|
+
*
|
|
182
|
+
* - **Connection**: Found or created in a `useIsomorphicLayoutEffect` that watches
|
|
183
|
+
* `enabled`. The connection is a singleton per key, shared across all hooks using
|
|
184
|
+
* the same base URL path.
|
|
185
|
+
*
|
|
186
|
+
* - **Options Updates**: `useIsomorphicLayoutEffect` synchronously updates URI API options
|
|
187
|
+
* via the `options` setter when they change (deep-compared via `useDeepCompareMemoize`),
|
|
188
|
+
* preventing rendering with stale configuration.
|
|
189
|
+
*
|
|
190
|
+
* - **URL Replacement**: A separate `useIsomorphicLayoutEffect` watches `wsUrl` and calls
|
|
191
|
+
* `connection.replaceUrl()` when the URL changes (e.g. due to auth context changes).
|
|
192
|
+
*
|
|
193
|
+
* - **Cleanup**: `useEffect` registers this hook instance as a hook and provides cleanup
|
|
194
|
+
* that removes it. When the last hook is removed, the URI API automatically unsubscribes
|
|
195
|
+
* and is removed from the connection.
|
|
196
|
+
*
|
|
197
|
+
* @template TData - The type of data received from the WebSocket for this URI
|
|
198
|
+
* @template TBody - The type of message body sent to the WebSocket for this URI
|
|
199
|
+
*
|
|
200
|
+
* @param options - Configuration options including:
|
|
201
|
+
* - `url`: The WebSocket URL
|
|
202
|
+
* - `uri`: The specific URI endpoint for this subscription
|
|
203
|
+
* - `key`: Unique identifier for this subscription (used to retrieve it elsewhere via `useWebsocketSubscriptionByKey`)
|
|
204
|
+
* - `enabled`: Whether this subscription is enabled (default: true)
|
|
205
|
+
* - `body`: Optional payload for subscription or initial message
|
|
206
|
+
* - `onMessage`, `onSubscribe`, `onError`, `onMessageError`, `onClose`: Optional callbacks
|
|
207
|
+
* @returns The {@link WebsocketSubscriptionApiPublic} instance. Use `useStore(api.store, (s) => s.message)` to read data reactively.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* // Create subscription and read data via TanStack Store
|
|
212
|
+
* const voyageApi = useWebsocketSubscription<Voyage[], VoyageFilters>({
|
|
213
|
+
* key: 'voyages-list',
|
|
214
|
+
* url: '/api',
|
|
215
|
+
* uri: '/api/voyages',
|
|
216
|
+
* body: { status: 'active' }
|
|
217
|
+
* });
|
|
218
|
+
* const voyages = useStore(voyageApi.store, (s) => s.message);
|
|
219
|
+
*
|
|
220
|
+
* // Or use useWebsocketSubscriptionByKey in children to access the same store
|
|
221
|
+
* const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>('voyages-list');
|
|
222
|
+
* const voyages = useStore(voyagesStore, (s) => s.message);
|
|
223
|
+
* ```
|
|
224
|
+
*
|
|
225
|
+
* ## Edge Cases
|
|
226
|
+
*
|
|
227
|
+
* - **Multiple initiators**: Using the same `key` in multiple components registers multiple hooks.
|
|
228
|
+
* A console warning is emitted; multiple initiators can cause unexpected behavior.
|
|
229
|
+
* - **pendingSubscription**: Use `store.pendingSubscription` for loading states — it is `true`
|
|
230
|
+
* from subscribe until the first message is received.
|
|
231
|
+
*
|
|
232
|
+
* @see {@link useWebsocketSubscriptionByKey} - Access the store when the subscription is created in a parent
|
|
233
|
+
* @see {@link WebsocketSubscriptionStore} - Store shape: `{ message, subscribed, connected, ... }`
|
|
234
|
+
*/
|
|
235
|
+
export declare function useWebsocketSubscription<TData = unknown, TBody = unknown>(options: WebsocketSubscriptionOptions<TData, TBody>): WebsocketSubscriptionApiPublic<TData, TBody>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* React hook that returns the store of a WebSocket subscription by key.
|
|
239
|
+
*
|
|
240
|
+
* Use when a parent creates the subscription via `useWebsocketSubscription` and
|
|
241
|
+
* children need to read the data. The `key` must match the one used when creating
|
|
242
|
+
* the subscription.
|
|
243
|
+
*
|
|
244
|
+
* **Edge case**: Returns a fallback store (initial empty state) if the subscription
|
|
245
|
+
* does not exist yet (e.g. parent hasn't mounted). This avoids null checks but means
|
|
246
|
+
* children may briefly see empty data before the parent mounts and subscribes.
|
|
247
|
+
*
|
|
248
|
+
* @template TData - The type of data in the store's `message` field
|
|
249
|
+
* @param key - Unique key (must match `useWebsocketSubscription` options.key)
|
|
250
|
+
* @returns TanStack {@link Store} with shape {@link WebsocketSubscriptionStore}
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* // Parent creates subscription
|
|
255
|
+
* useWebsocketSubscription<Voyage[]>({ key: 'voyages-list', url: '...', uri: '...' });
|
|
256
|
+
*
|
|
257
|
+
* // Child reads store by key
|
|
258
|
+
* const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>('voyages-list');
|
|
259
|
+
* const voyages = useStore(voyagesStore, (s) => s.message);
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* @see {@link WebsocketSubscriptionStore} - Store shape
|
|
263
|
+
*/
|
|
264
|
+
export declare const useWebsocketSubscriptionByKey: <TData = unknown>(key: string) => Store_2<WebsocketSubscriptionStore<TData>>;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Manages a WebSocket connection with automatic reconnection, heartbeat monitoring, and URI-based message routing.
|
|
268
|
+
*
|
|
269
|
+
* This class provides:
|
|
270
|
+
* - Automatic reconnection with exponential backoff on connection loss
|
|
271
|
+
* - Heartbeat/ping mechanism to keep connections alive
|
|
272
|
+
* - Multiple URI API registration for routing messages to different handlers
|
|
273
|
+
* - Online/offline detection and handling
|
|
274
|
+
* - Custom logger support for monitoring (configure via {@link setCustomLogger}
|
|
275
|
+
* - User notifications for connection status
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```typescript
|
|
279
|
+
* const connection = new WebsocketConnection('ws://example.com/api');
|
|
280
|
+
* const uriApi = new WebsocketSubscriptionApi({
|
|
281
|
+
* key: 'messages',
|
|
282
|
+
* url: '/api',
|
|
283
|
+
* uri: '/messages',
|
|
284
|
+
* onMessage: ({ data }) => console.log('Received:', data),
|
|
285
|
+
* onError: (error) => console.log('Error:', error),
|
|
286
|
+
* onClose: (event) => console.log('Closed:', event)
|
|
287
|
+
* });
|
|
288
|
+
* connection.addListener(uriApi);
|
|
289
|
+
* ```
|
|
290
|
+
*
|
|
291
|
+
* @see {@link websocketConnectionsReconnect} - Reconnect all connections (e.g. on auth change)
|
|
292
|
+
* @see {@link setCustomLogger} - Configure logging and connection-failed callback
|
|
293
|
+
*/
|
|
294
|
+
export declare class WebsocketConnection {
|
|
295
|
+
/** Custom logger instance. Use {@link setCustomLogger} to configure. */
|
|
296
|
+
private static _logger;
|
|
297
|
+
/**
|
|
298
|
+
* Sets a custom logger for WebSocket connection events.
|
|
299
|
+
*
|
|
300
|
+
* @param logger - Logger implementation, or `undefined` to clear
|
|
301
|
+
*/
|
|
302
|
+
static setCustomLogger(logger: WebsocketLogger | undefined): void;
|
|
303
|
+
/** The underlying WebSocket instance */
|
|
304
|
+
private _socket?;
|
|
305
|
+
/** Map of all listeners (subscription and message APIs) keyed by their unique key */
|
|
306
|
+
private _listeners;
|
|
307
|
+
/** The WebSocket URL */
|
|
308
|
+
private _url;
|
|
309
|
+
/** Display name extracted from URL pathname for user notifications */
|
|
310
|
+
private _name;
|
|
311
|
+
/** Timeout for the next ping message */
|
|
312
|
+
private pingTimeOut;
|
|
313
|
+
/** Timeout for detecting missing pong after ping (dead-connection detection) */
|
|
314
|
+
private pongTimeOut;
|
|
315
|
+
/** Timeout for closing the connection when no URIs are registered */
|
|
316
|
+
private closeConnectionTimeOut;
|
|
317
|
+
/** Counter for reconnection attempts */
|
|
318
|
+
private reconnectTries;
|
|
319
|
+
/** Guard flag that prevents concurrent teardown-and-reconnect cycles (e.g. when both replaceUrl and reconnect fire in the same render). */
|
|
320
|
+
private _isReconnecting;
|
|
321
|
+
/** True when max retry attempts exceeded; stops automatic reconnection until manual retry. */
|
|
322
|
+
private _maxRetriesExceeded;
|
|
323
|
+
/**
|
|
324
|
+
* Queue of non-subscribe messages sent while the socket was not open.
|
|
325
|
+
* Flushed when the connection opens. Subscribe messages are NOT cached — they trigger connect only.
|
|
326
|
+
*/
|
|
327
|
+
private cachedMessages;
|
|
328
|
+
/**
|
|
329
|
+
* Creates a new WebSocket connection instance.
|
|
330
|
+
* Note: The connection is not established until a URI API is added.
|
|
331
|
+
*
|
|
332
|
+
* @param url - The WebSocket URL to connect to
|
|
333
|
+
*/
|
|
334
|
+
constructor(url: string);
|
|
335
|
+
/**
|
|
336
|
+
* Gets the current ready state of the WebSocket connection.
|
|
337
|
+
*
|
|
338
|
+
* @returns The WebSocket ready state (CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3) or undefined if no socket exists.
|
|
339
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState | WebSocket.readyState}
|
|
340
|
+
*/
|
|
341
|
+
get readyState(): number | undefined;
|
|
342
|
+
/**
|
|
343
|
+
* Gets the WebSocket URL for this connection.
|
|
344
|
+
*
|
|
345
|
+
* @returns The WebSocket URL string
|
|
346
|
+
*/
|
|
347
|
+
get url(): string;
|
|
348
|
+
/**
|
|
349
|
+
* Gets the underlying WebSocket instance.
|
|
350
|
+
*
|
|
351
|
+
* @returns The WebSocket instance if connected, or undefined if the connection hasn't been established yet or has been closed.
|
|
352
|
+
*/
|
|
353
|
+
getSocket: () => WebSocket | undefined;
|
|
354
|
+
/**
|
|
355
|
+
* Retrieves a registered subscription API by its unique key.
|
|
356
|
+
* Message APIs are not returned; use {@link getWebsocketMessageApiByKey} for those.
|
|
357
|
+
*
|
|
358
|
+
* @template TData - The type of data stored in the subscription store
|
|
359
|
+
* @param key - The unique key identifying the subscription (must match the key used in {@link addListener})
|
|
360
|
+
* @returns The {@link WebsocketSubscriptionApi} instance if found, or `undefined`
|
|
361
|
+
*
|
|
362
|
+
* @see {@link addListener} - Method to register a listener
|
|
363
|
+
* @see {@link getWebsocketUriApiByKey} - Global function to search across all connections
|
|
364
|
+
*/
|
|
365
|
+
getUriApiByKey: <TData = unknown>(key: string) => WebsocketSubscriptionApi<TData, any> | undefined;
|
|
366
|
+
/**
|
|
367
|
+
* Registers a listener (subscription or message API) with this connection.
|
|
368
|
+
*
|
|
369
|
+
* Initiates the WebSocket connection if not already connected. Sets up the send callback
|
|
370
|
+
* so the listener can transmit messages through this connection.
|
|
371
|
+
*
|
|
372
|
+
* If the socket is already open, immediately notifies subscription listeners via `onOpen`.
|
|
373
|
+
*
|
|
374
|
+
* @param listener - The {@link WebsocketListener} to register
|
|
375
|
+
* @returns The registered listener
|
|
376
|
+
*/
|
|
377
|
+
addListener: (listener: WebsocketListener) => WebsocketListener;
|
|
378
|
+
/**
|
|
379
|
+
* Unregisters a listener and schedules connection cleanup if no listeners remain.
|
|
380
|
+
*
|
|
381
|
+
* Disconnects the listener's send callback and removes it from the routing map.
|
|
382
|
+
* The WebSocket connection will be closed after {@link CONNECTION_CLEANUP_DELAY} if no other
|
|
383
|
+
* listeners are registered.
|
|
384
|
+
*
|
|
385
|
+
* @param listener - The listener instance to unregister
|
|
386
|
+
*/
|
|
387
|
+
removeListener: (listener: WebsocketListener) => void;
|
|
388
|
+
/** Schedules connection close after {@link CONNECTION_CLEANUP_DELAY} when no listeners remain. */
|
|
389
|
+
private scheduleConnectionCleanup;
|
|
390
|
+
/**
|
|
391
|
+
* Replaces the WebSocket URL and re-establishes the connection.
|
|
392
|
+
*
|
|
393
|
+
* Closes the current connection, resets all listeners, and reconnects using the new URL
|
|
394
|
+
* after a short delay (1 second) to allow cleanup to complete.
|
|
395
|
+
*
|
|
396
|
+
* @param newUrl - The new WebSocket URL to connect to
|
|
397
|
+
*/
|
|
398
|
+
replaceUrl: (newUrl: string) => Promise<void>;
|
|
399
|
+
/**
|
|
400
|
+
* Reconnects the WebSocket connection.
|
|
401
|
+
*
|
|
402
|
+
* Tears down the current connection by removing all event listeners, closing the socket,
|
|
403
|
+
* and resetting all registered URI APIs. After a short delay (1 second) to allow cleanup,
|
|
404
|
+
* re-establishes the connection. Typically triggered by {@link websocketConnectionsReconnect}
|
|
405
|
+
* when {@link useReconnectWebsocketConnections} (from @mono-fleet/common-components) detects
|
|
406
|
+
* the user's authentication context (region/role) change.
|
|
407
|
+
*
|
|
408
|
+
* Guarded by {@link _isReconnecting} in {@link teardownAndReconnect} — if a `replaceUrl`
|
|
409
|
+
* layout effect already started a reconnect cycle in the same render, this call is a no-op.
|
|
410
|
+
*/
|
|
411
|
+
reconnect: () => Promise<void>;
|
|
412
|
+
/**
|
|
413
|
+
* Resets the retry counter and re-establishes the connection.
|
|
414
|
+
*
|
|
415
|
+
* Used when the user manually retries after hitting {@link RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS}.
|
|
416
|
+
* Clears the max-retries-exceeded state and initiates a fresh connection attempt.
|
|
417
|
+
*/
|
|
418
|
+
resetRetriesAndReconnect: () => void;
|
|
419
|
+
/**
|
|
420
|
+
* Establishes the WebSocket connection if not already connecting or connected.
|
|
421
|
+
* Only creates a socket if at least one registered listener (subscription or message API) is enabled.
|
|
422
|
+
* Sets up all event listeners and logs the connection attempt via the custom logger if configured.
|
|
423
|
+
*/
|
|
424
|
+
private connect;
|
|
425
|
+
/**
|
|
426
|
+
* Tears down the current socket: clears all timers, removes all event listeners,
|
|
427
|
+
* closes the socket, and resets the socket reference.
|
|
428
|
+
*/
|
|
429
|
+
private teardownSocket;
|
|
430
|
+
/**
|
|
431
|
+
* Tears down the current connection, resets all listeners, waits for cleanup to complete,
|
|
432
|
+
* and re-establishes the connection. Shared by {@link replaceUrl} and {@link reconnect}.
|
|
433
|
+
*
|
|
434
|
+
* Guarded by {@link _isReconnecting} to prevent concurrent cycles. When
|
|
435
|
+
* `selectedRegionRole` changes, both the hook's `replaceUrl` layout effect and
|
|
436
|
+
* `useReconnectWebsocketConnections`'s reconnect effect may fire. Because layout effects run
|
|
437
|
+
* before regular effects, `replaceUrl` wins and updates the URL first; the reconnect call
|
|
438
|
+
* is safely skipped.
|
|
439
|
+
*/
|
|
440
|
+
private teardownAndReconnect;
|
|
441
|
+
/**
|
|
442
|
+
* Cleans up the WebSocket connection when no listeners are registered.
|
|
443
|
+
*/
|
|
444
|
+
private cleanupConnection;
|
|
445
|
+
/**
|
|
446
|
+
* Clears all active timers (ping heartbeat, pong timeout, and connection cleanup).
|
|
447
|
+
*/
|
|
448
|
+
private clearAllTimers;
|
|
449
|
+
/**
|
|
450
|
+
* Removes all event listeners from the WebSocket and window objects.
|
|
451
|
+
* Used during cleanup and reconnection processes.
|
|
452
|
+
*/
|
|
453
|
+
private removeListeners;
|
|
454
|
+
/**
|
|
455
|
+
* Attempts to reconnect the WebSocket connection with exponential backoff.
|
|
456
|
+
* Shows user notifications after the threshold number of attempts.
|
|
457
|
+
* Only attempts reconnection when the browser is online.
|
|
458
|
+
* When closeCode is 1013 (Try Again Later), waits an extra delay before reconnecting.
|
|
459
|
+
* Stops after {@link RECONNECTION_CONFIG.MAX_RETRY_ATTEMPTS} and shows a permanent error with a manual retry button.
|
|
460
|
+
*
|
|
461
|
+
* @param closeCode - Optional WebSocket close code; used to apply TRY_AGAIN_LATER delay when 1013
|
|
462
|
+
*/
|
|
463
|
+
private attemptReconnection;
|
|
464
|
+
/**
|
|
465
|
+
* Checks if the browser is offline and, if so, defers reconnection until it comes back online
|
|
466
|
+
* by registering a one-time 'online' event listener.
|
|
467
|
+
*
|
|
468
|
+
* @returns `true` if reconnection was deferred (browser is offline), `false` if browser is online
|
|
469
|
+
*/
|
|
470
|
+
private deferReconnectionUntilOnline;
|
|
471
|
+
/**
|
|
472
|
+
* Handles WebSocket close events.
|
|
473
|
+
*
|
|
474
|
+
* Implements automatic reconnection for any non-intentional close (anything other than
|
|
475
|
+
* 1000 Normal Closure). This includes 1001 Going Away, 1011 Internal Error, 1012 Service
|
|
476
|
+
* Restart, 1013 Try Again Later, 1006 Abnormal Closure, and other server-initiated codes.
|
|
477
|
+
* Reconnection only occurs when listeners are still registered. Shows user notifications
|
|
478
|
+
* after {@link RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD} failed attempts.
|
|
479
|
+
* Cleans up the connection if no listeners remain. Logs the close event via the custom logger if configured.
|
|
480
|
+
*
|
|
481
|
+
* @param event - The WebSocket close event containing code, reason, and whether the close was clean
|
|
482
|
+
*/
|
|
483
|
+
private handleClose;
|
|
484
|
+
/**
|
|
485
|
+
* Handles WebSocket open/connected events.
|
|
486
|
+
*
|
|
487
|
+
* Sets up offline detection, dismisses reconnection notifications, shows success message
|
|
488
|
+
* for recovered connections (only if {@link RECONNECTION_CONFIG.NOTIFICATION_THRESHOLD}
|
|
489
|
+
* was exceeded), resets reconnection counter, notifies all listeners, flushes cached
|
|
490
|
+
* messages, and initiates the heartbeat ping sequence.
|
|
491
|
+
*/
|
|
492
|
+
private handleOpen;
|
|
493
|
+
/**
|
|
494
|
+
* Handles incoming WebSocket messages.
|
|
495
|
+
*
|
|
496
|
+
* Routes messages to matching listeners: subscription APIs by URI, message APIs by pending request URI.
|
|
497
|
+
* Special handling for 'ping' messages to maintain heartbeat.
|
|
498
|
+
* Dispatches error-method messages to listener error handlers.
|
|
499
|
+
*
|
|
500
|
+
* @param event - The WebSocket message event containing JSON data
|
|
501
|
+
*/
|
|
502
|
+
private handleMessage;
|
|
503
|
+
/**
|
|
504
|
+
* Handles WebSocket error events.
|
|
505
|
+
* Logs the error via the custom logger if configured and notifies all registered listeners.
|
|
506
|
+
*
|
|
507
|
+
* @param event - The WebSocket error event
|
|
508
|
+
*/
|
|
509
|
+
private handleError;
|
|
510
|
+
/**
|
|
511
|
+
* Handles browser coming back online during offline detection.
|
|
512
|
+
* Removes the online listener and re-establishes the connection.
|
|
513
|
+
*/
|
|
514
|
+
private handleOnline;
|
|
515
|
+
/**
|
|
516
|
+
* Handles browser coming back online during reconnection attempts.
|
|
517
|
+
* Removes the online listener and resumes reconnection with a decremented counter
|
|
518
|
+
* to avoid adding extra wait time from being offline.
|
|
519
|
+
*/
|
|
520
|
+
private handleOnlineForReconnection;
|
|
521
|
+
/**
|
|
522
|
+
* Handles browser going offline.
|
|
523
|
+
*
|
|
524
|
+
* Notifies all listeners of the closure, tears down the socket, and sets up
|
|
525
|
+
* a listener to reconnect when the browser comes back online.
|
|
526
|
+
*/
|
|
527
|
+
private handleOffline;
|
|
528
|
+
/**
|
|
529
|
+
* Handles outgoing messages from listeners.
|
|
530
|
+
*
|
|
531
|
+
* - If socket is OPEN: serializes with correlation ID and sends immediately.
|
|
532
|
+
* - If socket is not open: subscribe messages trigger connect only; other messages are
|
|
533
|
+
* cached and sent when the connection opens.
|
|
534
|
+
*
|
|
535
|
+
* Passed to each listener via {@link WebsocketListener.setSendToConnection}.
|
|
536
|
+
*/
|
|
537
|
+
private handleSendMessage;
|
|
538
|
+
/**
|
|
539
|
+
* Sends a heartbeat ping message to keep the connection alive and detect disconnections.
|
|
540
|
+
* Sets a pong timeout; if no pong arrives within HEARTBEAT_CONFIG.PONG_TIMEOUT_MS,
|
|
541
|
+
* the connection is force-closed to trigger reconnection.
|
|
542
|
+
*/
|
|
543
|
+
private sendPing;
|
|
544
|
+
/**
|
|
545
|
+
* Clears the pong timeout (e.g. when a pong is received).
|
|
546
|
+
*/
|
|
547
|
+
private clearPongTimeout;
|
|
548
|
+
/**
|
|
549
|
+
* Schedules a timeout to detect missing pong. If no pong arrives within
|
|
550
|
+
* {@link HEARTBEAT_CONFIG.PONG_TIMEOUT_MS}, force-closes the socket to trigger reconnection.
|
|
551
|
+
*/
|
|
552
|
+
private schedulePongTimeout;
|
|
553
|
+
/**
|
|
554
|
+
* Schedules the next heartbeat ping after the configured interval (40 seconds).
|
|
555
|
+
* @see {@link getPingTime}
|
|
556
|
+
*/
|
|
557
|
+
private schedulePing;
|
|
558
|
+
/**
|
|
559
|
+
* Serializes a message with a unique correlation ID for WebSocket transmission.
|
|
560
|
+
* @param message - The message to serialize
|
|
561
|
+
* @returns JSON string for WebSocket send
|
|
562
|
+
*/
|
|
563
|
+
private serializeMessage;
|
|
564
|
+
/**
|
|
565
|
+
* Executes a callback for each listener that matches the given URI.
|
|
566
|
+
*
|
|
567
|
+
* - **Subscription listeners**: Match when `listener.uri === uri`
|
|
568
|
+
* - **Message listeners**: Match when `listener.hasWaitingUri(uri)` (pending request/response)
|
|
569
|
+
*
|
|
570
|
+
* A single message can be delivered to multiple listeners if both a subscription
|
|
571
|
+
* and a message API are waiting for the same URI.
|
|
572
|
+
*
|
|
573
|
+
* @param uri - The URI from the incoming message
|
|
574
|
+
* @param callback - Callback invoked for each matching listener
|
|
575
|
+
*/
|
|
576
|
+
private forEachMatchingListener;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export declare const websocketConnectionsReconnect: () => void;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Common interface for WebSocket listeners registered with {@link WebsocketConnection}.
|
|
583
|
+
*
|
|
584
|
+
* Both {@link WebsocketSubscriptionApi} and {@link WebsocketMessageApi} implement this interface,
|
|
585
|
+
* allowing the connection to treat them uniformly via {@link addListener} / {@link removeListener}.
|
|
586
|
+
*
|
|
587
|
+
* - **Subscription listeners**: Have `uri`, `onOpen`, `onMessage` — route by URI match
|
|
588
|
+
* - **Message listeners**: Have `hasWaitingUri`, `deliverMessage` — route by pending URI
|
|
589
|
+
*/
|
|
590
|
+
export declare interface WebsocketListener {
|
|
591
|
+
readonly key: string;
|
|
592
|
+
readonly url: string;
|
|
593
|
+
readonly isEnabled: boolean;
|
|
594
|
+
setSendToConnection(callback: SendToConnectionFn | null): void;
|
|
595
|
+
onError(error: WebsocketTransportError): void;
|
|
596
|
+
onMessageError(error: WebsocketServerError<unknown>): void;
|
|
597
|
+
onClose(event: CloseEvent): void;
|
|
598
|
+
reset(): void;
|
|
599
|
+
/** Subscription listeners: fixed URI for this endpoint */
|
|
600
|
+
readonly uri?: string;
|
|
601
|
+
/** Subscription listeners: called when connection opens */
|
|
602
|
+
onOpen?(): void;
|
|
603
|
+
/** Subscription listeners: called when a message is received for this URI */
|
|
604
|
+
onMessage?(data: unknown): void;
|
|
605
|
+
/** Message listeners: returns true if waiting for a response for the given URI */
|
|
606
|
+
hasWaitingUri?(uri: string): boolean;
|
|
607
|
+
/** Message listeners: delivers a response for a pending request */
|
|
608
|
+
deliverMessage?(uri: string, data: unknown): void;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Optional custom logger for WebSocket connection events.
|
|
613
|
+
* Set via {@link WebsocketConnection.setCustomLogger}.
|
|
614
|
+
*/
|
|
615
|
+
export declare interface WebsocketLogger {
|
|
616
|
+
/** Logs connection events (e.g. ws-connect, ws-close, ws-error, ws-reconnect) */
|
|
617
|
+
log?(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void;
|
|
618
|
+
/** Called when max retry attempts exceeded; use to trigger token refresh or other recovery */
|
|
619
|
+
connectionFailed?: (url: string, retries: number, subscriptions: number) => void;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Manages WebSocket request/response messaging without subscription.
|
|
624
|
+
*
|
|
625
|
+
* Use for one-off commands (validate, modify, mark read) rather than streaming.
|
|
626
|
+
* Send to any URI; optionally await a response. Tracks URIs only while waiting.
|
|
627
|
+
*
|
|
628
|
+
* ## Key Features
|
|
629
|
+
*
|
|
630
|
+
* - **Any URI**: Not bound to a single URI like {@link WebsocketSubscriptionApi}
|
|
631
|
+
* - **Request/Response**: `sendMessage` returns a Promise; optional per-call timeout
|
|
632
|
+
* - **Fire-and-forget**: `sendMessageNoWait` for commands that don't need a response
|
|
633
|
+
* - **No Subscription**: Use WebsocketSubscriptionApi for streaming data
|
|
634
|
+
*
|
|
635
|
+
* ## Edge Cases
|
|
636
|
+
*
|
|
637
|
+
* - **Overwrite**: Sending to the same URI while a request is pending cancels the previous
|
|
638
|
+
* request — the previous Promise rejects with "WebSocket request overwritten for URI".
|
|
639
|
+
* - **Disabled**: When `enabled=false`, `sendMessage` rejects; `sendMessageNoWait` is a no-op.
|
|
640
|
+
* - **Connection closed**: All pending requests reject with "WebSocket connection closed".
|
|
641
|
+
* - **Queued messages**: If the connection is not yet open, messages are queued and sent
|
|
642
|
+
* when the connection opens (via `setSendToConnection`).
|
|
643
|
+
*
|
|
644
|
+
* ## Cleanup
|
|
645
|
+
*
|
|
646
|
+
* {@link reset} is called by WebsocketConnection when the URL changes or during reconnection.
|
|
647
|
+
* When the last hook unmounts, {@link unregisterHook} triggers removal after
|
|
648
|
+
* {@link INITIATOR_REMOVAL_DELAY_MS}.
|
|
649
|
+
*
|
|
650
|
+
* @template TData - The type of data received in the response
|
|
651
|
+
* @template TBody - The type of message body sent to the WebSocket
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```typescript
|
|
655
|
+
* const api = new WebsocketMessageApi<MyResponse, MyRequest>({
|
|
656
|
+
* url: 'wss://example.com',
|
|
657
|
+
* key: 'my-message-api',
|
|
658
|
+
* responseTimeoutMs: 5000
|
|
659
|
+
* });
|
|
660
|
+
* connection.addListener(api);
|
|
661
|
+
*
|
|
662
|
+
* const response = await api.sendMessage('/api/command', 'post', { action: 'refresh' });
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
665
|
+
export declare class WebsocketMessageApi implements WebsocketListener {
|
|
666
|
+
private _options;
|
|
667
|
+
private _sendToConnection;
|
|
668
|
+
private _pendingByUri;
|
|
669
|
+
private _pendingMessages;
|
|
670
|
+
private _registeredHooks;
|
|
671
|
+
private _hookRemovalTimeout;
|
|
672
|
+
constructor(options: WebsocketMessageOptions);
|
|
673
|
+
/** Unique key identifier for this Message API. */
|
|
674
|
+
get key(): string;
|
|
675
|
+
get url(): string;
|
|
676
|
+
/** Whether this Message API is enabled. */
|
|
677
|
+
get isEnabled(): boolean;
|
|
678
|
+
/**
|
|
679
|
+
* Returns whether this API is waiting for a response for the given URI.
|
|
680
|
+
*
|
|
681
|
+
* Used by {@link WebsocketConnection} to route incoming messages to the correct
|
|
682
|
+
* listener. Message API receives messages only for URIs with pending requests.
|
|
683
|
+
*
|
|
684
|
+
* @param uri - The URI to check
|
|
685
|
+
* @returns `true` if a request is pending for this URI
|
|
686
|
+
*/
|
|
687
|
+
hasWaitingUri: (uri: string) => boolean;
|
|
688
|
+
/**
|
|
689
|
+
* Registers a hook (component) that is using this Message API.
|
|
690
|
+
*
|
|
691
|
+
* Tracks the hook ID so the API is only removed from the connection when
|
|
692
|
+
* the last hook unmounts.
|
|
693
|
+
*
|
|
694
|
+
* @param id - Unique identifier for the registering hook
|
|
695
|
+
*/
|
|
696
|
+
registerHook: (id: string) => void;
|
|
697
|
+
/**
|
|
698
|
+
* Unregisters a hook from this Message API.
|
|
699
|
+
*
|
|
700
|
+
* After {@link INITIATOR_REMOVAL_DELAY_MS}, if no hooks remain, invokes the
|
|
701
|
+
* cleanup callback to remove this API from the connection. The delay prevents
|
|
702
|
+
* rapid subscribe/unsubscribe cycles during React re-renders.
|
|
703
|
+
*
|
|
704
|
+
* @param id - The hook ID to unregister
|
|
705
|
+
* @param onRemove - Callback invoked when the last hook is removed (after delay)
|
|
706
|
+
*/
|
|
707
|
+
unregisterHook: (id: string, onRemove: () => void) => void;
|
|
708
|
+
/**
|
|
709
|
+
* Disconnects this Message API from the parent WebSocket connection.
|
|
710
|
+
*
|
|
711
|
+
* Called when the hook is disabled. After a delay, invokes the cleanup callback.
|
|
712
|
+
* Clears any pending hook-removal timeout to avoid duplicate cleanup.
|
|
713
|
+
*
|
|
714
|
+
* @param onRemoveFromSocket - Callback invoked after delay to remove from connection
|
|
715
|
+
*/
|
|
716
|
+
disconnect: (onRemoveFromSocket: () => void) => void;
|
|
717
|
+
/**
|
|
718
|
+
* Sets or clears the callback used to send messages through the parent WebSocket connection.
|
|
719
|
+
*
|
|
720
|
+
* When setting a callback, flushes any queued messages. When clearing, cancels all
|
|
721
|
+
* pending requests and clears the hook removal timeout to avoid redundant cleanup.
|
|
722
|
+
*
|
|
723
|
+
* @param callback - The send function, or null to disconnect
|
|
724
|
+
*/
|
|
725
|
+
setSendToConnection: (callback: SendToConnectionFn | null) => void;
|
|
726
|
+
/**
|
|
727
|
+
* Delivers an incoming message for a URI we're waiting on.
|
|
728
|
+
*
|
|
729
|
+
* Called by WebsocketConnection when a message arrives for a URI with a pending request.
|
|
730
|
+
*
|
|
731
|
+
* @param uri - The URI the response is for
|
|
732
|
+
* @param data - The response data
|
|
733
|
+
*/
|
|
734
|
+
deliverMessage: (uri: string, data: unknown) => void;
|
|
735
|
+
/**
|
|
736
|
+
* Sends a message to the given URI and optionally waits for a response.
|
|
737
|
+
*
|
|
738
|
+
* **Overwrite behavior**: If a request is already pending for this URI, it is
|
|
739
|
+
* cancelled (rejected with "WebSocket request overwritten for URI") and replaced.
|
|
740
|
+
*
|
|
741
|
+
* @param uri - The URI to send the message to
|
|
742
|
+
* @param bodyOrMethod - Message body (short form) or HTTP method (full form)
|
|
743
|
+
* @param bodyOrOptions - Message body or options (full form)
|
|
744
|
+
* @param options - Per-call options when using full signature
|
|
745
|
+
* @returns Promise that resolves with the response data; rejects on timeout, overwrite, or disabled
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* await api.sendMessage('/api/command', 'post', { action: 'refresh' });
|
|
749
|
+
* await api.sendMessage('/api/command', 'post', { action: 'refresh' }, { timeout: 5000 });
|
|
750
|
+
*/
|
|
751
|
+
sendMessage<TData = unknown, TBody = unknown>(uri: string, method: string, body?: TBody, options?: SendMessageOptions): Promise<TData>;
|
|
752
|
+
/**
|
|
753
|
+
* Sends a message without waiting for a response (fire-and-forget).
|
|
754
|
+
*
|
|
755
|
+
* @param uri - The URI to send the message to
|
|
756
|
+
* @param methodOrBody - HTTP method (full form) or message body (short form)
|
|
757
|
+
* @param body - Message body when using full form
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* api.sendMessageNoWait('/api/log', 'post', { event: 'click' });
|
|
761
|
+
*/
|
|
762
|
+
sendMessageNoWait<TBody = unknown>(uri: string, method: string, body?: TBody): void;
|
|
763
|
+
/** @inheritdoc */
|
|
764
|
+
onError: (error: WebsocketTransportError) => void;
|
|
765
|
+
/** @inheritdoc */
|
|
766
|
+
onMessageError: (error: WebsocketServerError) => void;
|
|
767
|
+
/** @inheritdoc */
|
|
768
|
+
onClose: (event: CloseEvent) => void;
|
|
769
|
+
/**
|
|
770
|
+
* Resets this Message API, cancelling all pending requests.
|
|
771
|
+
*
|
|
772
|
+
* Called by WebsocketConnection when the URL changes or during reconnection.
|
|
773
|
+
* Clears the hook removal timeout to prevent stale cleanup callbacks.
|
|
774
|
+
*/
|
|
775
|
+
reset: () => void;
|
|
776
|
+
private _clearHookRemovalTimeout;
|
|
777
|
+
private _scheduleHookRemoval;
|
|
778
|
+
private _flushPendingMessages;
|
|
779
|
+
private _sendOrQueue;
|
|
780
|
+
private _cancelPendingForUri;
|
|
781
|
+
private _cancelAllPending;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export declare type WebsocketMessageApiPublic = Pick<WebsocketMessageApi, 'sendMessage' | 'sendMessageNoWait' | 'reset' | 'url' | 'key' | 'isEnabled'>;
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Configuration options for WebSocket Message API.
|
|
788
|
+
*
|
|
789
|
+
* Message API is for request/response style communication: send a message to any URI
|
|
790
|
+
* and optionally wait for a response. No subscription support.
|
|
791
|
+
*
|
|
792
|
+
* @template TData - The type of data received in the response
|
|
793
|
+
* @template TBody - The type of message body sent to the WebSocket
|
|
794
|
+
*/
|
|
795
|
+
export declare interface WebsocketMessageOptions {
|
|
796
|
+
/** The base URL of the WebSocket connection. */
|
|
797
|
+
url: string;
|
|
798
|
+
/**
|
|
799
|
+
* Unique key for the Message API.
|
|
800
|
+
*
|
|
801
|
+
* Used to identify the API in the connection.
|
|
802
|
+
*/
|
|
803
|
+
key: string;
|
|
804
|
+
/** Whether this Message API is enabled (default: true). When disabled, messages are not sent. */
|
|
805
|
+
enabled?: boolean;
|
|
806
|
+
/**
|
|
807
|
+
* Default timeout in ms when waiting for a response.
|
|
808
|
+
*
|
|
809
|
+
* Can be overridden per sendMessage call.
|
|
810
|
+
*/
|
|
811
|
+
responseTimeoutMs?: number;
|
|
812
|
+
/**
|
|
813
|
+
* Callback invoked when a WebSocket transport error occurs.
|
|
814
|
+
*/
|
|
815
|
+
onError?: (error: WebsocketTransportError) => void;
|
|
816
|
+
/**
|
|
817
|
+
* Callback invoked when a server error message is received.
|
|
818
|
+
*/
|
|
819
|
+
onMessageError?: (error: WebsocketServerError) => void;
|
|
820
|
+
/**
|
|
821
|
+
* Callback invoked when the WebSocket connection closes.
|
|
822
|
+
*/
|
|
823
|
+
onClose?: (event: CloseEvent) => void;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Error sent by the server via a message with method 'error', 'conflict', or 'exception'.
|
|
828
|
+
* Contains the parsed message body for application-level error handling.
|
|
829
|
+
*
|
|
830
|
+
* @template TBody - The type of the error body payload
|
|
831
|
+
*/
|
|
832
|
+
export declare interface WebsocketServerError<TBody = unknown> {
|
|
833
|
+
readonly type: 'server';
|
|
834
|
+
readonly message: IncomingWebsocketMessage<TBody>;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Manages a single WebSocket URI endpoint with subscription lifecycle and message handling.
|
|
839
|
+
*
|
|
840
|
+
* Use for streaming data (voyage list, notifications). Provides a TanStack Store for
|
|
841
|
+
* reactive updates. Multiple components share one instance via a unique key.
|
|
842
|
+
*
|
|
843
|
+
* ## Key Features
|
|
844
|
+
*
|
|
845
|
+
* - **Reactive Store**: TanStack Store updates when messages are received
|
|
846
|
+
* - **pendingSubscription**: `true` from subscribe until first message (use for loading states)
|
|
847
|
+
* - **Auto-subscribe**: Subscribes when the WebSocket connection opens
|
|
848
|
+
* - **Hook Tracking**: Tracks components; unsubscribes when the last hook unmounts
|
|
849
|
+
*
|
|
850
|
+
* ## Edge Cases
|
|
851
|
+
*
|
|
852
|
+
* - **Multiple initiators**: Using the same key in multiple components emits a console warning;
|
|
853
|
+
* multiple initiators can cause unexpected behavior.
|
|
854
|
+
* - **Body change in subscribe-on-open**: When `options.body` changes, re-subscribes automatically.
|
|
855
|
+
* - **enabled=false**: Unsubscribes and disconnects; re-enabling triggers subscribe.
|
|
856
|
+
* - **reset**: Called by connection on URL change/reconnect; clears store state.
|
|
857
|
+
*
|
|
858
|
+
* ## Cleanup
|
|
859
|
+
*
|
|
860
|
+
* {@link reset} is called by WebsocketConnection on URL change or reconnection.
|
|
861
|
+
* {@link unregisterHook} triggers removal after {@link INITIATOR_REMOVAL_DELAY_MS}.
|
|
862
|
+
*
|
|
863
|
+
* @template TData - The type of data received from the WebSocket
|
|
864
|
+
* @template TBody - The type of message body sent
|
|
865
|
+
*
|
|
866
|
+
* @example
|
|
867
|
+
* ```typescript
|
|
868
|
+
* const api = new WebsocketSubscriptionApi<MyData, MyBody>({
|
|
869
|
+
* url: 'wss://example.com',
|
|
870
|
+
* uri: '/api/stream',
|
|
871
|
+
* key: 'my-stream-key',
|
|
872
|
+
* body: { filter: 'active' },
|
|
873
|
+
* onMessage: (data) => console.log('Received:', data)
|
|
874
|
+
* });
|
|
875
|
+
*
|
|
876
|
+
* const data = useStore(api.store, (s) => s.message);
|
|
877
|
+
* const isPending = useStore(api.store, (s) => s.pendingSubscription);
|
|
878
|
+
* api.sendMessage({ method: 'refresh', body: { force: true } });
|
|
879
|
+
* ```
|
|
880
|
+
*
|
|
881
|
+
* @see {@link useWebsocketSubscription} - React hook
|
|
882
|
+
* @see {@link WebsocketConnection} - Connection manager
|
|
883
|
+
*/
|
|
884
|
+
export declare class WebsocketSubscriptionApi<TData = unknown, TBody = unknown> implements WebsocketListener {
|
|
885
|
+
private _options;
|
|
886
|
+
private _state;
|
|
887
|
+
private _registeredHooks;
|
|
888
|
+
private _disconnectTimeout;
|
|
889
|
+
private _hookRemovalTimeout;
|
|
890
|
+
private _sendToConnection;
|
|
891
|
+
private _pendingMessages;
|
|
892
|
+
constructor(options: WebsocketSubscriptionOptions<TData, TBody>);
|
|
893
|
+
/** Unique key identifier for this WebSocket URI API. */
|
|
894
|
+
get key(): string;
|
|
895
|
+
/** URI path for this WebSocket subscription. */
|
|
896
|
+
get uri(): string;
|
|
897
|
+
get url(): string;
|
|
898
|
+
/** Configuration options for this WebSocket URI. */
|
|
899
|
+
get options(): WebsocketSubscriptionOptions<TData, TBody>;
|
|
900
|
+
/**
|
|
901
|
+
* Current data from the store.
|
|
902
|
+
*
|
|
903
|
+
* **Do not use in React components** — it does not trigger re-renders. Use
|
|
904
|
+
* `useStore(api.store, (s) => s.message)` for reactive updates.
|
|
905
|
+
*/
|
|
906
|
+
get data(): TData | undefined;
|
|
907
|
+
/** TanStack store containing subscription state (message, subscribed, connected, pendingSubscription, etc.). */
|
|
908
|
+
get store(): Store<WebsocketSubscriptionStore<TData>>;
|
|
909
|
+
/** Whether this WebSocket URI is enabled. */
|
|
910
|
+
get isEnabled(): boolean;
|
|
911
|
+
/**
|
|
912
|
+
* Updates the configuration options for this subscription.
|
|
913
|
+
*
|
|
914
|
+
* Handles lifecycle changes:
|
|
915
|
+
* - **Body change** (subscribe-on-open): Re-subscribes with new body
|
|
916
|
+
* - **Enabled: false → true**: Subscribes
|
|
917
|
+
* - **Enabled: true → false**: Unsubscribes
|
|
918
|
+
*
|
|
919
|
+
* Uses deep equality to skip no-op updates.
|
|
920
|
+
*
|
|
921
|
+
* @param options - New options (merged with existing)
|
|
922
|
+
*/
|
|
923
|
+
set options(options: WebsocketSubscriptionOptions<TData, TBody>);
|
|
924
|
+
/**
|
|
925
|
+
* Sets or clears the callback used to send messages through the parent WebSocket connection.
|
|
926
|
+
*
|
|
927
|
+
* When clearing, flushes pending messages and clears removal timeouts to avoid redundant cleanup.
|
|
928
|
+
*
|
|
929
|
+
* @param callback - The send function, or null to disconnect
|
|
930
|
+
*/
|
|
931
|
+
setSendToConnection: (callback: SendToConnectionFn | null) => void;
|
|
932
|
+
/**
|
|
933
|
+
* Registers a hook (component) that is using this subscription.
|
|
934
|
+
*
|
|
935
|
+
* Clears pending removal/disconnect timeouts and tracks the hook ID.
|
|
936
|
+
* Emits a console warning if more than one hook is registered (multiple initiators).
|
|
937
|
+
*
|
|
938
|
+
* @param id - Unique identifier for the registering hook
|
|
939
|
+
*/
|
|
940
|
+
registerHook: (id: string) => void;
|
|
941
|
+
/**
|
|
942
|
+
* Unregisters a hook from this subscription.
|
|
943
|
+
*
|
|
944
|
+
* After {@link INITIATOR_REMOVAL_DELAY_MS}, if no hooks remain, unsubscribes
|
|
945
|
+
* and invokes the cleanup callback. The delay prevents rapid subscribe/unsubscribe
|
|
946
|
+
* during React re-renders.
|
|
947
|
+
*
|
|
948
|
+
* @param id - The hook ID to unregister
|
|
949
|
+
* @param onRemove - Callback invoked when the last hook is removed (after delay)
|
|
950
|
+
*/
|
|
951
|
+
unregisterHook: (id: string, onRemove: () => void) => void;
|
|
952
|
+
/**
|
|
953
|
+
* Disconnects this subscription from the parent WebSocket connection.
|
|
954
|
+
*
|
|
955
|
+
* Immediately unsubscribes, then after {@link INITIATOR_REMOVAL_DELAY_MS} invokes
|
|
956
|
+
* the cleanup callback. Called when the hook is disabled (`enabled=false`).
|
|
957
|
+
*
|
|
958
|
+
* @param onRemoveFromSocket - Callback invoked after delay to remove from connection
|
|
959
|
+
*/
|
|
960
|
+
disconnect: (onRemoveFromSocket: () => void) => void;
|
|
961
|
+
/**
|
|
962
|
+
* Resets this subscription to its initial state.
|
|
963
|
+
*
|
|
964
|
+
* Clears connection/subscription state, resets store data, and cancels pending timeouts.
|
|
965
|
+
* Only runs when currently connected. Called by WebsocketConnection on URL change
|
|
966
|
+
* or reconnection.
|
|
967
|
+
*/
|
|
968
|
+
reset: () => void;
|
|
969
|
+
/**
|
|
970
|
+
* Sends a custom message through the WebSocket for this URI.
|
|
971
|
+
*
|
|
972
|
+
* Automatically appends the URI and method. Queues if connection not yet set.
|
|
973
|
+
*
|
|
974
|
+
* @param message - The message to send (uri and method may be overridden)
|
|
975
|
+
*/
|
|
976
|
+
sendMessage: (message: SendMessage<string, string, TBody>) => void;
|
|
977
|
+
/**
|
|
978
|
+
* Subscribes to this WebSocket URI to start receiving messages.
|
|
979
|
+
*
|
|
980
|
+
* Only subscribes when enabled. Sends a 'subscribe' message through the parent connection.
|
|
981
|
+
*
|
|
982
|
+
* @param body - Optional body to send with the subscription
|
|
983
|
+
*/
|
|
984
|
+
subscribe: (body?: TBody) => void;
|
|
985
|
+
/**
|
|
986
|
+
* Unsubscribes from this WebSocket URI to stop receiving messages.
|
|
987
|
+
*
|
|
988
|
+
* Only unsubscribes when currently subscribed.
|
|
989
|
+
*/
|
|
990
|
+
unsubscribe: () => void;
|
|
991
|
+
/**
|
|
992
|
+
* Called by WebsocketConnection when the WebSocket connection opens.
|
|
993
|
+
*
|
|
994
|
+
* Subscribes with the configured body.
|
|
995
|
+
*/
|
|
996
|
+
onOpen: () => void;
|
|
997
|
+
/**
|
|
998
|
+
* Called by WebsocketConnection when a message is received for this URI.
|
|
999
|
+
*
|
|
1000
|
+
* @param data - The message data
|
|
1001
|
+
*/
|
|
1002
|
+
onMessage: (data: TData) => void;
|
|
1003
|
+
/** @inheritdoc */
|
|
1004
|
+
onError: (error: WebsocketTransportError) => void;
|
|
1005
|
+
/**
|
|
1006
|
+
* Called by WebsocketConnection when a server error message is received.
|
|
1007
|
+
*
|
|
1008
|
+
* @param error - Server error with parsed message body
|
|
1009
|
+
*/
|
|
1010
|
+
onMessageError: (error: WebsocketServerError<TBody>) => void;
|
|
1011
|
+
/**
|
|
1012
|
+
* Called by WebsocketConnection when the WebSocket connection closes.
|
|
1013
|
+
*
|
|
1014
|
+
* Resets subscription state to ensure a fresh subscription on reconnect.
|
|
1015
|
+
*/
|
|
1016
|
+
onClose: (event: CloseEvent) => void;
|
|
1017
|
+
private _clearPendingTimeouts;
|
|
1018
|
+
private _scheduleHookRemoval;
|
|
1019
|
+
private _flushPendingMessages;
|
|
1020
|
+
private _sendOrQueue;
|
|
1021
|
+
private _handleSubscriptionUpdates;
|
|
1022
|
+
private _handleUnsubscribeOnDisable;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export declare type WebsocketSubscriptionApiPublic<TData = unknown, TBody = unknown> = Pick<WebsocketSubscriptionApi<TData, TBody>, 'reset' | 'url' | 'key' | 'isEnabled' | 'store'>;
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Configuration options for WebSocket URI APIs.
|
|
1029
|
+
*
|
|
1030
|
+
* Subscriptions automatically subscribe when the WebSocket connection opens.
|
|
1031
|
+
*
|
|
1032
|
+
* @template TData - The type of data received from the WebSocket
|
|
1033
|
+
* @template TBody - The type of message body sent to the WebSocket
|
|
1034
|
+
*/
|
|
1035
|
+
export declare interface WebsocketSubscriptionOptions<TData = unknown, TBody = unknown> {
|
|
1036
|
+
/** The base URL of the WebSocket connection. */
|
|
1037
|
+
url: string;
|
|
1038
|
+
/** The URI path for this subscription. */
|
|
1039
|
+
uri: string;
|
|
1040
|
+
/**
|
|
1041
|
+
* Unique key for the URI API.
|
|
1042
|
+
*
|
|
1043
|
+
* Used to identify the URI API in the connection.
|
|
1044
|
+
*/
|
|
1045
|
+
key: string;
|
|
1046
|
+
/** Whether this URI API is enabled (default: true). When disabled, messages are not sent. */
|
|
1047
|
+
enabled?: boolean;
|
|
1048
|
+
/** Optional body payload to send with subscription or initial message */
|
|
1049
|
+
body?: TBody;
|
|
1050
|
+
/** Optional HTTP method for custom messages sent via sendMessage */
|
|
1051
|
+
method?: string;
|
|
1052
|
+
/**
|
|
1053
|
+
* Callback invoked when subscription is successful.
|
|
1054
|
+
*
|
|
1055
|
+
* @param uri - The URI path that was subscribed to
|
|
1056
|
+
* @param body - The body that was sent with the subscription
|
|
1057
|
+
*/
|
|
1058
|
+
onSubscribe?: (props: {
|
|
1059
|
+
uri: string;
|
|
1060
|
+
body?: TBody;
|
|
1061
|
+
uriApi: WebsocketSubscriptionApi<TData, TBody>;
|
|
1062
|
+
}) => void;
|
|
1063
|
+
/**
|
|
1064
|
+
* Callback invoked when a message is received for this URI.
|
|
1065
|
+
*
|
|
1066
|
+
* @param data - The message data received from the WebSocket
|
|
1067
|
+
* @param uriApi - The URI API instance that received the message
|
|
1068
|
+
*/
|
|
1069
|
+
onMessage?: (props: {
|
|
1070
|
+
data: TData;
|
|
1071
|
+
uriApi: WebsocketSubscriptionApi<TData, TBody>;
|
|
1072
|
+
}) => void;
|
|
1073
|
+
/**
|
|
1074
|
+
* Callback invoked when a WebSocket error occurs.
|
|
1075
|
+
*
|
|
1076
|
+
* @param error - Discriminated error: use `error.type === 'server'` for server-sent error messages
|
|
1077
|
+
* (parsed body in `error.message`), or `error.type === 'transport'` for connection failures.
|
|
1078
|
+
*/
|
|
1079
|
+
onError?: (error: WebsocketTransportError) => void;
|
|
1080
|
+
/**
|
|
1081
|
+
* A callback function called when an error occurs with a subscription or message.
|
|
1082
|
+
*
|
|
1083
|
+
* @param event - The error event object.
|
|
1084
|
+
*/
|
|
1085
|
+
onMessageError?: (error: WebsocketServerError<TBody>) => void;
|
|
1086
|
+
/**
|
|
1087
|
+
* Callback invoked when the WebSocket connection closes.
|
|
1088
|
+
*
|
|
1089
|
+
* @param event - The close event from the WebSocket connection
|
|
1090
|
+
*/
|
|
1091
|
+
onClose?: (event: CloseEvent) => void;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
export declare interface WebsocketSubscriptionStore<TData = unknown> {
|
|
1095
|
+
message: TData | undefined;
|
|
1096
|
+
subscribed: boolean;
|
|
1097
|
+
/**
|
|
1098
|
+
* Whether a subscription has been sent but no response received yet.
|
|
1099
|
+
*
|
|
1100
|
+
* - `true`: A subscribe message was sent and we are waiting for the first (or next) message.
|
|
1101
|
+
* - `false`: No subscription is active, the connection is closed, or we have already received a response.
|
|
1102
|
+
*
|
|
1103
|
+
* Use this to show loading/placeholder UI while waiting for initial data after subscribing.
|
|
1104
|
+
*/
|
|
1105
|
+
pendingSubscription: boolean;
|
|
1106
|
+
subscribedAt: number | undefined;
|
|
1107
|
+
receivedAt: number | undefined;
|
|
1108
|
+
connected: boolean;
|
|
1109
|
+
messageError: WebsocketTransportError | undefined;
|
|
1110
|
+
serverError: WebsocketServerError<unknown> | undefined;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Error from the WebSocket transport layer (connection failure, network issues, etc.).
|
|
1115
|
+
* Contains the raw Event from the WebSocket 'error' handler.
|
|
1116
|
+
*/
|
|
1117
|
+
export declare interface WebsocketTransportError {
|
|
1118
|
+
readonly type: 'transport';
|
|
1119
|
+
readonly event: Event;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export { }
|