| |
| |
| |
| |
|
|
| class CryptoWebSocketClient { |
| constructor(url = null) { |
| this.url = url || `ws://${window.location.host}/ws`; |
| this.ws = null; |
| this.sessionId = null; |
| this.isConnected = false; |
| this.reconnectAttempts = 0; |
| this.maxReconnectAttempts = 5; |
| this.reconnectDelay = 3000; |
| this.reconnectTimer = null; |
| this.heartbeatTimer = null; |
|
|
| |
| this.messageHandlers = new Map(); |
| this.connectionCallbacks = []; |
|
|
| |
| this.connect(); |
| } |
|
|
| |
| |
| |
| connect() { |
| |
| this.disconnect(); |
|
|
| try { |
| console.log('[WebSocket] Connecting to:', this.url); |
| this.ws = new WebSocket(this.url); |
|
|
| |
| this.ws.onopen = this.handleOpen.bind(this); |
| this.ws.onmessage = this.handleMessage.bind(this); |
| this.ws.onerror = this.handleError.bind(this); |
| this.ws.onclose = this.handleClose.bind(this); |
|
|
| } catch (error) { |
| console.error('[WebSocket] Connection error:', error); |
| this.scheduleReconnect(); |
| } |
| } |
|
|
| |
| |
| |
| disconnect() { |
| |
| if (this.reconnectTimer) { |
| clearTimeout(this.reconnectTimer); |
| this.reconnectTimer = null; |
| } |
|
|
| if (this.heartbeatTimer) { |
| clearInterval(this.heartbeatTimer); |
| this.heartbeatTimer = null; |
| } |
|
|
| |
| if (this.ws) { |
| this.ws.onopen = null; |
| this.ws.onmessage = null; |
| this.ws.onerror = null; |
| this.ws.onclose = null; |
|
|
| if (this.ws.readyState === WebSocket.OPEN) { |
| this.ws.close(); |
| } |
|
|
| this.ws = null; |
| } |
|
|
| this.isConnected = false; |
| this.sessionId = null; |
| } |
|
|
| |
| |
| |
| handleOpen(event) { |
| console.log('[WebSocket] Connected'); |
| this.isConnected = true; |
| this.reconnectAttempts = 0; |
|
|
| |
| this.notifyConnection(true); |
|
|
| |
| this.updateConnectionStatus(true); |
|
|
| |
| this.startHeartbeat(); |
| } |
|
|
| |
| |
| |
| handleMessage(event) { |
| try { |
| const message = JSON.parse(event.data); |
| const type = message.type; |
|
|
| console.log('[WebSocket] Received message type:', type); |
|
|
| |
| switch (type) { |
| case 'welcome': |
| this.sessionId = message.session_id; |
| console.log('[WebSocket] Session ID:', this.sessionId); |
| break; |
|
|
| case 'heartbeat': |
| this.send({ type: 'pong' }); |
| break; |
|
|
| case 'stats_update': |
| this.handleStatsUpdate(message.data); |
| break; |
|
|
| case 'provider_stats': |
| this.handleProviderStats(message.data); |
| break; |
|
|
| case 'market_update': |
| this.handleMarketUpdate(message.data); |
| break; |
|
|
| case 'price_update': |
| this.handlePriceUpdate(message.data); |
| break; |
|
|
| case 'alert': |
| this.handleAlert(message.data); |
| break; |
| } |
|
|
| |
| const handler = this.messageHandlers.get(type); |
| if (handler) { |
| handler(message); |
| } |
|
|
| } catch (error) { |
| console.error('[WebSocket] Error processing message:', error); |
| } |
| } |
|
|
| |
| |
| |
| handleError(error) { |
| console.error('[WebSocket] Error:', error); |
| this.isConnected = false; |
| this.updateConnectionStatus(false); |
| } |
|
|
| |
| |
| |
| handleClose(event) { |
| console.log('[WebSocket] Disconnected'); |
| this.isConnected = false; |
| this.sessionId = null; |
|
|
| |
| this.notifyConnection(false); |
|
|
| |
| this.updateConnectionStatus(false); |
|
|
| |
| if (this.heartbeatTimer) { |
| clearInterval(this.heartbeatTimer); |
| this.heartbeatTimer = null; |
| } |
|
|
| |
| this.scheduleReconnect(); |
| } |
|
|
| |
| |
| |
| scheduleReconnect() { |
| if (this.reconnectAttempts < this.maxReconnectAttempts) { |
| this.reconnectAttempts++; |
| console.log(`[WebSocket] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); |
|
|
| this.reconnectTimer = setTimeout(() => { |
| this.connect(); |
| }, this.reconnectDelay); |
| } else { |
| console.error('[WebSocket] Max reconnection attempts reached'); |
| this.showReconnectButton(); |
| } |
| } |
|
|
| |
| |
| |
| startHeartbeat() { |
| |
| this.heartbeatTimer = setInterval(() => { |
| if (this.isConnected) { |
| this.send({ type: 'ping' }); |
| } |
| }, 30000); |
| } |
|
|
| |
| |
| |
| send(data) { |
| if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { |
| this.ws.send(JSON.stringify(data)); |
| } else { |
| console.warn('[WebSocket] Cannot send - not connected'); |
| } |
| } |
|
|
| |
| |
| |
| subscribe(group) { |
| this.send({ |
| type: 'subscribe', |
| group: group |
| }); |
| } |
|
|
| |
| |
| |
| unsubscribe(group) { |
| this.send({ |
| type: 'unsubscribe', |
| group: group |
| }); |
| } |
|
|
| |
| |
| |
| requestStats() { |
| this.send({ |
| type: 'get_stats' |
| }); |
| } |
|
|
| |
| |
| |
| on(type, handler) { |
| this.messageHandlers.set(type, handler); |
|
|
| |
| return () => { |
| this.messageHandlers.delete(type); |
| }; |
| } |
|
|
| |
| |
| |
| off(type) { |
| this.messageHandlers.delete(type); |
| } |
|
|
| |
| |
| |
| onConnection(callback) { |
| this.connectionCallbacks.push(callback); |
|
|
| |
| return () => { |
| const index = this.connectionCallbacks.indexOf(callback); |
| if (index > -1) { |
| this.connectionCallbacks.splice(index, 1); |
| } |
| }; |
| } |
|
|
| |
| |
| |
| notifyConnection(connected) { |
| this.connectionCallbacks.forEach(callback => { |
| try { |
| callback(connected); |
| } catch (error) { |
| console.error('[WebSocket] Error in connection callback:', error); |
| } |
| }); |
| } |
|
|
| |
|
|
| handleStatsUpdate(data) { |
| const activeConnections = data.active_connections || 0; |
| const totalSessions = data.total_sessions || 0; |
|
|
| this.updateOnlineUsers(activeConnections, totalSessions); |
|
|
| if (data.client_types) { |
| this.updateClientTypes(data.client_types); |
| } |
| } |
|
|
| handleProviderStats(data) { |
| if (window.dashboardApp && window.dashboardApp.updateProviderStats) { |
| window.dashboardApp.updateProviderStats(data); |
| } |
| } |
|
|
| handleMarketUpdate(data) { |
| if (window.dashboardApp && window.dashboardApp.updateMarketData) { |
| window.dashboardApp.updateMarketData(data); |
| } |
| } |
|
|
| handlePriceUpdate(data) { |
| if (window.dashboardApp && window.dashboardApp.updatePrice) { |
| window.dashboardApp.updatePrice(data.symbol, data.price, data.change_24h); |
| } |
| } |
|
|
| handleAlert(data) { |
| this.showAlert(data.message, data.severity); |
| } |
|
|
| |
|
|
| updateConnectionStatus(connected) { |
| const statusBar = document.querySelector('.connection-status-bar'); |
| const statusDot = document.getElementById('ws-status-dot'); |
| const statusText = document.getElementById('ws-status-text'); |
|
|
| if (statusBar) { |
| if (connected) { |
| statusBar.classList.remove('disconnected'); |
| } else { |
| statusBar.classList.add('disconnected'); |
| } |
| } |
|
|
| if (statusDot) { |
| statusDot.className = connected ? 'status-dot status-online' : 'status-dot status-offline'; |
| } |
|
|
| if (statusText) { |
| statusText.textContent = connected ? 'Connected' : 'Disconnected'; |
| } |
| } |
|
|
| updateOnlineUsers(active, total) { |
| const activeEl = document.getElementById('active-users-count'); |
| const totalEl = document.getElementById('total-sessions-count'); |
|
|
| if (activeEl) { |
| activeEl.textContent = active; |
| activeEl.classList.add('count-updated'); |
| setTimeout(() => activeEl.classList.remove('count-updated'), 500); |
| } |
|
|
| if (totalEl) { |
| totalEl.textContent = total; |
| } |
| } |
|
|
| updateClientTypes(types) { |
| |
| if (window.dashboardApp && window.dashboardApp.updateClientTypes) { |
| window.dashboardApp.updateClientTypes(types); |
| } |
| } |
|
|
| showAlert(message, severity = 'info') { |
| const alertContainer = document.getElementById('alerts-container') || document.body; |
|
|
| const alert = document.createElement('div'); |
| alert.className = `alert alert-${severity}`; |
| alert.innerHTML = ` |
| <strong>${severity === 'error' ? '❌' : severity === 'warning' ? '⚠️' : 'ℹ️'}</strong> |
| ${message} |
| `; |
|
|
| alertContainer.appendChild(alert); |
|
|
| |
| setTimeout(() => { |
| alert.remove(); |
| }, 5000); |
| } |
|
|
| showReconnectButton() { |
| const statusBar = document.querySelector('.connection-status-bar'); |
| if (statusBar && !document.getElementById('ws-reconnect-btn')) { |
| const button = document.createElement('button'); |
| button.id = 'ws-reconnect-btn'; |
| button.className = 'btn btn-sm btn-secondary'; |
| button.textContent = '🔄 Reconnect'; |
| button.onclick = () => { |
| this.reconnectAttempts = 0; |
| this.connect(); |
| button.remove(); |
| }; |
| statusBar.appendChild(button); |
| } |
| } |
|
|
| |
| |
| |
| destroy() { |
| console.log('[WebSocket] Destroying client'); |
| this.disconnect(); |
| this.messageHandlers.clear(); |
| this.connectionCallbacks = []; |
| } |
| } |
|
|
| |
| window.wsClient = null; |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| try { |
| window.wsClient = new CryptoWebSocketClient(); |
| console.log('[WebSocket] Client initialized'); |
| } catch (error) { |
| console.error('[WebSocket] Initialization error:', error); |
| } |
| }); |
|
|
| |
| window.addEventListener('beforeunload', () => { |
| if (window.wsClient) { |
| window.wsClient.destroy(); |
| } |
| }); |
|
|
| console.log('[WebSocket] Module loaded'); |
|
|