|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WSClient { |
|
|
constructor() { |
|
|
this.socket = null; |
|
|
this.status = 'disconnected'; |
|
|
this.statusSubscribers = new Set(); |
|
|
this.globalSubscribers = new Set(); |
|
|
this.typeSubscribers = new Map(); |
|
|
this.eventLog = []; |
|
|
this.backoff = 1000; |
|
|
this.maxBackoff = 16000; |
|
|
this.shouldReconnect = true; |
|
|
this.reconnectAttempts = 0; |
|
|
this.connectionStartTime = null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get url() { |
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
|
|
const host = window.location.host; |
|
|
return `${protocol}//${host}/ws`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logEvent(event) { |
|
|
const entry = { |
|
|
...event, |
|
|
time: new Date().toISOString(), |
|
|
attempt: this.reconnectAttempts |
|
|
}; |
|
|
this.eventLog.push(entry); |
|
|
|
|
|
if (this.eventLog.length > 100) { |
|
|
this.eventLog = this.eventLog.slice(-100); |
|
|
} |
|
|
console.log('[WSClient]', entry); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onStatusChange(callback) { |
|
|
if (typeof callback !== 'function') { |
|
|
throw new Error('Callback must be a function'); |
|
|
} |
|
|
this.statusSubscribers.add(callback); |
|
|
|
|
|
callback(this.status); |
|
|
return () => this.statusSubscribers.delete(callback); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMessage(callback) { |
|
|
if (typeof callback !== 'function') { |
|
|
throw new Error('Callback must be a function'); |
|
|
} |
|
|
this.globalSubscribers.add(callback); |
|
|
return () => this.globalSubscribers.delete(callback); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subscribe(type, callback) { |
|
|
if (typeof callback !== 'function') { |
|
|
throw new Error('Callback must be a function'); |
|
|
} |
|
|
if (!this.typeSubscribers.has(type)) { |
|
|
this.typeSubscribers.set(type, new Set()); |
|
|
} |
|
|
const set = this.typeSubscribers.get(type); |
|
|
set.add(callback); |
|
|
return () => set.delete(callback); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateStatus(newStatus) { |
|
|
if (this.status !== newStatus) { |
|
|
const oldStatus = this.status; |
|
|
this.status = newStatus; |
|
|
this.logEvent({ |
|
|
type: 'status_change', |
|
|
from: oldStatus, |
|
|
to: newStatus |
|
|
}); |
|
|
this.statusSubscribers.forEach(cb => { |
|
|
try { |
|
|
cb(newStatus); |
|
|
} catch (error) { |
|
|
console.error('[WSClient] Error in status subscriber:', error); |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
connect() { |
|
|
|
|
|
if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) { |
|
|
console.log('[WSClient] Already connected or connecting'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.connectionStartTime = Date.now(); |
|
|
this.updateStatus('connecting'); |
|
|
|
|
|
try { |
|
|
this.socket = new WebSocket(this.url); |
|
|
this.logEvent({ |
|
|
type: 'connection_attempt', |
|
|
url: this.url, |
|
|
attempt: this.reconnectAttempts + 1 |
|
|
}); |
|
|
|
|
|
this.socket.onopen = () => { |
|
|
const connectionTime = Date.now() - this.connectionStartTime; |
|
|
this.backoff = 1000; |
|
|
this.reconnectAttempts = 0; |
|
|
this.updateStatus('connected'); |
|
|
this.logEvent({ |
|
|
type: 'connection_established', |
|
|
connectionTime: `${connectionTime}ms` |
|
|
}); |
|
|
console.log(`[WSClient] Connected to ${this.url} in ${connectionTime}ms`); |
|
|
}; |
|
|
|
|
|
this.socket.onmessage = (event) => { |
|
|
try { |
|
|
const data = JSON.parse(event.data); |
|
|
this.logEvent({ |
|
|
type: 'message_received', |
|
|
messageType: data.type || 'unknown', |
|
|
size: event.data.length |
|
|
}); |
|
|
|
|
|
|
|
|
this.globalSubscribers.forEach(cb => { |
|
|
try { |
|
|
cb(data); |
|
|
} catch (error) { |
|
|
console.error('[WSClient] Error in global subscriber:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (data.type && this.typeSubscribers.has(data.type)) { |
|
|
this.typeSubscribers.get(data.type).forEach(cb => { |
|
|
try { |
|
|
cb(data); |
|
|
} catch (error) { |
|
|
console.error(`[WSClient] Error in ${data.type} subscriber:`, error); |
|
|
} |
|
|
}); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('[WSClient] Message parse error:', error); |
|
|
this.logEvent({ |
|
|
type: 'parse_error', |
|
|
error: error.message, |
|
|
rawData: event.data.substring(0, 100) |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
this.socket.onclose = (event) => { |
|
|
const wasConnected = this.status === 'connected'; |
|
|
this.updateStatus('disconnected'); |
|
|
this.logEvent({ |
|
|
type: 'connection_closed', |
|
|
code: event.code, |
|
|
reason: event.reason || 'No reason provided', |
|
|
wasClean: event.wasClean |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.shouldReconnect) { |
|
|
this.reconnectAttempts++; |
|
|
const delay = this.backoff; |
|
|
this.backoff = Math.min(this.backoff * 2, this.maxBackoff); |
|
|
|
|
|
console.log(`[WSClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`); |
|
|
this.logEvent({ |
|
|
type: 'reconnect_scheduled', |
|
|
delay: `${delay}ms`, |
|
|
nextBackoff: `${this.backoff}ms` |
|
|
}); |
|
|
|
|
|
setTimeout(() => this.connect(), delay); |
|
|
} |
|
|
}; |
|
|
|
|
|
this.socket.onerror = (error) => { |
|
|
console.error('[WSClient] WebSocket error:', error); |
|
|
this.updateStatus('error'); |
|
|
this.logEvent({ |
|
|
type: 'connection_error', |
|
|
error: error.message || 'Unknown error', |
|
|
readyState: this.socket ? this.socket.readyState : 'null' |
|
|
}); |
|
|
}; |
|
|
} catch (error) { |
|
|
console.error('[WSClient] Failed to create WebSocket:', error); |
|
|
this.updateStatus('error'); |
|
|
this.logEvent({ |
|
|
type: 'creation_error', |
|
|
error: error.message |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.shouldReconnect) { |
|
|
this.reconnectAttempts++; |
|
|
const delay = this.backoff; |
|
|
this.backoff = Math.min(this.backoff * 2, this.maxBackoff); |
|
|
setTimeout(() => this.connect(), delay); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
disconnect() { |
|
|
this.shouldReconnect = false; |
|
|
if (this.socket) { |
|
|
this.logEvent({ type: 'manual_disconnect' }); |
|
|
this.socket.close(1000, 'Client disconnect'); |
|
|
this.socket = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reconnect() { |
|
|
this.disconnect(); |
|
|
this.shouldReconnect = true; |
|
|
this.backoff = 1000; |
|
|
this.reconnectAttempts = 0; |
|
|
this.connect(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
send(data) { |
|
|
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { |
|
|
console.error('[WSClient] Cannot send message: not connected'); |
|
|
this.logEvent({ |
|
|
type: 'send_failed', |
|
|
reason: 'not_connected', |
|
|
readyState: this.socket ? this.socket.readyState : 'null' |
|
|
}); |
|
|
return false; |
|
|
} |
|
|
|
|
|
try { |
|
|
const message = JSON.stringify(data); |
|
|
this.socket.send(message); |
|
|
this.logEvent({ |
|
|
type: 'message_sent', |
|
|
messageType: data.type || 'unknown', |
|
|
size: message.length |
|
|
}); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('[WSClient] Failed to send message:', error); |
|
|
this.logEvent({ |
|
|
type: 'send_error', |
|
|
error: error.message |
|
|
}); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getEvents() { |
|
|
return [...this.eventLog]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStats() { |
|
|
return { |
|
|
status: this.status, |
|
|
reconnectAttempts: this.reconnectAttempts, |
|
|
currentBackoff: this.backoff, |
|
|
maxBackoff: this.maxBackoff, |
|
|
shouldReconnect: this.shouldReconnect, |
|
|
subscriberCounts: { |
|
|
status: this.statusSubscribers.size, |
|
|
global: this.globalSubscribers.size, |
|
|
typed: Array.from(this.typeSubscribers.entries()).map(([type, subs]) => ({ |
|
|
type, |
|
|
count: subs.size |
|
|
})) |
|
|
}, |
|
|
eventLogSize: this.eventLog.length, |
|
|
url: this.url |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isConnected() { |
|
|
return this.socket && this.socket.readyState === WebSocket.OPEN; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearSubscribers() { |
|
|
this.statusSubscribers.clear(); |
|
|
this.globalSubscribers.clear(); |
|
|
this.typeSubscribers.clear(); |
|
|
this.logEvent({ type: 'subscribers_cleared' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const wsClient = new WSClient(); |
|
|
|
|
|
|
|
|
wsClient.connect(); |
|
|
|
|
|
|
|
|
export default wsClient; |