1/** 2 * Unified WebSocket Client cho Real-time Updates 3 * Tương thÃch vá»i server-socket service từ colatv40 react 4 * 5 * Usage: 6 * const wsClient = new RealtimeWebSocketClient('wss://your-socket-server.com'); 7 * wsClient.subscribe('matches:updated', (data) => { ... }); 8 * wsClient.connect(); 9 */ 10class RealtimeWebSocketClient { 11 constructor(wsUrl) { 12 this.wsUrl = wsUrl; 13 this.ws = null; 14 this.reconnectAttempts = 0; 15 this.maxReconnectAttempts = 10; 16 this.reconnectDelay = 2000; 17 this.maxReconnectDelay = 30000; 18 this.listeners = new Map(); // eventType -> Set<callbacks> 19 this.isConnected = false; 20 this.heartbeatInterval = null; 21 this.heartbeatIntervalMs = 30000; // 30 seconds 22 this.shouldReconnect = true; 23 this.reconnectTimeout = null; 24 } 25 26 /** 27 * Connect to WebSocket server 28 */ 29 connect() { 30 if (this.ws && this.ws.readyState === WebSocket.OPEN) { 31 console.log('[WebSocket] Already connected'); 32 return; 33 } 34 35 if (this.ws && this.ws.readyState === WebSocket.CONNECTING) { 36 console.log('[WebSocket] Connection in progress...'); 37 return; 38 } 39 40 try { 41 console.log('[WebSocket] Connecting to:', this.wsUrl); 42 this.ws = new WebSocket(this.wsUrl); 43 44 this.ws.onopen = () => { 45 console.log('[WebSocket] â Connected'); 46 this.isConnected = true; 47 this.reconnectAttempts = 0; 48 this.startHeartbeat(); 49 this.emit('connected'); 50 }; 51 52 this.ws.onmessage = (event) => { 53 try { 54 const message = JSON.parse(event.data); 55 this.handleMessage(message); 56 } catch (error) { 57 console.error('[WebSocket] Error parsing message:', error, event.data); 58 } 59 }; 60 61 this.ws.onerror = (error) => { 62 console.error('[WebSocket] â Error:', error); 63 this.isConnected = false; 64 this.emit('error', error); 65 }; 66 67 this.ws.onclose = (event) => { 68 console.log('[WebSocket] Connection closed:', event.code, event.reason || 'N/A'); 69 this.isConnected = false; 70 this.stopHeartbeat(); 71 72 // Auto reconnect nếu không phải Äóng bình thưá»ng 73 if (this.shouldReconnect && event.code !== 1000) { 74 this.scheduleReconnect(); 75 } 76 77 this.emit('disconnected', event); 78 }; 79 80 } catch (error) { 81 console.error('[WebSocket] Failed to create connection:', error); 82 this.scheduleReconnect(); 83 } 84 } 85 86 /** 87 * Handle incoming WebSocket messages 88 */ 89 handleMessage(message) { 90 if (message.type === 'pong') { 91 // Heartbeat response - không cần xá» lý 92 return; 93 } 94 95 if (message.type === 'connected') { 96 console.log('[WebSocket] â Connection confirmed by server'); 97 return; 98 } 99 100 // Emit to all listeners for this event type 101 const callbacks = this.listeners.get(message.type); 102 if (callbacks) { 103 callbacks.forEach(callback => { 104 try { 105 callback(message.data || message); 106 } catch (error) { 107 console.error('[WebSocket] Error in callback for', message.type, ':', error); 108 } 109 }); 110 } 111 112 // Also emit generic 'message' event 113 this.emit('message', message); 114 } 115 116 /** 117 * Subscribe to specific event type 118 * @param {string} eventType - Event type to listen for (e.g., 'matches:updated', 'detail_live:updated') 119 * @param {Function} callback - Callback function to call when event is received 120 * @returns {Function} Unsubscribe function 121 */ 122 subscribe(eventType, callback) { 123 if (typeof callback !== 'function') { 124 console.error('[WebSocket] Callback must be a function'); 125 return () => {}; 126 } 127 128 if (!this.listeners.has(eventType)) { 129 this.listeners.set(eventType, new Set()); 130 } 131 this.listeners.get(eventType).add(callback); 132 133 // Auto-connect if not connected 134 if (!this.isConnected && !this.ws) { 135 this.connect(); 136 } 137 138 // Return unsubscribe function 139 return () => this.unsubscribe(eventType, callback); 140 } 141 142 /** 143 * Unsubscribe from event type 144 */ 145 unsubscribe(eventType, callback) { 146 const callbacks = this.listeners.get(eventType); 147 if (callbacks) { 148 callbacks.delete(callback); 149 if (callbacks.size === 0) { 150 this.listeners.delete(eventType); 151 } 152 } 153 } 154 155 /** 156 * Emit event to internal listeners 157 */ 158 emit(eventType, data) { 159 const callbacks = this.listeners.get(eventType); 160 if (callbacks) { 161 callbacks.forEach(callback => { 162 try { 163 callback(data); 164 } catch (error) { 165 console.error('[WebSocket] Error in emit callback:', error); 166 } 167 }); 168 } 169 } 170 171 /** 172 * Send message to server 173 */ 174 send(data) { 175 if (this.ws && this.ws.readyState === WebSocket.OPEN) { 176 this.ws.send(JSON.stringify(data)); 177 } else { 178 console.warn('[WebSocket] Cannot send, not connected. ReadyState:', this.ws ? this.ws.readyState : 'null'); 179 } 180 } 181 182 /** 183 * Start heartbeat to keep connection alive 184 */ 185 startHeartbeat() { 186 this.stopHeartbeat(); 187 this.heartbeatInterval = setInterval(() => { 188 if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { 189 this.send({ type: 'ping' }); 190 } else { 191 // Connection lost, stop heartbeat 192 this.stopHeartbeat(); 193 } 194 }, this.heartbeatIntervalMs); 195 } 196 197 /** 198 * Stop heartbeat 199 */ 200 stopHeartbeat() { 201 if (this.heartbeatInterval) { 202 clearInterval(this.heartbeatInterval); 203 this.heartbeatInterval = null; 204 } 205 } 206 207 /** 208 * Schedule reconnection with exponential backoff 209 */ 210 scheduleReconnect() { 211 if (!this.shouldReconnect) return; 212 if (this.reconnectAttempts >= this.maxReconnectAttempts) { 213 console.error('[WebSocket] Max reconnect attempts reached, stopping reconnection');
214 this.emit('maxReconnectAttemptsReached'); 215 return; 216 } 217 218 // Clear existing reconnect timeout 219 if (this.reconnectTimeout) { 220 clearTimeout(this.reconnectTimeout); 221 } 222 223 const delay = Math.min( 224 this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 225 this.maxReconnectDelay 226 ); 227 228 this.reconnectAttempts++; 229 console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); 230 231 this.reconnectTimeout = setTimeout(() => { 232 this.reconnectTimeout = null; 233 this.connect(); 234 }, delay); 235 } 236 237 /** 238 * Disconnect and prevent reconnection 239 */ 240 disconnect() { 241 console.log('[WebSocket] Disconnecting...'); 242 this.shouldReconnect = false; 243 this.stopHeartbeat(); 244 245 if (this.reconnectTimeout) { 246 clearTimeout(this.reconnectTimeout); 247 this.reconnectTimeout = null; 248 } 249 250 if (this.ws) { 251 this.ws.close(1000, 'Client disconnect'); 252 this.ws = null; 253 } 254 255 this.isConnected = false; 256 } 257 258 /** 259 * Get connection status 260 */ 261 getStatus() { 262 return { 263 isConnected: this.isConnected, 264 readyState: this.ws ? this.ws.readyState : WebSocket.CLOSED, 265 reconnectAttempts: this.reconnectAttempts, 266 url: this.wsUrl 267 }; 268 } 269 270 /** 271 * Reset reconnection attempts (useful after manual reconnect) 272 */ 273 resetReconnectAttempts() { 274 this.reconnectAttempts = 0; 275 } 276} 277 278// Export for use in other files 279if (typeof window !== 'undefined') { 280 window.RealtimeWebSocketClient = RealtimeWebSocketClient; 281} 282 283// Also support CommonJS/Node.js if needed 284if (typeof module !== 'undefined' && module.exports) { 285 module.exports = RealtimeWebSocketClient; 286} 287
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.