|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
|