zynapse 0.1.11 → 0.1.13

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.
@@ -0,0 +1,257 @@
1
+ import { useEffect, useRef, useCallback, useState } from "react";
2
+ export { WebSocketConnectionStatus };
3
+ var WebSocketConnectionStatus;
4
+ (function (WebSocketConnectionStatus) {
5
+ WebSocketConnectionStatus["CONNECTING"] = "CONNECTING";
6
+ WebSocketConnectionStatus["CONNECTED"] = "CONNECTED";
7
+ WebSocketConnectionStatus["DISCONNECTED"] = "DISCONNECTED";
8
+ WebSocketConnectionStatus["ERROR"] = "ERROR";
9
+ })(WebSocketConnectionStatus || (WebSocketConnectionStatus = {}));
10
+ /**
11
+ * Advanced WebSocket hook with reconnection logic and type safety
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * interface ClientMessage {
16
+ * type: 'subscribe';
17
+ * channel: string;
18
+ * }
19
+ *
20
+ * interface ServerMessage {
21
+ * type: 'update';
22
+ * data: any;
23
+ * }
24
+ *
25
+ * const { sendMessage, lastParsedMessage, connectionStatus, isConnected } =
26
+ * useWebSocket<ClientMessage, ServerMessage>(
27
+ * 'wss://api.example.com',
28
+ * {
29
+ * onMessage: (event) => console.log('Received:', event.data),
30
+ * reconnect: true,
31
+ * maxReconnectAttempts: 10,
32
+ * }
33
+ * );
34
+ * ```
35
+ */
36
+ export function useWebSocket(url, options = {}) {
37
+ const { onMessage, onOpen, onClose, onError, reconnect = true, maxReconnectAttempts = Infinity, reconnectInterval = 1000, maxReconnectInterval = 30000, reconnectBackoffMultiplier = 1.5, protocols, connectOnMount = true, } = options;
38
+ // Use refs to store values that shouldn't trigger re-renders
39
+ const webSocketRef = useRef(null);
40
+ const reconnectTimeoutRef = useRef(null);
41
+ const reconnectAttemptsRef = useRef(0);
42
+ const currentReconnectIntervalRef = useRef(reconnectInterval);
43
+ const shouldReconnectRef = useRef(reconnect);
44
+ const isManualDisconnectRef = useRef(false);
45
+ const cleanupDelayTimeoutRef = useRef(null);
46
+ const isMountedRef = useRef(false);
47
+ // Store callbacks in refs to avoid recreating WebSocket on callback changes
48
+ const onMessageRef = useRef(onMessage);
49
+ const onOpenRef = useRef(onOpen);
50
+ const onCloseRef = useRef(onClose);
51
+ const onErrorRef = useRef(onError);
52
+ // Update refs when callbacks change
53
+ useEffect(() => {
54
+ onMessageRef.current = onMessage;
55
+ }, [onMessage]);
56
+ useEffect(() => {
57
+ onOpenRef.current = onOpen;
58
+ }, [onOpen]);
59
+ useEffect(() => {
60
+ onCloseRef.current = onClose;
61
+ }, [onClose]);
62
+ useEffect(() => {
63
+ onErrorRef.current = onError;
64
+ }, [onError]);
65
+ useEffect(() => {
66
+ shouldReconnectRef.current = reconnect;
67
+ }, [reconnect]);
68
+ // State for values that should trigger re-renders
69
+ const [lastRawMessage, setLastMessage] = useState(null);
70
+ const [lastMessage, setLastParsedMessage] = useState(null);
71
+ const [connectionStatus, setConnectionStatus] = useState(WebSocketConnectionStatus.DISCONNECTED);
72
+ const [reconnectAttempts, setReconnectAttempts] = useState(0);
73
+ // Clear all timers
74
+ const clearTimers = useCallback(() => {
75
+ if (reconnectTimeoutRef.current) {
76
+ clearTimeout(reconnectTimeoutRef.current);
77
+ reconnectTimeoutRef.current = null;
78
+ }
79
+ if (cleanupDelayTimeoutRef.current) {
80
+ clearTimeout(cleanupDelayTimeoutRef.current);
81
+ cleanupDelayTimeoutRef.current = null;
82
+ }
83
+ }, []);
84
+ // Cleanup WebSocket
85
+ const cleanup = useCallback(() => {
86
+ clearTimers();
87
+ if (webSocketRef.current) {
88
+ const ws = webSocketRef.current;
89
+ webSocketRef.current = null;
90
+ // Remove all event listeners before closing
91
+ ws.onopen = null;
92
+ ws.onclose = null;
93
+ ws.onerror = null;
94
+ ws.onmessage = null;
95
+ if (ws.readyState === WebSocket.OPEN ||
96
+ ws.readyState === WebSocket.CONNECTING) {
97
+ ws.close(1000, "Component unmounting or reconnecting");
98
+ }
99
+ }
100
+ }, [clearTimers]);
101
+ // Connect to WebSocket
102
+ const connect = useCallback(() => {
103
+ if (!url)
104
+ return;
105
+ // Cancel any pending delayed cleanup
106
+ if (cleanupDelayTimeoutRef.current) {
107
+ clearTimeout(cleanupDelayTimeoutRef.current);
108
+ cleanupDelayTimeoutRef.current = null;
109
+ }
110
+ // Prevent multiple simultaneous connections
111
+ if (webSocketRef.current?.readyState === WebSocket.CONNECTING ||
112
+ webSocketRef.current?.readyState === WebSocket.OPEN) {
113
+ return;
114
+ }
115
+ cleanup();
116
+ isManualDisconnectRef.current = false;
117
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTING);
118
+ try {
119
+ const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
120
+ webSocketRef.current = ws;
121
+ ws.onopen = (event) => {
122
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTED);
123
+ reconnectAttemptsRef.current = 0;
124
+ currentReconnectIntervalRef.current = reconnectInterval;
125
+ setReconnectAttempts(0);
126
+ onOpenRef.current?.(event);
127
+ };
128
+ ws.onclose = (event) => {
129
+ setConnectionStatus(WebSocketConnectionStatus.DISCONNECTED);
130
+ clearTimers();
131
+ onCloseRef.current?.(event);
132
+ // Attempt reconnection if enabled and not manually disconnected
133
+ if (shouldReconnectRef.current &&
134
+ !isManualDisconnectRef.current &&
135
+ reconnectAttemptsRef.current < maxReconnectAttempts) {
136
+ reconnectAttemptsRef.current += 1;
137
+ setReconnectAttempts(reconnectAttemptsRef.current);
138
+ const delay = Math.min(currentReconnectIntervalRef.current, maxReconnectInterval);
139
+ reconnectTimeoutRef.current = setTimeout(() => {
140
+ currentReconnectIntervalRef.current *= reconnectBackoffMultiplier;
141
+ connect();
142
+ }, delay);
143
+ }
144
+ };
145
+ ws.onerror = (event) => {
146
+ setConnectionStatus(WebSocketConnectionStatus.ERROR);
147
+ onErrorRef.current?.(event);
148
+ };
149
+ ws.onmessage = (event) => {
150
+ setLastMessage(event);
151
+ // Attempt to parse JSON data for typed message
152
+ try {
153
+ const parsed = typeof event.data === "string"
154
+ ? JSON.parse(event.data)
155
+ : event.data;
156
+ setLastParsedMessage(parsed);
157
+ }
158
+ catch (error) {
159
+ // If parsing fails, set parsed message to null
160
+ setLastParsedMessage(null);
161
+ }
162
+ onMessageRef.current?.(event);
163
+ };
164
+ }
165
+ catch (error) {
166
+ setConnectionStatus(WebSocketConnectionStatus.ERROR);
167
+ console.error("WebSocket connection error:", error);
168
+ }
169
+ }, [
170
+ url,
171
+ protocols,
172
+ reconnectInterval,
173
+ maxReconnectInterval,
174
+ maxReconnectAttempts,
175
+ reconnectBackoffMultiplier,
176
+ cleanup,
177
+ clearTimers,
178
+ ]);
179
+ // Disconnect from WebSocket
180
+ const disconnect = useCallback(() => {
181
+ isManualDisconnectRef.current = true;
182
+ cleanup();
183
+ setConnectionStatus(WebSocketConnectionStatus.DISCONNECTED);
184
+ reconnectAttemptsRef.current = 0;
185
+ setReconnectAttempts(0);
186
+ currentReconnectIntervalRef.current = reconnectInterval;
187
+ }, [cleanup, reconnectInterval]);
188
+ // Send typed message
189
+ const sendMessage = useCallback((message) => {
190
+ if (webSocketRef.current?.readyState === WebSocket.OPEN) {
191
+ const messageStr = typeof message === "string" ? message : JSON.stringify(message);
192
+ webSocketRef.current.send(messageStr);
193
+ }
194
+ else {
195
+ console.warn("WebSocket is not connected. Message not sent:", message);
196
+ }
197
+ }, []);
198
+ // Send raw string message
199
+ const sendRawMessage = useCallback((message) => {
200
+ if (webSocketRef.current?.readyState === WebSocket.OPEN) {
201
+ webSocketRef.current.send(message);
202
+ }
203
+ else {
204
+ console.warn("WebSocket is not connected. Message not sent:", message);
205
+ }
206
+ }, []);
207
+ // Connect on mount if enabled
208
+ useEffect(() => {
209
+ isMountedRef.current = true;
210
+ // Cancel any pending cleanup from previous unmount (Strict Mode case)
211
+ if (cleanupDelayTimeoutRef.current) {
212
+ clearTimeout(cleanupDelayTimeoutRef.current);
213
+ cleanupDelayTimeoutRef.current = null;
214
+ }
215
+ // If connection exists and is open/connecting, reuse it
216
+ if (webSocketRef.current &&
217
+ (webSocketRef.current.readyState === WebSocket.OPEN ||
218
+ webSocketRef.current.readyState === WebSocket.CONNECTING)) {
219
+ // Connection already exists, just update status if needed
220
+ if (webSocketRef.current.readyState === WebSocket.OPEN) {
221
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTED);
222
+ }
223
+ else {
224
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTING);
225
+ }
226
+ }
227
+ else if (connectOnMount && url) {
228
+ // No existing connection, create new one
229
+ connect();
230
+ }
231
+ // Delayed cleanup on unmount to handle Strict Mode
232
+ return () => {
233
+ isMountedRef.current = false;
234
+ // Delay cleanup by 100ms to handle Strict Mode remounting
235
+ cleanupDelayTimeoutRef.current = setTimeout(() => {
236
+ // Only cleanup if component hasn't remounted
237
+ if (!isMountedRef.current) {
238
+ isManualDisconnectRef.current = true;
239
+ cleanup();
240
+ }
241
+ }, 100);
242
+ };
243
+ // eslint-disable-next-line react-hooks/exhaustive-deps
244
+ }, [url, connectOnMount]);
245
+ return {
246
+ sendMessage,
247
+ sendRawMessage,
248
+ lastRawMessage,
249
+ lastMessage,
250
+ connectionStatus,
251
+ connect,
252
+ disconnect,
253
+ isConnected: connectionStatus === WebSocketConnectionStatus.CONNECTED,
254
+ reconnectAttempts,
255
+ };
256
+ }
257
+ //# sourceMappingURL=useWebSocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWebSocket.js","sourceRoot":"","sources":["../../src/schema/useWebSocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,428 @@
1
+ import { useEffect, useRef, useCallback, useState } from "react";
2
+
3
+ export enum WebSocketConnectionStatus {
4
+ CONNECTING = "CONNECTING",
5
+ CONNECTED = "CONNECTED",
6
+ DISCONNECTED = "DISCONNECTED",
7
+ ERROR = "ERROR",
8
+ }
9
+
10
+ export interface UseWebSocketOptions {
11
+ /**
12
+ * Callback fired when a message is received
13
+ */
14
+ onMessage?: (event: MessageEvent) => void;
15
+
16
+ /**
17
+ * Callback fired when connection opens
18
+ */
19
+ onOpen?: (event: Event) => void;
20
+
21
+ /**
22
+ * Callback fired when connection closes
23
+ */
24
+ onClose?: (event: CloseEvent) => void;
25
+
26
+ /**
27
+ * Callback fired on error
28
+ */
29
+ onError?: (event: Event) => void;
30
+
31
+ /**
32
+ * Enable automatic reconnection
33
+ * @default true
34
+ */
35
+ reconnect?: boolean;
36
+
37
+ /**
38
+ * Maximum number of reconnection attempts
39
+ * @default Infinity
40
+ */
41
+ maxReconnectAttempts?: number;
42
+
43
+ /**
44
+ * Initial reconnection delay in milliseconds
45
+ * @default 1000
46
+ */
47
+ reconnectInterval?: number;
48
+
49
+ /**
50
+ * Maximum reconnection delay in milliseconds
51
+ * @default 30000
52
+ */
53
+ maxReconnectInterval?: number;
54
+
55
+ /**
56
+ * Multiplier for exponential backoff
57
+ * @default 1.5
58
+ */
59
+ reconnectBackoffMultiplier?: number;
60
+
61
+ /**
62
+ * WebSocket protocols
63
+ */
64
+ protocols?: string | string[];
65
+
66
+ /**
67
+ * Should connect immediately on mount
68
+ * @default true
69
+ */
70
+ connectOnMount?: boolean;
71
+ }
72
+
73
+ export interface UseWebSocketReturn<TInput = any, TOutput = any> {
74
+ /**
75
+ * Send a message through the WebSocket (client to server)
76
+ */
77
+ sendMessage: (message: TInput) => void;
78
+
79
+ /**
80
+ * Send a raw string message
81
+ */
82
+ sendRawMessage: (message: string) => void;
83
+
84
+ /**
85
+ * The last received message (raw MessageEvent)
86
+ */
87
+ lastRawMessage: MessageEvent | null;
88
+
89
+ /**
90
+ * The last received message parsed as JSON (server to client)
91
+ */
92
+ lastMessage: TOutput | null;
93
+
94
+ /**
95
+ * Current connection status
96
+ */
97
+ connectionStatus: WebSocketConnectionStatus;
98
+
99
+ /**
100
+ * Manually connect to the WebSocket
101
+ */
102
+ connect: () => void;
103
+
104
+ /**
105
+ * Manually disconnect from the WebSocket
106
+ */
107
+ disconnect: () => void;
108
+
109
+ /**
110
+ * Check if currently connected
111
+ */
112
+ isConnected: boolean;
113
+
114
+ /**
115
+ * Number of reconnection attempts made
116
+ */
117
+ reconnectAttempts: number;
118
+ }
119
+
120
+ /**
121
+ * Advanced WebSocket hook with reconnection logic and type safety
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * interface ClientMessage {
126
+ * type: 'subscribe';
127
+ * channel: string;
128
+ * }
129
+ *
130
+ * interface ServerMessage {
131
+ * type: 'update';
132
+ * data: any;
133
+ * }
134
+ *
135
+ * const { sendMessage, lastParsedMessage, connectionStatus, isConnected } =
136
+ * useWebSocket<ClientMessage, ServerMessage>(
137
+ * 'wss://api.example.com',
138
+ * {
139
+ * onMessage: (event) => console.log('Received:', event.data),
140
+ * reconnect: true,
141
+ * maxReconnectAttempts: 10,
142
+ * }
143
+ * );
144
+ * ```
145
+ */
146
+ export function useWebSocket<TInput = any, TOutput = any>(
147
+ url: string | null,
148
+ options: UseWebSocketOptions = {},
149
+ ): UseWebSocketReturn<TInput, TOutput> {
150
+ const {
151
+ onMessage,
152
+ onOpen,
153
+ onClose,
154
+ onError,
155
+ reconnect = true,
156
+ maxReconnectAttempts = Infinity,
157
+ reconnectInterval = 1000,
158
+ maxReconnectInterval = 30000,
159
+ reconnectBackoffMultiplier = 1.5,
160
+ protocols,
161
+ connectOnMount = true,
162
+ } = options;
163
+
164
+ // Use refs to store values that shouldn't trigger re-renders
165
+ const webSocketRef = useRef<WebSocket | null>(null);
166
+ const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
167
+
168
+ const reconnectAttemptsRef = useRef(0);
169
+ const currentReconnectIntervalRef = useRef(reconnectInterval);
170
+ const shouldReconnectRef = useRef(reconnect);
171
+ const isManualDisconnectRef = useRef(false);
172
+ const cleanupDelayTimeoutRef = useRef<NodeJS.Timeout | null>(null);
173
+ const isMountedRef = useRef(false);
174
+
175
+ // Store callbacks in refs to avoid recreating WebSocket on callback changes
176
+ const onMessageRef = useRef(onMessage);
177
+ const onOpenRef = useRef(onOpen);
178
+ const onCloseRef = useRef(onClose);
179
+ const onErrorRef = useRef(onError);
180
+
181
+ // Update refs when callbacks change
182
+ useEffect(() => {
183
+ onMessageRef.current = onMessage;
184
+ }, [onMessage]);
185
+
186
+ useEffect(() => {
187
+ onOpenRef.current = onOpen;
188
+ }, [onOpen]);
189
+
190
+ useEffect(() => {
191
+ onCloseRef.current = onClose;
192
+ }, [onClose]);
193
+
194
+ useEffect(() => {
195
+ onErrorRef.current = onError;
196
+ }, [onError]);
197
+
198
+ useEffect(() => {
199
+ shouldReconnectRef.current = reconnect;
200
+ }, [reconnect]);
201
+
202
+ // State for values that should trigger re-renders
203
+ const [lastRawMessage, setLastMessage] = useState<MessageEvent | null>(null);
204
+ const [lastMessage, setLastParsedMessage] = useState<TOutput | null>(null);
205
+ const [connectionStatus, setConnectionStatus] =
206
+ useState<WebSocketConnectionStatus>(WebSocketConnectionStatus.DISCONNECTED);
207
+ const [reconnectAttempts, setReconnectAttempts] = useState(0);
208
+
209
+ // Clear all timers
210
+ const clearTimers = useCallback(() => {
211
+ if (reconnectTimeoutRef.current) {
212
+ clearTimeout(reconnectTimeoutRef.current);
213
+ reconnectTimeoutRef.current = null;
214
+ }
215
+
216
+ if (cleanupDelayTimeoutRef.current) {
217
+ clearTimeout(cleanupDelayTimeoutRef.current);
218
+ cleanupDelayTimeoutRef.current = null;
219
+ }
220
+ }, []);
221
+
222
+ // Cleanup WebSocket
223
+ const cleanup = useCallback(() => {
224
+ clearTimers();
225
+
226
+ if (webSocketRef.current) {
227
+ const ws = webSocketRef.current;
228
+ webSocketRef.current = null;
229
+
230
+ // Remove all event listeners before closing
231
+ ws.onopen = null;
232
+ ws.onclose = null;
233
+ ws.onerror = null;
234
+ ws.onmessage = null;
235
+
236
+ if (
237
+ ws.readyState === WebSocket.OPEN ||
238
+ ws.readyState === WebSocket.CONNECTING
239
+ ) {
240
+ ws.close(1000, "Component unmounting or reconnecting");
241
+ }
242
+ }
243
+ }, [clearTimers]);
244
+
245
+ // Connect to WebSocket
246
+ const connect = useCallback(() => {
247
+ if (!url) return;
248
+
249
+ // Cancel any pending delayed cleanup
250
+ if (cleanupDelayTimeoutRef.current) {
251
+ clearTimeout(cleanupDelayTimeoutRef.current);
252
+ cleanupDelayTimeoutRef.current = null;
253
+ }
254
+
255
+ // Prevent multiple simultaneous connections
256
+ if (
257
+ webSocketRef.current?.readyState === WebSocket.CONNECTING ||
258
+ webSocketRef.current?.readyState === WebSocket.OPEN
259
+ ) {
260
+ return;
261
+ }
262
+
263
+ cleanup();
264
+ isManualDisconnectRef.current = false;
265
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTING);
266
+
267
+ try {
268
+ const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
269
+
270
+ webSocketRef.current = ws;
271
+
272
+ ws.onopen = (event) => {
273
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTED);
274
+ reconnectAttemptsRef.current = 0;
275
+ currentReconnectIntervalRef.current = reconnectInterval;
276
+ setReconnectAttempts(0);
277
+
278
+ onOpenRef.current?.(event);
279
+ };
280
+
281
+ ws.onclose = (event) => {
282
+ setConnectionStatus(WebSocketConnectionStatus.DISCONNECTED);
283
+ clearTimers();
284
+ onCloseRef.current?.(event);
285
+
286
+ // Attempt reconnection if enabled and not manually disconnected
287
+ if (
288
+ shouldReconnectRef.current &&
289
+ !isManualDisconnectRef.current &&
290
+ reconnectAttemptsRef.current < maxReconnectAttempts
291
+ ) {
292
+ reconnectAttemptsRef.current += 1;
293
+ setReconnectAttempts(reconnectAttemptsRef.current);
294
+
295
+ const delay = Math.min(
296
+ currentReconnectIntervalRef.current,
297
+ maxReconnectInterval,
298
+ );
299
+
300
+ reconnectTimeoutRef.current = setTimeout(() => {
301
+ currentReconnectIntervalRef.current *= reconnectBackoffMultiplier;
302
+ connect();
303
+ }, delay);
304
+ }
305
+ };
306
+
307
+ ws.onerror = (event) => {
308
+ setConnectionStatus(WebSocketConnectionStatus.ERROR);
309
+ onErrorRef.current?.(event);
310
+ };
311
+
312
+ ws.onmessage = (event) => {
313
+ setLastMessage(event);
314
+
315
+ // Attempt to parse JSON data for typed message
316
+ try {
317
+ const parsed =
318
+ typeof event.data === "string"
319
+ ? JSON.parse(event.data)
320
+ : event.data;
321
+ setLastParsedMessage(parsed as TOutput);
322
+ } catch (error) {
323
+ // If parsing fails, set parsed message to null
324
+ setLastParsedMessage(null);
325
+ }
326
+
327
+ onMessageRef.current?.(event);
328
+ };
329
+ } catch (error) {
330
+ setConnectionStatus(WebSocketConnectionStatus.ERROR);
331
+ console.error("WebSocket connection error:", error);
332
+ }
333
+ }, [
334
+ url,
335
+ protocols,
336
+ reconnectInterval,
337
+ maxReconnectInterval,
338
+ maxReconnectAttempts,
339
+ reconnectBackoffMultiplier,
340
+ cleanup,
341
+ clearTimers,
342
+ ]);
343
+
344
+ // Disconnect from WebSocket
345
+ const disconnect = useCallback(() => {
346
+ isManualDisconnectRef.current = true;
347
+ cleanup();
348
+ setConnectionStatus(WebSocketConnectionStatus.DISCONNECTED);
349
+ reconnectAttemptsRef.current = 0;
350
+ setReconnectAttempts(0);
351
+ currentReconnectIntervalRef.current = reconnectInterval;
352
+ }, [cleanup, reconnectInterval]);
353
+
354
+ // Send typed message
355
+ const sendMessage = useCallback((message: TInput) => {
356
+ if (webSocketRef.current?.readyState === WebSocket.OPEN) {
357
+ const messageStr =
358
+ typeof message === "string" ? message : JSON.stringify(message);
359
+ webSocketRef.current.send(messageStr);
360
+ } else {
361
+ console.warn("WebSocket is not connected. Message not sent:", message);
362
+ }
363
+ }, []);
364
+
365
+ // Send raw string message
366
+ const sendRawMessage = useCallback((message: string) => {
367
+ if (webSocketRef.current?.readyState === WebSocket.OPEN) {
368
+ webSocketRef.current.send(message);
369
+ } else {
370
+ console.warn("WebSocket is not connected. Message not sent:", message);
371
+ }
372
+ }, []);
373
+
374
+ // Connect on mount if enabled
375
+ useEffect(() => {
376
+ isMountedRef.current = true;
377
+
378
+ // Cancel any pending cleanup from previous unmount (Strict Mode case)
379
+ if (cleanupDelayTimeoutRef.current) {
380
+ clearTimeout(cleanupDelayTimeoutRef.current);
381
+ cleanupDelayTimeoutRef.current = null;
382
+ }
383
+
384
+ // If connection exists and is open/connecting, reuse it
385
+ if (
386
+ webSocketRef.current &&
387
+ (webSocketRef.current.readyState === WebSocket.OPEN ||
388
+ webSocketRef.current.readyState === WebSocket.CONNECTING)
389
+ ) {
390
+ // Connection already exists, just update status if needed
391
+ if (webSocketRef.current.readyState === WebSocket.OPEN) {
392
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTED);
393
+ } else {
394
+ setConnectionStatus(WebSocketConnectionStatus.CONNECTING);
395
+ }
396
+ } else if (connectOnMount && url) {
397
+ // No existing connection, create new one
398
+ connect();
399
+ }
400
+
401
+ // Delayed cleanup on unmount to handle Strict Mode
402
+ return () => {
403
+ isMountedRef.current = false;
404
+
405
+ // Delay cleanup by 100ms to handle Strict Mode remounting
406
+ cleanupDelayTimeoutRef.current = setTimeout(() => {
407
+ // Only cleanup if component hasn't remounted
408
+ if (!isMountedRef.current) {
409
+ isManualDisconnectRef.current = true;
410
+ cleanup();
411
+ }
412
+ }, 100);
413
+ };
414
+ // eslint-disable-next-line react-hooks/exhaustive-deps
415
+ }, [url, connectOnMount]);
416
+
417
+ return {
418
+ sendMessage,
419
+ sendRawMessage,
420
+ lastRawMessage,
421
+ lastMessage,
422
+ connectionStatus,
423
+ connect,
424
+ disconnect,
425
+ isConnected: connectionStatus === WebSocketConnectionStatus.CONNECTED,
426
+ reconnectAttempts,
427
+ };
428
+ }