219 lines
6.1 KiB
TypeScript
Raw Permalink Normal View History

2025-10-07 10:59:52 +01:00
import { useEffect, useRef, useState, useCallback } from 'react';
import { AuditLogEntry, WebSocketMessage } from '@/types/audit';
export enum WebSocketReadyState {
CONNECTING = 0,
OPEN = 1,
CLOSING = 2,
CLOSED = 3,
}
interface UseWebSocketOptions {
url: string;
onMessage?: (message: WebSocketMessage) => void;
onNewAuditLog?: (log: AuditLogEntry) => void;
onComplianceAlert?: (alert: any) => void;
onSystemStatus?: (status: any) => void;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
shouldReconnect?: boolean;
reconnectInterval?: number;
maxReconnectAttempts?: number;
protocols?: string | string[];
}
interface UseWebSocketReturn {
readyState: WebSocketReadyState;
lastMessage: WebSocketMessage | null;
lastJsonMessage: any;
sendMessage: (message: string) => void;
sendJsonMessage: (message: object) => void;
connectionStatus: 'Connecting' | 'Open' | 'Closing' | 'Closed';
isConnected: boolean;
reconnect: () => void;
close: () => void;
reconnectAttempts: number;
}
export const useWebSocket = (options: UseWebSocketOptions): UseWebSocketReturn => {
const {
url,
onMessage,
onNewAuditLog,
onComplianceAlert,
onSystemStatus,
onOpen,
onClose,
onError,
shouldReconnect = true,
reconnectInterval = 3000,
maxReconnectAttempts = 10,
protocols
} = options;
const [readyState, setReadyState] = useState<WebSocketReadyState>(WebSocketReadyState.CONNECTING);
const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null);
const [lastJsonMessage, setLastJsonMessage] = useState<any>(null);
const [reconnectAttempts, setReconnectAttempts] = useState(0);
const websocketRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const shouldReconnectRef = useRef(shouldReconnect);
const urlRef = useRef(url);
// Update refs when props change
useEffect(() => {
shouldReconnectRef.current = shouldReconnect;
}, [shouldReconnect]);
useEffect(() => {
urlRef.current = url;
}, [url]);
const connect = useCallback(() => {
try {
const ws = new WebSocket(url, protocols);
websocketRef.current = ws;
ws.onopen = (event) => {
setReadyState(WebSocketReadyState.OPEN);
setReconnectAttempts(0);
onOpen?.(event);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as WebSocketMessage;
setLastMessage(data);
setLastJsonMessage(data.data);
// Route messages to specific handlers
switch (data.type) {
case 'new_audit_log':
onNewAuditLog?.(data.data as AuditLogEntry);
break;
case 'compliance_alert':
onComplianceAlert?.(data.data);
break;
case 'system_status':
onSystemStatus?.(data.data);
break;
case 'heartbeat':
// Handle heartbeat silently
break;
default:
console.warn('Unknown WebSocket message type:', data.type);
}
onMessage?.(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.onclose = (event) => {
setReadyState(WebSocketReadyState.CLOSED);
websocketRef.current = null;
onClose?.(event);
// Attempt to reconnect if enabled
if (shouldReconnectRef.current && reconnectAttempts < maxReconnectAttempts) {
setReconnectAttempts(prev => prev + 1);
reconnectTimeoutRef.current = setTimeout(() => {
connect();
}, reconnectInterval);
}
};
ws.onerror = (event) => {
setReadyState(WebSocketReadyState.CLOSED);
onError?.(event);
};
// Set initial connecting state
setReadyState(WebSocketReadyState.CONNECTING);
} catch (error) {
console.error('Failed to create WebSocket connection:', error);
setReadyState(WebSocketReadyState.CLOSED);
}
}, [url, protocols, onOpen, onClose, onError, onMessage, onNewAuditLog, onComplianceAlert, onSystemStatus, reconnectInterval, maxReconnectAttempts, reconnectAttempts]);
const sendMessage = useCallback((message: string) => {
if (websocketRef.current?.readyState === WebSocketReadyState.OPEN) {
websocketRef.current.send(message);
} else {
console.warn('WebSocket is not connected. Message not sent:', message);
}
}, []);
const sendJsonMessage = useCallback((message: object) => {
sendMessage(JSON.stringify(message));
}, [sendMessage]);
const reconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (websocketRef.current) {
websocketRef.current.close();
}
setReconnectAttempts(0);
connect();
}, [connect]);
const close = useCallback(() => {
shouldReconnectRef.current = false;
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (websocketRef.current) {
websocketRef.current.close();
}
}, []);
// Initial connection and cleanup
useEffect(() => {
connect();
return () => {
shouldReconnectRef.current = false;
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (websocketRef.current) {
websocketRef.current.close();
}
};
}, [connect]);
const connectionStatus = (() => {
switch (readyState) {
case WebSocketReadyState.CONNECTING:
return 'Connecting';
case WebSocketReadyState.OPEN:
return 'Open';
case WebSocketReadyState.CLOSING:
return 'Closing';
case WebSocketReadyState.CLOSED:
return 'Closed';
default:
return 'Closed';
}
})();
return {
readyState,
lastMessage,
lastJsonMessage,
sendMessage,
sendJsonMessage,
connectionStatus,
isConnected: readyState === WebSocketReadyState.OPEN,
reconnect,
close,
reconnectAttempts,
};
2026-01-12 05:03:09 +00:00
};