2025-12-03 21:56:50 +00:00
interface WebSocketMessage {
type : string ;
data : any ;
}
interface WebSocketService {
connect : ( ) = > Promise < void > ;
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
2025-12-13 02:34:34 +00:00
off : ( event : string , callback : ( . . . args : any [ ] ) = > void ) = > void ;
2025-12-03 21:56:50 +00:00
connectChat ? : ( ) = > Promise < void > ; // Alias pour compatibilité ChatInterface
2025-12-13 02:34:34 +00:00
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 ;
2025-12-03 21:56:50 +00:00
}
2026-01-07 10:15:48 +00:00
import { logger } from '@/utils/logger' ;
2025-12-03 21:56:50 +00:00
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 < void > {
if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
return ;
}
// CORRECTION: Le serveur Rust expose le WebSocket sur /ws
2025-12-17 13:07:35 +00:00
const wsUrl = ( ( ) = > {
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' ) ;
}
// Fallback uniquement en développement
return 'ws://127.0.0.1:8081/ws' ;
}
2026-01-15 18:26:53 +00:00
// Convertir URL relative en URL absolue pour WebSocket
if ( url . startsWith ( '/' ) ) {
const protocol = window . location . protocol === 'https:' ? 'wss:' : 'ws:' ;
return ` ${ protocol } // ${ window . location . host } ${ url } ` ;
}
2025-12-17 13:07:35 +00:00
return url ;
} ) ( ) ;
2025-12-13 02:34:34 +00:00
2025-12-03 21:56:50 +00:00
return new Promise ( ( resolve , reject ) = > {
try {
this . ws = new WebSocket ( wsUrl ) ;
this . ws . onopen = ( ) = > {
this . reconnectAttempts = 0 ;
2025-12-13 02:34:34 +00:00
this . openHandlers . forEach ( ( handler ) = > handler ( ) ) ;
2025-12-03 21:56:50 +00:00
resolve ( ) ;
} ;
this . ws . onmessage = ( event ) = > {
try {
const message = JSON . parse ( event . data ) ;
2025-12-13 02:34:34 +00:00
this . messageHandlers . forEach ( ( handler ) = > handler ( message ) ) ;
2025-12-03 21:56:50 +00:00
} catch ( error ) {
2026-01-07 10:15:48 +00:00
logger . error ( 'Failed to parse WebSocket message:' , { error } ) ;
2025-12-03 21:56:50 +00:00
}
} ;
this . ws . onerror = ( error ) = > {
2025-12-13 02:34:34 +00:00
this . errorHandlers . forEach ( ( handler ) = > handler ( error ) ) ;
2025-12-03 21:56:50 +00:00
// 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);
} ;
2025-12-13 02:34:34 +00:00
this . ws . onclose = ( _event ) = > {
this . closeHandlers . forEach ( ( handler ) = > handler ( ) ) ;
2025-12-03 21:56:50 +00:00
// CORRECTION DURABLE: Reconnexion intelligente avec backoff exponentiel
// Ne pas spammer la console si le serveur est down
if ( this . reconnectAttempts < this . maxReconnectAttempts ) {
this . reconnectAttempts ++ ;
2025-12-13 02:34:34 +00:00
const delay =
this . reconnectDelay * Math . pow ( 2 , this . reconnectAttempts - 1 ) ; // Backoff exponentiel
2025-12-03 21:56:50 +00:00
// Afficher un message seulement pour les premières tentatives ou si c'est la dernière
if ( this . reconnectAttempts === 1 ) {
2026-01-07 10:15:48 +00:00
logger . warn (
2025-12-13 02:34:34 +00:00
` [WebSocket] Connection closed. Chat server unavailable. Retrying in ${ delay / 1000 } s... ` ,
) ;
2025-12-03 21:56:50 +00:00
} else if ( this . reconnectAttempts === this . maxReconnectAttempts ) {
2026-01-07 10:15:48 +00:00
logger . error (
2025-12-13 02:34:34 +00:00
` [WebSocket] Max reconnection attempts ( ${ this . maxReconnectAttempts } ) reached. Chat server unavailable at ${ wsUrl } . Please ensure the chat server is running. ` ,
) ;
2025-12-03 21:56:50 +00:00
}
2025-12-13 02:34:34 +00:00
2025-12-03 21:56:50 +00:00
setTimeout ( ( ) = > {
this . connect ( ) . catch ( ( ) = > {
// Silently fail, retry will happen via reconnect logic
} ) ;
} , delay ) ;
} else {
2026-01-07 10:15:48 +00:00
logger . error (
2025-12-13 02:34:34 +00:00
` [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' ) ,
) ;
2025-12-03 21:56:50 +00:00
}
} ;
} 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 {
2026-01-07 10:15:48 +00:00
logger . error ( 'WebSocket is not connected' ) ;
2025-12-03 21:56:50 +00:00
}
}
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 :
2026-01-07 10:15:48 +00:00
logger . warn ( ` [WebSocket] Event ' ${ event } ' not supported ` ) ;
2025-12-03 21:56:50 +00:00
}
}
2025-12-13 02:34:34 +00:00
off ( event : string , callback : ( . . . args : any [ ] ) = > void ) : void {
switch ( event ) {
case 'message' :
case 'chat_message' :
this . removeMessageHandler (
callback as ( message : WebSocketMessage ) = > void ,
) ;
break ;
// TODO: Implement removal for other event types if needed
default :
// No-op for now as we don't track other handlers by reference in the same way
// or they are specific arrays (openHandlers, etc) and we need a removeOpenHandler etc.
break ;
}
}
2025-12-03 21:56:50 +00:00
connectChat ( ) : Promise < void > {
return this . connect ( ) ;
}
2025-12-13 02:34:34 +00:00
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 } ) ;
}
2025-12-03 21:56:50 +00:00
}
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 } ;
}