298 lines
No EOL
10 KiB
TypeScript
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;
|
|
} |