import { io } from 'socket.io-client'; class MetricsService { constructor(baseUrl) { this.baseUrl = baseUrl || window.location.origin; this.socket = null; this.subscriptions = new Map(); this.pendingRequests = new Map(); window.addEventListener('beforeunload', this.cleanupAll.bind(this)); window.addEventListener('pagehide', this.cleanupAll.bind(this)); window.addEventListener('beforeunload', () => { this.cleanupAll(); }); } connectWebSocket() { if (this.socket) { console.log('WebSocket already exists'); return; } console.log('Connecting WebSocket...'); this.socket = io(`${this.baseUrl.replace('http', 'ws')}/ws`, { transports: ['websocket'], withCredentials: true, }); this.socket.on('connect', () => { console.log('WebSocket connected'); // Восстанавливаем подписки при переподключении this.subscriptions.forEach((_, metricKey) => { const [metric, query] = metricKey.split('?'); const filters = this.parseFiltersFromKey(metricKey); this.socket.emit('subscribe-metric', { metric, filters }); }); }); this.socket.on('disconnect', () => { console.log('WebSocket disconnected'); this.socket = null; }); this.socket.on('metrics-data', ({ metric, data, requestId }) => { if (requestId && this.pendingRequests.has(requestId)) { const { resolve } = this.pendingRequests.get(requestId); resolve(data); this.pendingRequests.delete(requestId); return; } const callbacks = this.subscriptions.get(metric) || []; callbacks.forEach(cb => cb(data)); }); this.socket.on('metrics-error', ({ error, requestId }) => { if (requestId && this.pendingRequests.has(requestId)) { const { reject } = this.pendingRequests.get(requestId); reject(new Error(error)); this.pendingRequests.delete(requestId); } }); } async fetchMetricsRange(metric, start, end, step = 15, filters = {}) { return new Promise((resolve, reject) => { this.connectWebSocket(); const requestId = `range-${Date.now()}`; this.pendingRequests.set(requestId, { resolve, reject }); this.socket.emit('get-metrics', { metric, start, end, step, filters, isRangeQuery: true, requestId }); setTimeout(() => { if (this.pendingRequests.has(requestId)) { reject(new Error('Request timeout')); this.pendingRequests.delete(requestId); } }, 30000); }); } subscribeToMetric(metricKey, callback, interval = 5000, filters = {}) { this.connectWebSocket(); if (!this.subscriptions.has(metricKey)) { this.subscriptions.set(metricKey, []); const [metric] = metricKey.split('?'); this.socket.emit('subscribe-metric', { metric, interval, filters }); } const callbacks = this.subscriptions.get(metricKey); callbacks.push(callback); return () => { this.unsubscribeFromMetric(metricKey, callback); }; } unsubscribeFromMetric(metricKey, callback) { const callbacks = this.subscriptions.get(metricKey) || []; const filtered = callbacks.filter(cb => cb !== callback); if (filtered.length === 0) { this.subscriptions.delete(metricKey); if (this.socket && this.socket.connected) { const [metric] = metricKey.split('?'); this.socket.emit('unsubscribe-metric', { metric }); } } else { this.subscriptions.set(metricKey, filtered); } } parseFiltersFromKey(metricKey) { const parts = metricKey.split('?'); if (parts.length < 2) return {}; return parts[1].split('&').reduce((acc, pair) => { const [key, value] = pair.split('='); if (key && value) acc[key] = value; return acc; }, {}); } cleanupAll() { if (this.socket && this.socket.connected) { this.socket.emit('unsubscribe-all'); } this.subscriptions.clear(); this.disconnectWebSocket(); } disconnectWebSocket() { if (this.socket) { this.socket.disconnect(); this.socket = null; } } } // Создаем экземпляр сервиса const metricsService = new MetricsService(); export default metricsService;