// Define the runtime configuration interface interface RuntimeConfig { API_URL: string; } declare global { interface Window { runtimeConfig?: RuntimeConfig; } } class WebSocketService { private ws: WebSocket | null = null; private messageHandlers: ((message: any) => void)[] = []; private errorHandlers: ((error: Event) => void)[] = []; private sessionTerminatedHandlers: (() => void)[] = []; private currentSessionId: string | null = null; private currentClientId: string | null = null; private heartbeatInterval: NodeJS.Timeout | null = null; connect(sessionId: string, clientId: string) { // Prevent multiple connections if (this.ws) { return; } this.currentSessionId = sessionId; this.currentClientId = clientId; // Read the API_URL from the runtime configuration const apiUrl = window.runtimeConfig?.API_URL || 'ws://localhost:8000'; const wsUrl = `${apiUrl.replace(/^http/, 'ws')}/sessions/${sessionId}`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); // Send JOIN_SESSION message on open to inform the server of the client and session IDs this.sendMessage({ type: 'JOIN_SESSION', payload: { clientId: this.currentClientId, sessionId: this.currentSessionId } }); // Start heartbeat to keep connection alive this.heartbeatInterval = setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'PING', clientId: this.currentClientId, sessionId: this.currentSessionId })); } }, 30000); // Send ping every 30 seconds }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log('WebSocketService: Received and parsed message:', message); this.messageHandlers.forEach(handler => handler(message)); } catch (error) { console.error('Error parsing incoming message:', error); } }; this.ws.onclose = () => { console.log('WebSocket disconnected'); if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } this.sessionTerminatedHandlers.forEach(handler => handler()); this.ws = null; this.currentSessionId = null; this.currentClientId = null; }; this.ws.onerror = (event) => { console.error('WebSocket error:', event); if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } this.errorHandlers.forEach(handler => handler(event)); }; } disconnect() { if (this.ws) { this.ws.close(); } if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } sendMessage(message: any) { if (this.ws && this.ws.readyState === WebSocket.OPEN && this.currentClientId && this.currentSessionId) { const messageToSend = { ...message, clientId: this.currentClientId, sessionId: this.currentSessionId, }; this.ws.send(JSON.stringify(messageToSend)); } else { // This error can be ignored if it happens during initial connection in StrictMode // console.error('WebSocket is not connected or clientId/sessionId is missing.'); } } onMessage(handler: (message: any) => void) { this.messageHandlers.push(handler); } removeMessageHandler(handler: (message: any) => void) { this.messageHandlers = this.messageHandlers.filter(h => h !== handler); } onError(handler: (error: Event) => void) { this.errorHandlers.push(handler); } removeErrorHandler(handler: (error: Event) => void) { this.errorHandlers = this.errorHandlers.filter(h => h !== handler); } onSessionTerminated(handler: () => void) { this.sessionTerminatedHandlers.push(handler); } removeSessionTerminatedHandler(handler: () => void) { this.sessionTerminatedHandlers = this.sessionTerminatedHandlers.filter(h => h !== handler); } } export const webSocketService = new WebSocketService();