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:
syntaxbullet
2026-04-02 13:25:52 +02:00
parent aa145592c5
commit eb7dfaf6f5

View 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 };
}