semblance_backup/src/services/websocketServiceNew.ts
2025-08-10 18:08:34 -05:00

298 lines
No EOL
10 KiB
TypeScript

/**
* 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;
}