Files
vna_system/vna_system/web_ui/static/js/modules/websocket.js
2025-09-23 18:42:55 +03:00

460 lines
12 KiB
JavaScript

/**
* WebSocket Manager
* Handles real-time communication with the VNA backend
*/
export class WebSocketManager {
constructor(config, notifications) {
this.config = config;
this.notifications = notifications;
this.ws = null;
this.isConnected = false;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.reconnectTimer = null;
this.pingTimer = null;
this.lastPing = null;
this.eventListeners = new Map();
// Message queue for when disconnected
this.messageQueue = [];
this.maxQueueSize = 100;
// Statistics
this.stats = {
messagesReceived: 0,
messagesPerSecond: 0,
lastMessageTime: null,
connectionTime: null,
bytesReceived: 0
};
// Rate limiting for message processing
this.messageRateCounter = [];
this.rateWindowMs = 1000;
}
/**
* Connect to WebSocket server
*/
async connect() {
if (this.isConnected || this.isConnecting) {
console.log('🔌 WebSocket already connected/connecting');
return;
}
try {
this.isConnecting = true;
this.emit('connecting');
console.log(`🔌 Connecting to WebSocket: ${this.config.url}`);
this.ws = new WebSocket(this.config.url);
this.setupWebSocketEvents();
// Wait for connection or timeout
await this.waitForConnection(5000);
} catch (error) {
this.isConnecting = false;
console.error('❌ WebSocket connection failed:', error);
this.handleConnectionError(error);
throw error;
}
}
/**
* Wait for WebSocket connection with timeout
*/
waitForConnection(timeoutMs) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('WebSocket connection timeout'));
}, timeoutMs);
const checkConnection = () => {
if (this.isConnected) {
clearTimeout(timeout);
resolve();
} else if (this.ws?.readyState === WebSocket.CLOSED ||
this.ws?.readyState === WebSocket.CLOSING) {
clearTimeout(timeout);
reject(new Error('WebSocket connection failed'));
}
};
// Check immediately and then every 100ms
checkConnection();
const interval = setInterval(() => {
checkConnection();
if (this.isConnected) {
clearInterval(interval);
}
}, 100);
// Clean up interval on resolve/reject
const originalResolve = resolve;
const originalReject = reject;
resolve = (...args) => {
clearInterval(interval);
originalResolve(...args);
};
reject = (...args) => {
clearInterval(interval);
originalReject(...args);
};
});
}
/**
* Set up WebSocket event handlers
*/
setupWebSocketEvents() {
if (!this.ws) return;
this.ws.onopen = (event) => {
console.log('✅ WebSocket connected');
this.isConnected = true;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.stats.connectionTime = new Date();
this.startPingPong();
this.processPendingMessages();
this.emit('connected', event);
this.notifications.show({
type: 'success',
title: 'Connected',
message: 'Real-time connection established'
});
};
this.ws.onmessage = (event) => {
try {
this.handleMessage(event.data);
} catch (error) {
console.error('❌ Error processing WebSocket message:', error);
this.notifications.show({
type: 'error',
title: 'Message Error',
message: 'Failed to process received data'
});
}
};
this.ws.onerror = (error) => {
console.error('❌ WebSocket error:', error);
this.handleConnectionError(error);
};
this.ws.onclose = (event) => {
console.log('🔌 WebSocket closed:', event.code, event.reason);
this.handleDisconnection(event);
};
}
/**
* Handle incoming messages
*/
handleMessage(data) {
// Update statistics
this.stats.messagesReceived++;
this.stats.lastMessageTime = new Date();
this.stats.bytesReceived += data.length;
// Rate limiting check
this.updateMessageRate();
try {
let parsedData;
// Handle different message types
if (typeof data === 'string') {
if (data === 'ping') {
this.handlePing();
return;
} else if (data === 'pong') {
this.handlePong();
return;
} else {
parsedData = JSON.parse(data);
}
} else {
// Handle binary data if needed
console.warn('⚠️ Received binary data, not implemented');
return;
}
// Emit data event
this.emit('data', parsedData);
} catch (error) {
console.error('❌ Failed to parse WebSocket message:', error);
console.log('📝 Raw message:', data);
}
}
/**
* Update message rate statistics
*/
updateMessageRate() {
const now = Date.now();
this.messageRateCounter.push(now);
// Remove messages outside the rate window
this.messageRateCounter = this.messageRateCounter.filter(
time => now - time < this.rateWindowMs
);
this.stats.messagesPerSecond = this.messageRateCounter.length;
}
/**
* Handle ping message
*/
handlePing() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send('pong');
}
}
/**
* Handle pong message
*/
handlePong() {
if (this.lastPing) {
const latency = Date.now() - this.lastPing;
console.log(`🏓 WebSocket latency: ${latency}ms`);
this.lastPing = null;
}
}
/**
* Start ping-pong mechanism
*/
startPingPong() {
this.pingTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.lastPing = Date.now();
this.ws.send('ping');
}
}, 30000); // Ping every 30 seconds
}
/**
* Handle connection errors
*/
handleConnectionError(error) {
console.error('❌ WebSocket connection error:', error);
if (!this.isConnected) {
this.notifications.show({
type: 'error',
title: 'Connection Failed',
message: 'Unable to establish real-time connection'
});
}
}
/**
* Handle disconnection
*/
handleDisconnection(event) {
const wasConnected = this.isConnected;
this.isConnected = false;
this.isConnecting = false;
// Clean up timers
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = null;
}
this.emit('disconnected', event);
if (wasConnected) {
this.notifications.show({
type: 'warning',
title: 'Disconnected',
message: 'Real-time connection lost. Attempting to reconnect...'
});
// Auto-reconnect
this.scheduleReconnect();
}
}
/**
* Schedule automatic reconnection
*/
scheduleReconnect() {
if (this.reconnectTimer) return;
const delay = Math.min(
this.config.reconnectInterval * Math.pow(2, this.reconnectAttempts),
30000 // Max 30 seconds
);
console.log(`🔄 Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.reconnect();
}, delay);
}
/**
* Manually trigger reconnection
*/
async reconnect() {
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
console.error('❌ Max reconnection attempts reached');
this.notifications.show({
type: 'error',
title: 'Connection Failed',
message: 'Unable to reconnect after multiple attempts'
});
return;
}
this.reconnectAttempts++;
// Close existing connection
if (this.ws) {
this.ws.close();
this.ws = null;
}
try {
await this.connect();
} catch (error) {
console.error(`❌ Reconnection attempt ${this.reconnectAttempts} failed:`, error);
this.scheduleReconnect();
}
}
/**
* Send message
*/
send(data) {
if (!this.isConnected || !this.ws) {
// Queue message for later
if (this.messageQueue.length < this.maxQueueSize) {
this.messageQueue.push(data);
console.log('📤 Message queued (not connected)');
} else {
console.warn('⚠️ Message queue full, dropping message');
}
return false;
}
try {
const message = typeof data === 'string' ? data : JSON.stringify(data);
this.ws.send(message);
return true;
} catch (error) {
console.error('❌ Failed to send message:', error);
return false;
}
}
/**
* Process pending messages after reconnection
*/
processPendingMessages() {
if (this.messageQueue.length > 0) {
console.log(`📤 Processing ${this.messageQueue.length} queued messages`);
const messages = [...this.messageQueue];
this.messageQueue = [];
messages.forEach(message => {
this.send(message);
});
}
}
/**
* Disconnect WebSocket
*/
disconnect() {
console.log('🔌 Disconnecting WebSocket');
// Clear reconnection timer
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// Clear ping timer
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = null;
}
// Close WebSocket
if (this.ws) {
this.ws.close(1000, 'Manual disconnect');
this.ws = null;
}
this.isConnected = false;
this.isConnecting = false;
this.reconnectAttempts = 0;
}
/**
* Get connection statistics
*/
getStats() {
return {
...this.stats,
isConnected: this.isConnected,
isConnecting: this.isConnecting,
reconnectAttempts: this.reconnectAttempts,
queuedMessages: this.messageQueue.length
};
}
/**
* Event listener management
*/
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
off(event, callback) {
if (this.eventListeners.has(event)) {
const listeners = this.eventListeners.get(event);
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
emit(event, data) {
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`❌ Error in event listener for ${event}:`, error);
}
});
}
}
/**
* Cleanup
*/
destroy() {
this.disconnect();
this.eventListeners.clear();
this.messageQueue = [];
}
}