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.
- package/dist/cli/index.js +24 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/schema/client_side.d.ts.map +1 -1
- package/dist/schema/client_side.js +29 -115
- package/dist/schema/client_side.js.map +1 -1
- package/dist/schema/index.d.ts +3 -14
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/useWebSocket.d.ts +124 -0
- package/dist/schema/useWebSocket.d.ts.map +1 -0
- package/dist/schema/useWebSocket.js +257 -0
- package/dist/schema/useWebSocket.js.map +1 -0
- package/dist/schema/useWebSocket.ts +428 -0
- package/dist/server/index.d.ts +2 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +42 -31
- package/dist/server/index.js.map +1 -1
- package/package.json +12 -9
|
@@ -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
|
+
}
|