@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/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
|