feat(panel): add shared useWebSocket hook with reconnection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
100
panel/src/lib/useWebSocket.ts
Normal file
100
panel/src/lib/useWebSocket.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useRef, useCallback, useState } from "react";
|
||||
|
||||
type MessageHandler = (data: any) => void;
|
||||
|
||||
let globalWs: WebSocket | null = null;
|
||||
let globalHandlers = new Set<MessageHandler>();
|
||||
let globalConnected = false;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectAttempt = 0;
|
||||
|
||||
function getWsUrl(): string {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}/ws`;
|
||||
}
|
||||
|
||||
function connect(): void {
|
||||
if (globalWs?.readyState === WebSocket.OPEN || globalWs?.readyState === WebSocket.CONNECTING) return;
|
||||
|
||||
const ws = new WebSocket(getWsUrl());
|
||||
|
||||
ws.onopen = () => {
|
||||
globalConnected = true;
|
||||
reconnectAttempt = 0;
|
||||
globalHandlers.forEach(h => h({ type: "__WS_CONNECTED" }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
globalHandlers.forEach(h => h(data));
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
globalConnected = false;
|
||||
globalWs = null;
|
||||
globalHandlers.forEach(h => h({ type: "__WS_DISCONNECTED" }));
|
||||
|
||||
const delay = Math.min(1000 * 2 ** reconnectAttempt, 30000);
|
||||
reconnectAttempt++;
|
||||
reconnectTimer = setTimeout(connect, delay);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
|
||||
globalWs = ws;
|
||||
}
|
||||
|
||||
function disconnect(): void {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
globalWs?.close();
|
||||
globalWs = null;
|
||||
globalConnected = false;
|
||||
}
|
||||
|
||||
export function useWebSocket() {
|
||||
const [connected, setConnected] = useState(globalConnected);
|
||||
const refCount = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
refCount.current++;
|
||||
if (refCount.current === 1 && !globalWs) {
|
||||
connect();
|
||||
}
|
||||
|
||||
const handler: MessageHandler = (data) => {
|
||||
if (data.type === "__WS_CONNECTED") setConnected(true);
|
||||
if (data.type === "__WS_DISCONNECTED") setConnected(false);
|
||||
};
|
||||
globalHandlers.add(handler);
|
||||
|
||||
return () => {
|
||||
globalHandlers.delete(handler);
|
||||
refCount.current--;
|
||||
if (refCount.current === 0) {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const send = useCallback((data: unknown) => {
|
||||
if (globalWs?.readyState === WebSocket.OPEN) {
|
||||
globalWs.send(JSON.stringify(data));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const subscribe = useCallback((handler: MessageHandler) => {
|
||||
globalHandlers.add(handler);
|
||||
return () => { globalHandlers.delete(handler); };
|
||||
}, []);
|
||||
|
||||
return { connected, send, subscribe };
|
||||
}
|
||||
Reference in New Issue
Block a user