cohorta/src/services/websocketServiceNew.ts
Vadym Samoilenko e01569c412
All checks were successful
Deploy to Production / deploy (push) Successful in 2m23s
feat: commit all app changes — billing API, new auth, design overhaul
Includes frontend redesign (Navigation, billingApi), backend updates
(auth routes, admin routes, LLM service refactor), MSAL removal,
and dependency updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 19:04:43 +01:00

182 lines
5.1 KiB
TypeScript
Executable file

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