Really-amin's picture
Upload 325 files
b66240d verified
/**
* WebSocket Client for Real-time Communication
* Manages WebSocket connections with automatic reconnection and exponential backoff
* Supports message routing to type-specific subscribers
*/
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; // Initial backoff delay in ms
this.maxBackoff = 16000; // Maximum backoff delay in ms
this.shouldReconnect = true;
this.reconnectAttempts = 0;
this.connectionStartTime = null;
}
/**
* Automatically determine WebSocket URL based on current window location
* Always uses the current origin to avoid hardcoded URLs
*/
get url() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
return `${protocol}//${host}/ws`;
}
/**
* Log WebSocket events for debugging and monitoring
* Maintains a rolling buffer of the last 100 events
* @param {Object} event - Event object to log
*/
logEvent(event) {
const entry = {
...event,
time: new Date().toISOString(),
attempt: this.reconnectAttempts
};
this.eventLog.push(entry);
// Keep only last 100 events
if (this.eventLog.length > 100) {
this.eventLog = this.eventLog.slice(-100);
}
console.log('[WSClient]', entry);
}
/**
* Subscribe to connection status changes
* @param {Function} callback - Called with new status ('connecting', 'connected', 'disconnected', 'error')
* @returns {Function} Unsubscribe function
*/
onStatusChange(callback) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
this.statusSubscribers.add(callback);
// Immediately call with current status
callback(this.status);
return () => this.statusSubscribers.delete(callback);
}
/**
* Subscribe to all WebSocket messages
* @param {Function} callback - Called with parsed message data
* @returns {Function} Unsubscribe function
*/
onMessage(callback) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
this.globalSubscribers.add(callback);
return () => this.globalSubscribers.delete(callback);
}
/**
* Subscribe to specific message types
* @param {string} type - Message type to subscribe to (e.g., 'market_update', 'news_update')
* @param {Function} callback - Called with messages of the specified type
* @returns {Function} Unsubscribe function
*/
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);
}
/**
* Update connection status and notify all subscribers
* @param {string} newStatus - New status value
*/
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);
}
});
}
}
/**
* Establish WebSocket connection with automatic reconnection
* Implements exponential backoff for reconnection attempts
*/
connect() {
// Prevent multiple simultaneous connection attempts
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; // Reset backoff on successful connection
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
});
// Notify global subscribers
this.globalSubscribers.forEach(cb => {
try {
cb(data);
} catch (error) {
console.error('[WSClient] Error in global subscriber:', error);
}
});
// Notify type-specific subscribers
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
});
// Attempt reconnection if enabled
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
});
// Retry connection if enabled
if (this.shouldReconnect) {
this.reconnectAttempts++;
const delay = this.backoff;
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
setTimeout(() => this.connect(), delay);
}
}
}
/**
* Gracefully disconnect WebSocket and disable automatic reconnection
*/
disconnect() {
this.shouldReconnect = false;
if (this.socket) {
this.logEvent({ type: 'manual_disconnect' });
this.socket.close(1000, 'Client disconnect');
this.socket = null;
}
}
/**
* Manually trigger reconnection (useful for testing or recovery)
*/
reconnect() {
this.disconnect();
this.shouldReconnect = true;
this.backoff = 1000; // Reset backoff
this.reconnectAttempts = 0;
this.connect();
}
/**
* Send a message through the WebSocket connection
* @param {Object} data - Data to send (will be JSON stringified)
* @returns {boolean} True if sent successfully, false otherwise
*/
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;
}
}
/**
* Get a copy of the event log
* @returns {Array} Array of logged events
*/
getEvents() {
return [...this.eventLog];
}
/**
* Get current connection statistics
* @returns {Object} Connection statistics
*/
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
};
}
/**
* Check if WebSocket is currently connected
* @returns {boolean} True if connected
*/
isConnected() {
return this.socket && this.socket.readyState === WebSocket.OPEN;
}
/**
* Clear all subscribers (useful for cleanup)
*/
clearSubscribers() {
this.statusSubscribers.clear();
this.globalSubscribers.clear();
this.typeSubscribers.clear();
this.logEvent({ type: 'subscribers_cleared' });
}
}
// Create singleton instance
const wsClient = new WSClient();
// Auto-connect on module load
wsClient.connect();
// Export singleton instance
export default wsClient;