@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,350 @@
1
+ # @max-troost-io/use-websocket
2
+
3
+ A robust WebSocket connection management package for React applications with automatic reconnection, heartbeat monitoring, URI-based message routing, and React integration via TanStack Store.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @max-troost-io/use-websocket
9
+ ```
10
+
11
+ ## 📚 Navigation
12
+
13
+ ### Internal Sections
14
+
15
+ - [Features & Purpose](#-features--purpose)
16
+ - [Code Structure](#-code-structure)
17
+ - [Data Flow & Architecture](#-data-flow--architecture)
18
+ - [Key Behaviors](#-key-behaviors)
19
+ - [Usage & Integration](#-usage--integration)
20
+ - [Testing Strategy](#-testing-strategy)
21
+ - [Troubleshooting & Debugging](#-troubleshooting--debugging)
22
+ - [Dependencies](#-dependencies)
23
+
24
+ ---
25
+
26
+ ## 🎯 Features & Purpose
27
+
28
+ This package provides a comprehensive WebSocket solution for React applications that require real-time data streaming and request/response messaging over a single connection.
29
+
30
+ ### Problems Solved
31
+
32
+ - **Duplicate connections**: Prevents multiple WebSocket connections to the same URL
33
+ - **Stale connections**: Detects and recovers from silent connection failures via heartbeat
34
+ - **Reconnection complexity**: Handles reconnection with exponential backoff and browser online/offline detection
35
+ - **Subscription sharing**: Multiple components can share the same subscription via a unique key
36
+ - **Auth-aware URLs**: WebSocket URLs are built from the current auth context (region, role, user)
37
+
38
+ ### Key Features
39
+
40
+ | Feature | Description |
41
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------- |
42
+ | **Singleton Connection** | One connection per URL shared across all hooks |
43
+ | **Key-Based API Management** | Subscription and Message APIs identified by unique keys; components with the same key share the instance |
44
+ | **Automatic Reconnection** | Three-phase exponential backoff (4s → 30s → 90s) |
45
+ | **Heartbeat Monitoring** | Ping/pong every 40 seconds to detect stale connections |
46
+ | **URI-Based Routing** | Multiple subscriptions over a single connection |
47
+ | **React Integration** | TanStack Store for reactive data updates |
48
+ | **Online/Offline Detection** | Browser connectivity change handling |
49
+ | **Two API Types** | **Subscription** for streaming data; **Message** for request/response commands |
50
+
51
+ ### Target Users
52
+
53
+ - **Developers** integrating real-time data (voyages, rotations, notifications) into React apps
54
+ - **Applications** using `@mono-fleet/iam-provider` for region-based authentication
55
+
56
+ ---
57
+
58
+ ## 🏗️ Code Structure
59
+
60
+ ```
61
+ packages/use-websocket/
62
+ ├── src/
63
+ │ ├── index.ts # Public exports
64
+ │ └── lib/
65
+ │ ├── WebsocketHook.ts # React hooks (useWebsocketSubscription, useWebsocketMessage, useWebsocketSubscriptionByKey)
66
+ │ ├── WebsocketConnection.ts # Connection lifecycle, reconnection, heartbeat
67
+ │ ├── WebsocketSubscriptionApi.ts # Streaming subscription per URI
68
+ │ ├── WebsocketMessageApi.ts # Request/response messaging (no subscription)
69
+ │ ├── websocketStores.ts # Global TanStack stores (connections, listeners)
70
+ │ ├── websocketStores.helpers.ts # findOrCreateWebsocketConnection, createWebsocketSubscriptionApi, etc.
71
+ │ ├── types.ts # Types, options, store shapes
72
+ │ ├── constants.ts # Timing, close codes, defaults
73
+ │ ├── WebsocketConnection.helpers.ts # Reconnection, ping, notifications
74
+ │ └── WEBSOCKET_CONNECTION.md # Detailed architecture and flows
75
+ ├── README.md
76
+ ├── CHART.md # Mermaid flow diagrams
77
+ └── package.json
78
+ ```
79
+
80
+ ### Component Hierarchy
81
+
82
+ ```mermaid
83
+ graph TB
84
+ subgraph "React Layer"
85
+ Hook[useWebsocketSubscription / useWebsocketMessage]
86
+ ByKey[useWebsocketSubscriptionByKey]
87
+ Component[React Components]
88
+ end
89
+
90
+ subgraph "Connection Layer"
91
+ Connection[WebsocketConnection<br/>Singleton per URL]
92
+ SubApi[WebsocketSubscriptionApi<br/>One per key]
93
+ MsgApi[WebsocketMessageApi<br/>One per key]
94
+ end
95
+
96
+ subgraph "WebSocket API"
97
+ Socket[WebSocket]
98
+ end
99
+
100
+ Component -->|uses| Hook
101
+ Component -->|uses| ByKey
102
+ Hook -->|manages| Connection
103
+ Hook -->|creates| SubApi
104
+ Hook -->|creates| MsgApi
105
+ Connection -->|manages| Socket
106
+ Connection -->|routes messages to| SubApi
107
+ Connection -->|routes messages to| MsgApi
108
+ SubApi -->|TanStack Store| Component
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 🔄 Data Flow & Architecture
114
+
115
+ ### Choosing the Right Hook
116
+
117
+ | Hook | Use Case |
118
+ | ------------------------------- | ----------------------------------------------------------------- |
119
+ | `useWebsocketSubscription` | Streaming data (voyage list, notifications, live updates) |
120
+ | `useWebsocketMessage` | One-off commands (validate, modify, mark read) — request/response |
121
+ | `useWebsocketSubscriptionByKey` | Child component needs parent's subscription data |
122
+
123
+ ### Message Flow: Subscription
124
+
125
+ ```mermaid
126
+ sequenceDiagram
127
+ participant Component
128
+ participant Hook as useWebsocketSubscription
129
+ participant Connection as WebsocketConnection
130
+ participant SubApi as WebsocketSubscriptionApi
131
+ participant Socket as WebSocket
132
+ participant Server
133
+
134
+ Component->>Hook: useWebsocketSubscription(options)
135
+ Hook->>Connection: findOrCreateWebsocketConnection(url)
136
+ Hook->>Connection: addListener(SubApi)
137
+ Connection->>Socket: new WebSocket(url)
138
+
139
+ Socket-->>Connection: open event
140
+ Connection->>SubApi: onOpen()
141
+ SubApi->>Socket: subscribe message
142
+ Socket->>Server: subscribe
143
+
144
+ Server-->>Socket: message (uri, body)
145
+ Socket-->>Connection: message event
146
+ Connection->>Connection: Route by URI
147
+ Connection->>SubApi: onMessage(body)
148
+ SubApi->>SubApi: Update TanStack Store
149
+ SubApi-->>Component: Store update triggers re-render
150
+ ```
151
+
152
+ ### Message Flow: Request/Response (useWebsocketMessage)
153
+
154
+ ```mermaid
155
+ sequenceDiagram
156
+ participant Component
157
+ participant MsgApi as WebsocketMessageApi
158
+ participant Connection as WebsocketConnection
159
+ participant Socket as WebSocket
160
+ participant Server
161
+
162
+ Component->>MsgApi: sendMessage(uri, method, body?)
163
+ MsgApi->>Socket: Message with correlation ID
164
+ Socket->>Server: message
165
+
166
+ Server-->>Socket: response (same correlation)
167
+ Socket-->>Connection: message event
168
+ Connection->>MsgApi: deliverMessage(uri, data)
169
+ MsgApi->>MsgApi: resolve Promise
170
+ MsgApi-->>Component: await result
171
+ ```
172
+
173
+ ---
174
+
175
+ ## ⚙️ Key Behaviors
176
+
177
+ ### Subscription Behavior
178
+
179
+ Subscriptions automatically subscribe when the WebSocket connection opens.
180
+
181
+ ### Store Shape (WebsocketSubscriptionStore)
182
+
183
+ ```typescript
184
+ interface WebsocketSubscriptionStore<TData> {
185
+ message: TData | undefined; // Latest data from server
186
+ subscribed: boolean; // Subscription confirmed
187
+ pendingSubscription: boolean; // Subscribe sent, waiting for first response (for loading UI)
188
+ subscribedAt: number | undefined;
189
+ receivedAt: number | undefined;
190
+ connected: boolean; // WebSocket open
191
+ messageError: WebsocketTransportError | undefined;
192
+ serverError: WebsocketServerError<unknown> | undefined;
193
+ }
194
+ ```
195
+
196
+ ### Reconnection Backoff
197
+
198
+ | Attempt Range | Wait Time |
199
+ | ------------- | ---------- |
200
+ | 0–4 attempts | 4 seconds |
201
+ | 5–9 attempts | 30 seconds |
202
+ | 10+ attempts | 90 seconds |
203
+
204
+ User notifications are shown after 10 failed attempts. Reconnection stops after 20 attempts (~18 minutes); users can retry manually via the notification action.
205
+
206
+ ---
207
+
208
+ ## 🔧 Usage & Integration
209
+
210
+ ### Subscription (Streaming Data)
211
+
212
+ ```typescript
213
+ import { useWebsocketSubscription } from "@max-troost-io/use-websocket";
214
+ import { useStore } from "@tanstack/react-store";
215
+
216
+ function VoyageList() {
217
+ const voyageApi = useWebsocketSubscription<Voyage[], VoyageFilters>({
218
+ key: "voyages-list",
219
+ url: "/api",
220
+ uri: "/api/voyages",
221
+ body: { status: "active" },
222
+ });
223
+
224
+ const voyages = useStore(voyageApi.store, (s) => s.message);
225
+ const pending = useStore(voyageApi.store, (s) => s.pendingSubscription);
226
+
227
+ if (pending) return <Skeleton />;
228
+ return <div>{/* Render voyages */}</div>;
229
+ }
230
+ ```
231
+
232
+ ### Access Store by Key (Child Components)
233
+
234
+ ```typescript
235
+ import { useWebsocketSubscriptionByKey } from "@max-troost-io/use-websocket";
236
+ import { useStore } from "@tanstack/react-store";
237
+
238
+ function VoyageCount() {
239
+ const voyagesStore = useWebsocketSubscriptionByKey<Voyage[]>("voyages-list");
240
+ const voyages = useStore(voyagesStore, (s) => s.message);
241
+ return <div>Total: {voyages?.length ?? 0}</div>;
242
+ }
243
+ ```
244
+
245
+ ### Message API (Request/Response)
246
+
247
+ ```typescript
248
+ import { useWebsocketMessage } from "@max-troost-io/use-websocket";
249
+
250
+ function VoyageActions() {
251
+ const api = useWebsocketMessage<ModifyVoyageUim, ModifyVoyageUim>({
252
+ key: "voyages/modify",
253
+ url: "/api",
254
+ responseTimeoutMs: 5000,
255
+ });
256
+
257
+ const handleValidate = async () => {
258
+ const result = await api.sendMessage(
259
+ "voyages/modify/validate",
260
+ "post",
261
+ formValues
262
+ );
263
+ // ...
264
+ };
265
+
266
+ const handleMarkRead = () => {
267
+ api.sendMessageNoWait(`notifications/${id}/read`, "post");
268
+ };
269
+ }
270
+ ```
271
+
272
+ ### Options Reference
273
+
274
+ #### WebsocketSubscriptionOptions
275
+
276
+ | Option | Type | Description |
277
+ | ------------------------------------------------------------------ | --------- | ---------------------------------------------------------------------- |
278
+ | `key` | `string` | Unique identifier; components with same key share the API |
279
+ | `url` | `string` | Base WebSocket path (full URL; apps typically build from auth context) |
280
+ | `uri` | `string` | URI endpoint for this subscription |
281
+ | `body` | `TBody` | Optional payload for subscription |
282
+ | `enabled` | `boolean` | When `false`, disconnects (default: `true`) |
283
+ | `onMessage`, `onSubscribe`, `onError`, `onMessageError`, `onClose` | callbacks | Lifecycle callbacks |
284
+
285
+ #### WebsocketMessageOptions
286
+
287
+ | Option | Type | Description |
288
+ | ------------------- | --------- | -------------------------------------------------- |
289
+ | `key` | `string` | Unique identifier |
290
+ | `url` | `string` | Base WebSocket path |
291
+ | `enabled` | `boolean` | When `false`, disconnects |
292
+ | `responseTimeoutMs` | `number` | Default timeout for `sendMessage` (default: 10000) |
293
+
294
+ ---
295
+
296
+ ## 🐛 Troubleshooting & Debugging
297
+
298
+ ### Common Issues
299
+
300
+ #### Subscription Never Receives Data
301
+
302
+ - **Symptoms**: `message` stays `undefined`, `pendingSubscription` remains `true`
303
+ - **Possible causes**: Wrong `uri`, server not sending to that URI, connection not open
304
+ - **Debugging**: Check `connected` in store; verify server logs for incoming subscribe; ensure `useWebsocketConnectionConfig` and `useReconnectWebsocketConnections` are called at app root inside auth provider
305
+ - **Solution**: Confirm `uri` matches server route; check network tab for WebSocket frames
306
+
307
+ #### Connection Drops Repeatedly
308
+
309
+ - **Symptoms**: Frequent reconnects, notifications after 10 attempts
310
+ - **Possible causes**: Auth token expiry, CORS, wrong URL, server rejecting connection
311
+ - **Debugging**: `WebsocketConnection.setCustomLogger` to log events; check `connectionFailed` callback (token refresh triggered after 5 retries)
312
+ - **Solution**: Pass WebSocket secret via `useWebsocketConnectionConfig` for local dev; verify auth context provides valid region/role for URL construction
313
+
314
+ #### Child Component Gets Empty Store
315
+
316
+ - **Symptoms**: `useWebsocketSubscriptionByKey` returns fallback store with `message: undefined`
317
+ - **Possible causes**: Parent with `useWebsocketSubscription` not mounted yet; different `key` used
318
+ - **Debugging**: Ensure parent mounts first; verify `key` string matches exactly
319
+ - **Solution**: Use same `key` in parent and child; consider lifting subscription higher in tree
320
+
321
+ ### Debugging Tools
322
+
323
+ - **Browser DevTools**: Network tab → WS filter for WebSocket frames
324
+ - **Debugging**: `WebsocketConnection.setCustomLogger` to log events; check `connectionFailed` callback (token refresh triggered after 5 retries)
325
+ - **Store inspection**: `useStore(api.store)` to read full state
326
+
327
+ ### Error Types
328
+
329
+ - **WebsocketTransportError**: Connection failure, network issues (`error.type === 'transport'`)
330
+ - **WebsocketServerError**: Server-sent error message (`error.type === 'server'`, body in `error.message`)
331
+
332
+ ---
333
+
334
+ ## 📦 Dependencies
335
+
336
+ | Dependency | Purpose |
337
+ | ----------------------- | ---------------------------------------- |
338
+ | `@tanstack/react-store` | Reactive state for components |
339
+ | `@tanstack/store` | Core store implementation |
340
+ | `notistack` | User notifications (reconnection errors) |
341
+ | `uuid` | Correlation IDs |
342
+ | `fast-equals` | Deep equality for options |
343
+ | `usehooks-ts` | `useIsomorphicLayoutEffect` |
344
+
345
+ ---
346
+
347
+ ## Learn More
348
+
349
+ - **[WEBSOCKET_CONNECTION.md](src/lib/WEBSOCKET_CONNECTION.md)** — Detailed architecture, class diagrams, connection lifecycle, URI API lifecycle, browser online/offline handling, full API reference
350
+ - **[CHART.md](CHART.md)** — Mermaid flow diagrams for hooks, connection, and error flows