Files
unisono/frontend/src/services/websocket.ts

132 lines
4.5 KiB
TypeScript

// 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();