/** * Singleton WebSocket Service */ import { io, Socket } from "socket.io-client"; const BASE_URL = import.meta.env.DEV ? "http://localhost:5137" : (import.meta.env.VITE_WEBSOCKET_URL || window.location.origin); const SOCKET_PATH = import.meta.env.VITE_WEBSOCKET_PATH || "/semblance_back/socket.io/"; let socket: Socket | null = null; let currentRoom: string | null = null; let coreListenersBound = false; export function initSocket(getToken: () => string): Socket { if (socket) { socket.io.opts.auth = { token: getToken() }; return socket; } socket = io(BASE_URL, { path: SOCKET_PATH, transports: ["websocket"], reconnection: true, autoConnect: false, timeout: 60000, pingInterval: 45000, pingTimeout: 120000, auth: (cb) => cb({ token: getToken() }), }); socket.io.on("reconnect_attempt", () => { socket!.io.opts.auth = { token: getToken() }; }); socket.on("connect", () => { bindCoreListeners(); if (currentRoom) rejoinRoom(); }); socket.onAny((event, ...args) => { const payload = args[0]; switch (event) { case 'joined_focus_group': window.dispatchEvent(new CustomEvent("ws:joined_focus_group", { detail: payload })); break; case 'left_focus_group': window.dispatchEvent(new CustomEvent("ws:left_focus_group", { detail: payload })); break; case 'message_update': window.dispatchEvent(new CustomEvent("ws:message_update", { detail: payload })); break; case 'ai_status_update': window.dispatchEvent(new CustomEvent("ws:ai_status_update", { detail: payload })); break; case 'moderator_status_update': window.dispatchEvent(new CustomEvent("ws:moderator_status_update", { detail: payload })); break; case 'theme_update': window.dispatchEvent(new CustomEvent("ws:theme_update", { detail: payload })); break; case 'focus_group_update': window.dispatchEvent(new CustomEvent("ws:focus_group_update", { detail: payload })); break; case 'mode_event_update': window.dispatchEvent(new CustomEvent("ws:mode_event_update", { detail: payload })); break; case 'task_started': window.dispatchEvent(new CustomEvent("ws:task_started", { detail: payload })); break; case 'task_cancelled': window.dispatchEvent(new CustomEvent("ws:task_cancelled", { detail: payload })); break; case 'task_completed': window.dispatchEvent(new CustomEvent("ws:task_completed", { detail: payload })); break; case 'task_failed': window.dispatchEvent(new CustomEvent("ws:task_failed", { detail: payload })); break; case 'bulk_export_progress': window.dispatchEvent(new CustomEvent("ws:bulk_export_progress", { detail: payload })); break; case 'quota_warning': window.dispatchEvent(new CustomEvent("quota_warning", { detail: payload })); break; case 'quota_exceeded': window.dispatchEvent(new CustomEvent("quota_exceeded", { detail: payload })); break; case 'error': console.error('[WebSocket] Error:', payload); break; case 'auth_error': console.error('[WebSocket] Auth error'); window.dispatchEvent(new CustomEvent("ws:auth_error", { detail: payload })); break; } }); socket.on("connect_error", (error) => { console.error('[WebSocket] Connect error:', error.message); }); socket.on("disconnect", (_reason) => { }); return socket; } export function connectSocket() { if (socket && !socket.connected) { socket.connect(); } } export function disconnectSocket() { if (socket) { socket.disconnect(); currentRoom = null; } } export function joinFocusGroup(focus_group_id: string, ack?: (resp: any) => void) { currentRoom = focus_group_id; if (!socket?.connected) { connectSocket(); setTimeout(() => { if (socket?.connected) { socket.emit("join_focus_group", { focus_group_id }, (resp: any) => ack?.(resp)); } }, 1000); return; } socket.emit("join_focus_group", { focus_group_id }, (resp: any) => ack?.(resp)); } export function leaveFocusGroup(focus_group_id: string) { if (currentRoom === focus_group_id) { currentRoom = null; } if (socket?.connected) { socket.emit("leave_focus_group", { focus_group_id }); } } function rejoinRoom() { if (!socket?.connected || !currentRoom) return; socket.emit("join_focus_group", { focus_group_id: currentRoom }); } function bindCoreListeners() { if (!socket) return; // Already bound — listeners are re-added via onAny, no need to duplicate if (coreListenersBound) return; socket.on("auth_error", (payload: any) => { console.error('[WebSocket] Auth error'); window.dispatchEvent(new CustomEvent("ws:auth_error", { detail: payload })); }); socket.on("error", (error: any) => { console.error('[WebSocket] Socket error:', error?.message || error); }); coreListenersBound = true; } export function getSocket() { return socket; } export function getSocketId() { return socket?.id; } export function isConnected() { return socket?.connected ?? false; }