interface WebSocketMessage { type: string; data: any; } interface WebSocketService { connect: () => Promise; disconnect: () => void; send: (message: any) => void; onMessage: (callback: (message: WebSocketMessage) => void) => void; onError: (callback: (error: Event) => void) => void; onOpen: (callback: () => void) => void; onClose: (callback: () => void) => void; isConnected: () => boolean; on: (event: string, callback: (...args: any[]) => void) => void; // EventEmitter-style API off: (event: string, callback: (...args: any[]) => void) => void; connectChat?: () => Promise; // Alias pour compatibilité ChatInterface joinConversation: (conversationId: string) => void; leaveConversation: (conversationId: string) => void; sendMessage: ( conversationId: string, content: string, parentId?: string, ) => void; startTyping: (conversationId: string) => void; stopTyping: (conversationId: string) => void; addReaction: (messageId: string, emoji: string) => void; removeReaction: (messageId: string, emoji: string) => void; } import { logger } from '@/utils/logger'; import { TokenStorage } from '@/services/tokenStorage'; class WebSocketServiceImpl implements WebSocketService { private ws: WebSocket | null = null; private messageHandlers: Array<(message: WebSocketMessage) => void> = []; private errorHandlers: Array<(error: Event) => void> = []; private openHandlers: Array<() => void> = []; private closeHandlers: Array<() => void> = []; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private reconnectDelay = 3000; async connect(): Promise { if (this.ws && this.ws.readyState === WebSocket.OPEN) { return; } // CORRECTION: Le serveur Rust expose le WebSocket sur /ws et requiert token en query (v0.101) const baseUrl = (() => { const url = import.meta.env.VITE_WS_URL; if (!url) { if (import.meta.env.PROD) { throw new Error('VITE_WS_URL must be defined in production'); } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws`; } if (url.startsWith('/')) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}${url}`; } return url; })(); const token = TokenStorage.getAccessToken(); const wsUrl = token ? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` : baseUrl; return new Promise((resolve, reject) => { try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { this.reconnectAttempts = 0; this.openHandlers.forEach((handler) => handler()); resolve(); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.messageHandlers.forEach((handler) => handler(message)); } catch (error) { logger.error('Failed to parse WebSocket message:', { error }); } }; this.ws.onerror = (error) => { this.errorHandlers.forEach((handler) => handler(error)); // CORRECTION: Ne pas rejeter immédiatement, attendre onclose // Ne pas logger l'erreur ici car onclose sera appelé après avec plus de détails // reject(error); }; this.ws.onclose = (_event) => { this.closeHandlers.forEach((handler) => handler()); // CORRECTION DURABLE: Reconnexion intelligente avec backoff exponentiel // Ne pas spammer la console si le serveur est down if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // Backoff exponentiel // Afficher un message seulement pour les premières tentatives ou si c'est la dernière if (this.reconnectAttempts === 1) { logger.warn( `[WebSocket] Connection closed. Chat server unavailable. Retrying in ${delay / 1000}s...`, ); } else if (this.reconnectAttempts === this.maxReconnectAttempts) { logger.error( `[WebSocket] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Chat server unavailable at ${wsUrl}. Please ensure the chat server is running.`, ); } setTimeout(() => { this.connect().catch(() => { // Silently fail, retry will happen via reconnect logic }); }, delay); } else { logger.error( `[WebSocket] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Chat server unavailable at ${wsUrl}. Please ensure the chat server is running on port 8081.`, ); reject( new Error('WebSocket connection failed after maximum retries'), ); } }; } catch (error) { reject(error); } }); } disconnect(): void { if (this.ws) { this.ws.close(); this.ws = null; } } send(message: any): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } else { logger.error('WebSocket is not connected'); } } onMessage(callback: (message: WebSocketMessage) => void): void { this.messageHandlers.push(callback); } onError(callback: (error: Event) => void): void { this.errorHandlers.push(callback); } onOpen(callback: () => void): void { this.openHandlers.push(callback); } onClose(callback: () => void): void { this.closeHandlers.push(callback); } isConnected(): boolean { return this.ws !== null && this.ws.readyState === WebSocket.OPEN; } removeMessageHandler(callback: (message: WebSocketMessage) => void): void { const index = this.messageHandlers.indexOf(callback); if (index > -1) { this.messageHandlers.splice(index, 1); } } // CORRECTION DURABLE: Méthode .on() pour compatibilité EventEmitter // Utilisée par ChatInterface et store/chat on(event: string, callback: (...args: any[]) => void): void { switch (event) { case 'message': case 'chat_message': this.onMessage(callback as (message: WebSocketMessage) => void); break; case 'connected': case 'chat_connected': this.onOpen(callback as () => void); break; case 'disconnected': case 'chat_disconnected': this.onClose(callback as () => void); break; case 'error': case 'chat_error': this.onError(callback as (error: Event) => void); break; default: logger.warn(`[WebSocket] Event '${event}' not supported`); } } off(event: string, callback: (...args: any[]) => void): void { switch (event) { case 'message': case 'chat_message': this.removeMessageHandler( callback as (message: WebSocketMessage) => void, ); break; // NOTE: Removal for other event types (open, close, error) is not implemented // because these handlers are stored in arrays and would require specific removal methods // (removeOpenHandler, removeCloseHandler, removeErrorHandler). This is intentional // as these handlers are typically lifecycle hooks that don't need to be removed. // If removal is needed in the future, implement specific removal methods for each handler type. default: logger.debug(`[WebSocket] Removal for event '${event}' not implemented - handlers are lifecycle hooks`); break; } } connectChat(): Promise { return this.connect(); } joinConversation(conversationId: string): void { this.send({ type: 'join_conversation', conversation_id: conversationId }); } joinRoom(room: string): void { this.joinConversation(room); } leaveConversation(conversationId: string): void { this.send({ type: 'leave_conversation', conversation_id: conversationId }); } sendMessage( conversationId: string, content: string, parentId?: string, ): void { this.send({ type: 'send_message', conversation_id: conversationId, content, parent_id: parentId, }); } startTyping(conversationId: string): void { this.send({ type: 'start_typing', conversation_id: conversationId }); } stopTyping(conversationId: string): void { this.send({ type: 'stop_typing', conversation_id: conversationId }); } addReaction(messageId: string, emoji: string): void { this.send({ type: 'add_reaction', message_id: messageId, emoji }); } removeReaction(messageId: string, emoji: string): void { this.send({ type: 'remove_reaction', message_id: messageId, emoji }); } } export const websocketService = new WebSocketServiceImpl(); // CORRECTION DURABLE: Export alias pour compatibilité avec imports existants // Certains fichiers utilisent 'wsService', d'autres 'websocketService' export const wsService = websocketService; // Helper hook for React components export function useWebSocket() { const connect = () => websocketService.connect(); const disconnect = () => websocketService.disconnect(); const send = (message: any) => websocketService.send(message); const isConnected = () => websocketService.isConnected(); return { connect, disconnect, send, isConnected }; }