105 lines
2.6 KiB
TypeScript
105 lines
2.6 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
export interface RealtimeLog {
|
|
level: string;
|
|
message: string;
|
|
}
|
|
|
|
interface ConnectOptions {
|
|
clear?: boolean;
|
|
onClose?: () => void;
|
|
onError?: () => void;
|
|
onResult?: () => void;
|
|
}
|
|
|
|
const MAX_LOGS = 1000;
|
|
|
|
function toWebSocketUrl(pathOrUrl: string): string {
|
|
if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) {
|
|
return pathOrUrl;
|
|
}
|
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
const path = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`;
|
|
return `${protocol}://${window.location.host}${path}`;
|
|
}
|
|
|
|
export function useWebSocketLogs() {
|
|
const [logs, setLogs] = useState<RealtimeLog[]>([]);
|
|
const [connected, setConnected] = useState(false);
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
const callbacksRef = useRef<ConnectOptions>({});
|
|
const suppressCloseRef = useRef(false);
|
|
|
|
const clearLogs = useCallback(() => {
|
|
setLogs([]);
|
|
}, []);
|
|
|
|
const close = useCallback((notify = false) => {
|
|
if (!wsRef.current) {
|
|
setConnected(false);
|
|
return;
|
|
}
|
|
suppressCloseRef.current = !notify;
|
|
wsRef.current.close();
|
|
wsRef.current = null;
|
|
setConnected(false);
|
|
}, []);
|
|
|
|
const connect = useCallback((pathOrUrl: string, options: ConnectOptions = {}) => {
|
|
close(false);
|
|
suppressCloseRef.current = false;
|
|
callbacksRef.current = options;
|
|
if (options.clear ?? true) {
|
|
setLogs([]);
|
|
}
|
|
|
|
const ws = new WebSocket(toWebSocketUrl(pathOrUrl));
|
|
wsRef.current = ws;
|
|
setConnected(true);
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data) as RealtimeLog;
|
|
if (msg.level === 'heartbeat') return;
|
|
if (msg.level === 'result') {
|
|
callbacksRef.current.onResult?.();
|
|
return;
|
|
}
|
|
setLogs((prev) => [...prev, msg].slice(-MAX_LOGS));
|
|
} catch {
|
|
setLogs((prev) => [...prev, { level: 'info', message: String(event.data) }].slice(-MAX_LOGS));
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
const isCurrent = wsRef.current === ws;
|
|
const shouldNotify = isCurrent && !suppressCloseRef.current;
|
|
if (isCurrent) {
|
|
wsRef.current = null;
|
|
setConnected(false);
|
|
suppressCloseRef.current = false;
|
|
}
|
|
if (shouldNotify) {
|
|
callbacksRef.current.onClose?.();
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
setConnected(false);
|
|
callbacksRef.current.onError?.();
|
|
};
|
|
}, [close]);
|
|
|
|
useEffect(() => () => {
|
|
close(false);
|
|
}, [close]);
|
|
|
|
return {
|
|
logs,
|
|
connected,
|
|
clearLogs,
|
|
connect,
|
|
close,
|
|
};
|
|
}
|