/** * Native WebSocket Client Wrapper * Provides Socket.IO-like interface using native WebSocket API * * This wrapper maintains compatibility with existing Socket.IO-style * event handlers while using the modern WebSocket API underneath. */ class WebSocketClient { constructor(url = null) { this.ws = null; this.url = url || this.getWebSocketUrl(); this.eventHandlers = new Map(); this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.isConnected = false; this.rooms = new Set(); this.messageQueue = []; this.autoReconnect = true; } /** * Get WebSocket URL based on current page URL */ getWebSocketUrl() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; return `${protocol}//${host}/ws/connect`; } /** * Connect to WebSocket server */ connect() { if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.log('WebSocket already connected'); return; } try { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; // Emit connect event this.emit('connect'); // Rejoin rooms this.rejoinRooms(); // Process queued messages this.processMessageQueue(); }; this.ws.onmessage = (event) => { this.handleMessage(event.data); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.emit('error', { error: 'WebSocket connection error' }); }; this.ws.onclose = (event) => { console.log('WebSocket disconnected', event.code, event.reason); this.isConnected = false; this.emit('disconnect', { code: event.code, reason: event.reason }); // Attempt reconnection if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * this.reconnectAttempts; console.log(`Attempting reconnection in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => this.connect(), delay); } }; } catch (error) { console.error('Failed to create WebSocket connection:', error); this.emit('error', { error: 'Failed to connect' }); } } /** * Disconnect from WebSocket server */ disconnect() { this.autoReconnect = false; if (this.ws) { this.ws.close(1000, 'Client disconnected'); } } /** * Handle incoming WebSocket message */ handleMessage(data) { try { const message = JSON.parse(data); const { type, data: payload, timestamp } = message; // Emit event with payload if (type) { this.emit(type, payload || {}); } } catch (error) { console.error('Failed to parse WebSocket message:', error, data); } } /** * Register event handler (Socket.IO-style) */ on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event).push(handler); } /** * Remove event handler */ off(event, handler) { if (!this.eventHandlers.has(event)) { return; } const handlers = this.eventHandlers.get(event); const index = handlers.indexOf(handler); if (index !== -1) { handlers.splice(index, 1); } } /** * Emit event to registered handlers */ emit(event, data = null) { if (!this.eventHandlers.has(event)) { return; } const handlers = this.eventHandlers.get(event); handlers.forEach(handler => { try { if (data !== null) { handler(data); } else { handler(); } } catch (error) { console.error(`Error in event handler for ${event}:`, error); } }); } /** * Send message to server */ send(action, data = {}) { const message = JSON.stringify({ action, data }); if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { this.ws.send(message); } else { console.warn('WebSocket not connected, queueing message'); this.messageQueue.push(message); } } /** * Join a room (subscribe to topic) */ join(room) { this.rooms.add(room); if (this.isConnected) { this.send('join', { room }); } } /** * Leave a room (unsubscribe from topic) */ leave(room) { this.rooms.delete(room); if (this.isConnected) { this.send('leave', { room }); } } /** * Rejoin all rooms after reconnection */ rejoinRooms() { this.rooms.forEach(room => { this.send('join', { room }); }); } /** * Process queued messages after connection */ processMessageQueue() { while (this.messageQueue.length > 0 && this.isConnected) { const message = this.messageQueue.shift(); this.ws.send(message); } } /** * Check if connected */ connected() { return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN; } } /** * Create global io() function for Socket.IO compatibility */ function io(url = null) { const client = new WebSocketClient(url); client.connect(); return client; }