import { io, Socket } from "socket.io-client"; /** * Socket event types for type safety */ export interface SocketEvents { // Incoming events (from server) new_message: (message: any) => void; message_read: (data: { messageId: string; readAt: string; readBy: string; }) => void; user_typing: (data: { userId: string; firstName: string; isTyping: boolean; }) => void; conversation_joined: (data: { conversationRoom: string; otherUserId: string; }) => void; // Connection events connect: () => void; disconnect: (reason: string) => void; connect_error: (error: Error) => void; error: (error: Error) => void; } /** * Socket service for managing WebSocket connection * Implements singleton pattern to ensure only one socket instance */ class SocketService { private socket: Socket | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private connectionListeners: Array<(connected: boolean) => void> = []; /** * Get the socket instance URL based on environment */ private getSocketUrl(): string { // Use environment variable or default to localhost:5001 (matches backend) return import.meta.env.VITE_BASE_URL || "http://localhost:5001"; } /** * Initialize and connect to the socket server * Authentication happens via cookies (sent automatically) */ connect(): Socket { if (this.socket?.connected) { return this.socket; } this.socket = io(this.getSocketUrl(), { withCredentials: true, // Send cookies for authentication transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling path: "/socket.io", // Explicit Socket.io path reconnection: true, reconnectionAttempts: this.maxReconnectAttempts, reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 20000, }); // Connection event handlers this.socket.on("connect", () => { this.reconnectAttempts = 0; this.notifyConnectionListeners(true); }); this.socket.on("disconnect", (reason) => { this.notifyConnectionListeners(false); }); this.socket.on("connect_error", (error) => { this.reconnectAttempts++; if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error("[Socket] Max reconnection attempts reached"); this.disconnect(); } }); return this.socket; } /** * Disconnect from the socket server */ disconnect(): void { if (this.socket) { this.socket.disconnect(); this.socket = null; this.notifyConnectionListeners(false); } } /** * Get the current socket instance */ getSocket(): Socket | null { return this.socket; } /** * Check if socket is connected */ isConnected(): boolean { return this.socket?.connected ?? false; } /** * Join a conversation room */ joinConversation(otherUserId: string): void { if (!this.socket?.connected) { console.warn("[Socket] Not connected, cannot join conversation"); return; } this.socket.emit("join_conversation", { otherUserId }); } /** * Leave a conversation room */ leaveConversation(otherUserId: string): void { if (!this.socket?.connected) { return; } this.socket.emit("leave_conversation", { otherUserId }); } /** * Emit typing start event */ emitTypingStart(receiverId: string): void { if (!this.socket?.connected) { return; } this.socket.emit("typing_start", { receiverId }); } /** * Emit typing stop event */ emitTypingStop(receiverId: string): void { if (!this.socket?.connected) { return; } this.socket.emit("typing_stop", { receiverId }); } /** * Emit mark message as read event */ emitMarkMessageRead(messageId: string, senderId: string): void { if (!this.socket?.connected) { return; } this.socket.emit("mark_message_read", { messageId, senderId }); } /** * Listen for new messages */ onNewMessage(callback: (message: any) => void): () => void { if (!this.socket) { console.warn("[Socket] Socket not initialized"); return () => {}; } this.socket.on("new_message", callback); // Return cleanup function return () => { this.socket?.off("new_message", callback); }; } /** * Listen for message read events */ onMessageRead( callback: (data: { messageId: string; readAt: string; readBy: string; }) => void ): () => void { if (!this.socket) { console.warn("[Socket] Socket not initialized"); return () => {}; } this.socket.on("message_read", callback); // Return cleanup function return () => { this.socket?.off("message_read", callback); }; } /** * Listen for typing indicators */ onUserTyping( callback: (data: { userId: string; firstName: string; isTyping: boolean; }) => void ): () => void { if (!this.socket) { console.warn("[Socket] Socket not initialized"); return () => {}; } this.socket.on("user_typing", callback); // Return cleanup function return () => { this.socket?.off("user_typing", callback); }; } /** * Listen for conversation joined event */ onConversationJoined( callback: (data: { conversationRoom: string; otherUserId: string }) => void ): () => void { if (!this.socket) { console.warn("[Socket] Socket not initialized"); return () => {}; } this.socket.on("conversation_joined", callback); // Return cleanup function return () => { this.socket?.off("conversation_joined", callback); }; } /** * Add connection status listener */ addConnectionListener(callback: (connected: boolean) => void): () => void { this.connectionListeners.push(callback); // Immediately notify of current status callback(this.isConnected()); // Return cleanup function return () => { this.connectionListeners = this.connectionListeners.filter( (cb) => cb !== callback ); }; } /** * Notify all connection listeners of status change */ private notifyConnectionListeners(connected: boolean): void { this.connectionListeners.forEach((callback) => { try { callback(connected); } catch (error) { console.error("[Socket] Error in connection listener", error); } }); } /** * Remove all event listeners */ removeAllListeners(): void { if (this.socket) { this.socket.removeAllListeners(); } } } // Export singleton instance export const socketService = new SocketService(); export default socketService;