/** * GPT-5 Singleton WebSocket Service * Fixes listener binding issues by ensuring stable socket instance and re-binding on every connect */ 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 = (() => { if (import.meta.env.DEV) return "/socket.io/"; const urlParams = new URLSearchParams(window.location.search); return urlParams.get("direct") === "1" ? "/socket.io/" : "/semblance_back/socket.io/"; })(); let socket: Socket | null = null; let currentRoom: string | null = null; let coreListenersBound = false; // GPT-5 FIX: Pass token getter function to ensure fresh tokens on reconnect export function initSocket(getToken: () => string): Socket { if (socket) { // Keep token fresh for future reconnects socket.io.opts.auth = { token: getToken() }; return socket; } console.log('🔧 [GPT-5] Creating singleton socket:', BASE_URL, SOCKET_PATH); socket = io(BASE_URL, { path: SOCKET_PATH, transports: ["websocket"], reconnection: true, autoConnect: false, timeout: 60000, pingInterval: 45000, pingTimeout: 120000, // Using auth callback guarantees latest token on every (re)connect auth: (cb) => cb({ token: getToken() }), }); // Always refresh token before reconnect attempts socket.io.on("reconnect_attempt", () => { console.log('🔧 [GPT-5] Reconnect attempt - refreshing token'); socket!.io.opts.auth = { token: getToken() }; }); // GPT-5 CRITICAL: Bind listeners on EVERY connect (initial + reconnects) const onConnected = () => { console.log('🔧 [GPT-5] Socket connected, rebinding listeners and rejoining room'); bindCoreListeners(); // idempotent if (currentRoom) rejoinRoom(); // keep room after reconnect }; socket.on("connect", onConnected); // GPT-5 FIX: Route all events through onAny since specific listeners are broken socket.onAny((event, ...args) => { console.log(`🔧 [GPT-5 onAny] ${event}:`, args); // Route to specific handlers manually since socket.on() doesn't work const payload = args[0]; // First argument is always the payload switch (event) { case 'joined_focus_group': console.log('🔧 [GPT-5] *** ROUTING joined_focus_group from onAny ***'); window.dispatchEvent(new CustomEvent("ws:joined_focus_group", { detail: payload })); break; case 'left_focus_group': console.log('🔧 [GPT-5] *** ROUTING left_focus_group from onAny ***'); window.dispatchEvent(new CustomEvent("ws:left_focus_group", { detail: payload })); break; case 'message_update': console.log('🔧 [GPT-5] *** ROUTING message_update from onAny ***'); try { window.dispatchEvent(new CustomEvent("ws:message_update", { detail: payload })); console.log('🔧 [GPT-5] DISPATCHED window event ws:message_update SUCCESS (via onAny)'); } catch (error) { console.error('🔧 [GPT-5] ERROR dispatching window event (via onAny):', error); } break; case 'ai_status_update': console.log('🔧 [GPT-5] *** ROUTING ai_status_update from onAny ***'); window.dispatchEvent(new CustomEvent("ws:ai_status_update", { detail: payload })); break; case 'moderator_status_update': console.log('🔧 [GPT-5] *** ROUTING moderator_status_update from onAny ***'); window.dispatchEvent(new CustomEvent("ws:moderator_status_update", { detail: payload })); break; case 'theme_update': console.log('🔧 [GPT-5] *** ROUTING theme_update from onAny ***'); window.dispatchEvent(new CustomEvent("ws:theme_update", { detail: payload })); break; case 'focus_group_update': console.log('🔧 [GPT-5] *** ROUTING focus_group_update from onAny ***'); window.dispatchEvent(new CustomEvent("ws:focus_group_update", { detail: payload })); break; case 'connected': console.log('🔧 [GPT-5] *** ROUTING connected from onAny ***'); // Handle connected events if needed break; case 'error': console.error('🔧 [GPT-5] *** ROUTING error from onAny ***', payload); break; } }); // Connection debugging socket.on("connect_error", (error) => { console.error('🔧 [GPT-5] Connect error:', error); }); socket.on("disconnect", (reason) => { console.log('🔧 [GPT-5] Disconnected:', reason); }); return socket; } export function connectSocket() { if (socket && !socket.connected) { console.log('🔧 [GPT-5] Connecting socket'); socket.connect(); } } export function disconnectSocket() { if (socket) { console.log('🔧 [GPT-5] Disconnecting socket'); socket.disconnect(); currentRoom = null; } } export function joinFocusGroup(focus_group_id: string, ack?: (resp: any) => void) { console.log('🔧 [GPT-5] Joining focus group:', focus_group_id); currentRoom = focus_group_id; if (!socket?.connected) { console.log('🔧 [GPT-5] Socket not connected, will auto-rejoin on connect'); // Force connection and try again connectSocket(); setTimeout(() => { if (socket?.connected) { console.log('🔧 [GPT-5] Retrying join after connection established'); socket.emit("join_focus_group", { focus_group_id }, (resp: any) => { console.log('🔧 [GPT-5] join_focus_group RETRY ACK:', resp); ack?.(resp); }); } else { console.log('🔧 [GPT-5] Still not connected, will rejoin on next connect event'); } }, 1000); return; } socket.emit("join_focus_group", { focus_group_id }, (resp: any) => { console.log('🔧 [GPT-5] join_focus_group ACK:', resp); ack?.(resp); }); } export function leaveFocusGroup(focus_group_id: string) { console.log('🔧 [GPT-5] Leaving focus group:', focus_group_id); if (currentRoom === focus_group_id) { currentRoom = null; } if (socket?.connected) { socket.emit("leave_focus_group", { focus_group_id }); } } // GPT-5 FIX: Auto-rejoin room on reconnect function rejoinRoom() { if (!socket?.connected || !currentRoom) return; console.log('🔧 [GPT-5] Auto-rejoining room after reconnect:', currentRoom); socket.emit("join_focus_group", { focus_group_id: currentRoom }); } // GPT-5 CRITICAL: Ensure we never lose handlers across reconnects or AI mode toggles function bindCoreListeners() { if (!socket) { console.log('🔧 [GPT-5] bindCoreListeners called but socket is null!'); return; } // GPT-5 FIX: Always rebind listeners on reconnect - don't let flag prevent rebinding if (coreListenersBound) { console.log('🔧 [GPT-5] Listeners already bound, but rebinding anyway for safety'); } console.log('🔧 [GPT-5] bindCoreListeners called - socket exists, binding listeners'); // IMPORTANT: Use stable function references so off() later removes exactly these const onJoined = (payload: any) => { console.log('🔧 [GPT-5] joined_focus_group:', payload); window.dispatchEvent(new CustomEvent("ws:joined_focus_group", { detail: payload })); }; const onLeft = (payload: any) => { console.log('🔧 [GPT-5] left_focus_group:', payload); window.dispatchEvent(new CustomEvent("ws:left_focus_group", { detail: payload })); }; const onMsg = (payload: any) => { console.log('🔧 [GPT-5] *** MESSAGE_UPDATE LISTENER FIRED! ***'); console.log('🔧 [GPT-5] message_update payload:', payload); console.log('🔧 [GPT-5] DISPATCHING window event ws:message_update'); try { window.dispatchEvent(new CustomEvent("ws:message_update", { detail: payload })); console.log('🔧 [GPT-5] DISPATCHED window event ws:message_update SUCCESS'); } catch (error) { console.error('🔧 [GPT-5] ERROR dispatching window event:', error); } }; const onAI = (payload: any) => { console.log('🔧 [GPT-5] ai_status_update:', payload); console.log('🔧 [GPT-5] DISPATCHING window event ws:ai_status_update'); window.dispatchEvent(new CustomEvent("ws:ai_status_update", { detail: payload })); console.log('🔧 [GPT-5] DISPATCHED window event ws:ai_status_update'); }; const onMod = (payload: any) => { console.log('🔧 [GPT-5] moderator_status_update:', payload); window.dispatchEvent(new CustomEvent("ws:moderator_status_update", { detail: payload })); }; const onTheme = (payload: any) => { console.log('🔧 [GPT-5] theme_update:', payload); window.dispatchEvent(new CustomEvent("ws:theme_update", { detail: payload })); }; const onFG = (payload: any) => { console.log('🔧 [GPT-5] focus_group_update:', payload); window.dispatchEvent(new CustomEvent("ws:focus_group_update", { detail: payload })); }; // Bind all listeners console.log('🔧 [GPT-5] BINDING specific listeners to socket'); socket.on("joined_focus_group", onJoined); socket.on("left_focus_group", onLeft); socket.on("message_update", onMsg); socket.on("ai_status_update", onAI); socket.on("moderator_status_update", onMod); socket.on("theme_update", onTheme); socket.on("focus_group_update", onFG); console.log('🔧 [GPT-5] BOUND specific listeners to socket'); // GPT-5 DEBUG: Verify listeners are actually attached console.log('🔧 [GPT-5] Socket listeners after binding:', socket.listeners('message_update').length); console.log('🔧 [GPT-5] Socket hasListeners message_update:', socket.hasListeners('message_update')); // GPT-5 TEST: Emit a test event to ourselves to verify binding setTimeout(() => { if (socket?.connected) { console.log('🔧 [GPT-5] SELF-TEST: Emitting test event'); (socket as any).emit('message_update', { test: 'self-emit-test' }); } }, 1000); // Handle connection events socket.on("connected", (payload: any) => { console.log('🔧 [GPT-5] connected:', payload); }); socket.on("error", (error: any) => { console.error('🔧 [GPT-5] socket error:', error); }); coreListenersBound = true; } // Utility to get current socket (for debugging) export function getSocket() { return socket; } export function getSocketId() { return socket?.id; } export function isConnected() { return socket?.connected ?? false; }