import { io, Socket } from 'socket.io-client'; /** * WebSocket Client for Real-Time Family Sync * * Features: * - Automatic connection/reconnection with exponential backoff * - Family room management * - Presence indicators * - Activity updates (create/update/delete) * - Connection status monitoring */ export interface WebSocketConfig { url: string; token: string; } export interface PresenceUpdate { onlineUsers: string[]; count: number; } export type WebSocketEventCallback = (data: T) => void; class WebSocketService { private socket: Socket | null = null; private config: WebSocketConfig | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 10; private reconnectDelay = 1000; // Start with 1 second private maxReconnectDelay = 30000; // Max 30 seconds private currentFamilyId: string | null = null; private eventListeners: Map> = new Map(); private connectionStatusListeners: Set<(connected: boolean) => void> = new Set(); constructor() {} /** * Connect to WebSocket server */ connect(config: WebSocketConfig): void { if (this.socket?.connected) { console.log('[WebSocket] Already connected'); return; } console.log('[WebSocket] Connecting to:', config.url); console.log('[WebSocket] Token length:', config.token?.length || 0); this.config = config; this.socket = io(config.url, { auth: { token: config.token, }, transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: this.maxReconnectAttempts, reconnectionDelay: this.reconnectDelay, reconnectionDelayMax: this.maxReconnectDelay, secure: config.url.startsWith('https'), rejectUnauthorized: false, // For development with self-signed certs path: '/socket.io/', // Explicit path for Socket.IO withCredentials: true, extraHeaders: { 'Authorization': `Bearer ${config.token}`, }, }); this.setupEventHandlers(); } /** * Disconnect from WebSocket server */ disconnect(): void { if (this.socket) { console.log('[WebSocket] Disconnecting...'); this.socket.disconnect(); this.socket = null; this.currentFamilyId = null; this.notifyConnectionStatus(false); } } /** * Join a family room to receive real-time updates */ joinFamily(familyId: string): void { if (!this.socket?.connected) { console.warn('[WebSocket] Not connected, cannot join family'); return; } console.log('[WebSocket] Joining family room:', familyId); this.currentFamilyId = familyId; this.socket.emit('joinFamily', { familyId }); } /** * Leave current family room */ leaveFamily(): void { if (!this.socket?.connected || !this.currentFamilyId) { return; } console.log('[WebSocket] Leaving family room:', this.currentFamilyId); this.socket.emit('leaveFamily'); this.currentFamilyId = null; } /** * Add event listener */ on(event: string, callback: WebSocketEventCallback): () => void { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, new Set()); } this.eventListeners.get(event)!.add(callback); // Return unsubscribe function return () => { this.off(event, callback); }; } /** * Remove event listener */ off(event: string, callback: WebSocketEventCallback): void { const listeners = this.eventListeners.get(event); if (listeners) { listeners.delete(callback); } } /** * Add connection status listener */ onConnectionStatusChange(callback: (connected: boolean) => void): () => void { this.connectionStatusListeners.add(callback); // Immediately call with current status callback(this.isConnected()); // Return unsubscribe function return () => { this.connectionStatusListeners.delete(callback); }; } /** * Check if connected */ isConnected(): boolean { return this.socket?.connected || false; } /** * Get current family ID */ getCurrentFamilyId(): string | null { return this.currentFamilyId; } /** * Setup socket event handlers */ private setupEventHandlers(): void { if (!this.socket) return; // Connection events this.socket.on('connect', () => { console.log('[WebSocket] Connected'); this.reconnectAttempts = 0; this.reconnectDelay = 1000; this.notifyConnectionStatus(true); // Rejoin family if we were in one if (this.currentFamilyId) { console.log('[WebSocket] Rejoining family after reconnect:', this.currentFamilyId); this.socket!.emit('joinFamily', { familyId: this.currentFamilyId }); } }); this.socket.on('disconnect', (reason) => { console.log('[WebSocket] Disconnected:', reason); this.notifyConnectionStatus(false); // Attempt reconnection with exponential backoff if (reason === 'io server disconnect') { // Server initiated disconnect, try to reconnect this.attemptReconnect(); } }); this.socket.on('connect_error', (error) => { console.error('[WebSocket] Connection error:', error.message); console.error('[WebSocket] Error details:', error); console.error('[WebSocket] Connection URL:', this.config?.url); this.attemptReconnect(); }); this.socket.on('error', (error) => { console.error('[WebSocket] Error:', error); this.emitToListeners('error', error); }); // Custom events this.socket.on('connected', (data) => { console.log('[WebSocket] Server acknowledged connection:', data); }); this.socket.on('familyJoined', (data) => { console.log('[WebSocket] Family joined:', data); this.emitToListeners('familyJoined', data); }); this.socket.on('familyLeft', (data) => { console.log('[WebSocket] Family left:', data); this.emitToListeners('familyLeft', data); }); // Presence updates this.socket.on('presenceUpdate', (data: PresenceUpdate) => { console.log('[WebSocket] Presence update:', data); this.emitToListeners('presenceUpdate', data); }); // Activity events this.socket.on('activityCreated', (activity) => { console.log('[WebSocket] Activity created:', activity); this.emitToListeners('activityCreated', activity); }); this.socket.on('activityUpdated', (activity) => { console.log('[WebSocket] Activity updated:', activity); this.emitToListeners('activityUpdated', activity); }); this.socket.on('activityDeleted', (data) => { console.log('[WebSocket] Activity deleted:', data); this.emitToListeners('activityDeleted', data); }); // Child events this.socket.on('childAdded', (child) => { console.log('[WebSocket] Child added:', child); this.emitToListeners('childAdded', child); }); this.socket.on('childUpdated', (child) => { console.log('[WebSocket] Child updated:', child); this.emitToListeners('childUpdated', child); }); this.socket.on('childDeleted', (data) => { console.log('[WebSocket] Child deleted:', data); this.emitToListeners('childDeleted', data); }); // Member events this.socket.on('memberAdded', (member) => { console.log('[WebSocket] Member added:', member); this.emitToListeners('memberAdded', member); }); this.socket.on('memberUpdated', (member) => { console.log('[WebSocket] Member updated:', member); this.emitToListeners('memberUpdated', member); }); this.socket.on('memberRemoved', (data) => { console.log('[WebSocket] Member removed:', data); this.emitToListeners('memberRemoved', data); }); } /** * Attempt reconnection with exponential backoff */ private attemptReconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('[WebSocket] Max reconnection attempts reached'); return; } this.reconnectAttempts++; const delay = Math.min( this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay ); console.log( `[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})` ); setTimeout(() => { if (this.config && !this.socket?.connected) { this.connect(this.config); } }, delay); } /** * Emit event to all registered listeners */ private emitToListeners(event: string, data: T): void { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach((callback) => { try { callback(data); } catch (error) { console.error(`[WebSocket] Error in event listener for '${event}':`, error); } }); } } /** * Notify connection status listeners */ private notifyConnectionStatus(connected: boolean): void { this.connectionStatusListeners.forEach((callback) => { try { callback(connected); } catch (error) { console.error('[WebSocket] Error in connection status listener:', error); } }); } } // Export singleton instance export const websocketService = new WebSocketService();