UNPKG

solver-sdk

Version:

SDK для интеграции с Code Solver Backend API

302 lines 14 kB
import { createWebSocketLogger } from './logger.js'; import { WebSocketEvents as WsEvents } from '../constants/websocket-events.constants.js'; /** * Класс для управления механизмом ping/pong */ export class PingPongManager { /** * Создает новый менеджер ping/pong * @param {PingPongManagerOptions} options Опции менеджера ping/pong */ constructor(options = {}) { /** Таймеры для ping/pong */ this.pingIntervals = new Map(); /** Статистика ping/pong */ this.pingStats = new Map(); /** Количество последовательных таймаутов */ this.pingTimeouts = new Map(); /** Хранилище обработчиков ping/pong */ this.pingPongEventHandlers = new Map(); /** Клиенты WebSocket по namespace */ this.clients = new Map(); this.pingInterval = options.pingInterval || 30000; this.pingTimeoutThreshold = options.pingTimeoutThreshold || 3; this.logger = options.logger || createWebSocketLogger('PingPongManager'); } /** * Регистрирует WebSocket клиент для указанного пространства имен * @param {WebSocketNamespace} namespace Пространство имен * @param {WebSocketClient} client WebSocket клиент */ registerClient(namespace, client) { this.clients.set(namespace, client); // Инициализируем статистику для namespace if (!this.pingStats.has(namespace)) { this.pingStats.set(namespace, { namespace, socketId: client.getSocketId(), pingSent: 0, pongReceived: 0, averageRtt: 0, minRtt: Number.MAX_SAFE_INTEGER, maxRtt: 0, lastRtt: 0, lastPongTimestamp: 0, isConnected: client.isConnected() }); } // Сбрасываем счетчик таймаутов this.pingTimeouts.set(namespace, 0); // Устанавливаем обработчик для события connection_pong client.on(WsEvents.CONNECTION_PONG, this.createPongHandler(namespace)); this.logger.debug(`Зарегистрирован клиент для ${namespace}`, { socketId: client.getSocketId() }); } /** * Удаляет регистрацию WebSocket клиента для указанного пространства имен * @param {WebSocketNamespace} namespace Пространство имен */ unregisterClient(namespace) { this.clients.delete(namespace); this.logger.debug(`Удалена регистрация клиента для ${namespace}`); } /** * Создает обработчик для события pong * @param {WebSocketNamespace} namespace Пространство имен * @returns {PingPongEventHandler} Обработчик события pong */ createPongHandler(namespace) { return (data) => { // Обновляем статистику const stats = this.pingStats.get(namespace); if (stats) { stats.pongReceived++; stats.lastPongTimestamp = Date.now(); // Рассчитываем RTT, если есть метка времени эхо if (data && data.echo) { const rtt = Date.now() - data.echo; stats.lastRtt = rtt; // Обновляем min и max stats.minRtt = Math.min(stats.minRtt, rtt); stats.maxRtt = Math.max(stats.maxRtt, rtt); // Обновляем среднее значение stats.averageRtt = (stats.averageRtt * (stats.pongReceived - 1) + rtt) / stats.pongReceived; } // Сбрасываем счетчик таймаутов this.pingTimeouts.set(namespace, 0); // Обновляем ID сокета, если он изменился const client = this.clients.get(namespace); if (client) { stats.socketId = client.getSocketId(); stats.isConnected = client.isConnected(); } } // Логируем получение pong this.logger.debug(`Получен pong для ${namespace}`, { rtt: stats?.lastRtt, socketId: stats?.socketId }); // Вызываем обработчики событий this.notifyEventHandlers('connection_pong', { namespace, timestamp: Date.now(), rtt: stats?.lastRtt, socketId: stats?.socketId }); }; } /** * Уведомляет обработчики о событии * @param {string} eventType Тип события * @param {any} data Данные события */ notifyEventHandlers(eventType, data) { const handlers = this.pingPongEventHandlers.get(eventType) || []; for (const handler of handlers) { try { handler(data); } catch (error) { this.logger.error(`Ошибка в обработчике события ${eventType}`, error); } } } /** * Включает автоматическую отправку ping-сообщений * @param {WebSocketNamespace} namespace Пространство имен * @returns {boolean} true, если механизм успешно включен */ enablePingPong(namespace) { const client = this.clients.get(namespace); if (!client || !client.isConnected()) { this.logger.warn(`Невозможно включить ping/pong для неактивного соединения в ${namespace}`); return false; } // Останавливаем существующий таймер, если есть this.disablePingPong(namespace); // Инициализируем статистику, если не была создана if (!this.pingStats.has(namespace)) { this.pingStats.set(namespace, { namespace, socketId: client.getSocketId(), pingSent: 0, pongReceived: 0, averageRtt: 0, minRtt: Number.MAX_SAFE_INTEGER, maxRtt: 0, lastRtt: 0, lastPongTimestamp: 0, isConnected: client.isConnected() }); } // Сбрасываем счетчик таймаутов this.pingTimeouts.set(namespace, 0); // Устанавливаем интервал отправки ping const pingInterval = setInterval(() => { const currentClient = this.clients.get(namespace); if (!currentClient || !currentClient.isConnected()) { this.disablePingPong(namespace); return; } // Формируем данные ping const pingData = { timestamp: Date.now(), echo: Date.now() }; // Отправляем ping const sent = currentClient.send({ event: WsEvents.CONNECTION_PING, data: pingData }); // Если успешно отправлено, обновляем статистику if (sent) { const stats = this.pingStats.get(namespace); if (stats) { stats.pingSent++; } this.logger.debug(`Отправлен ping для ${namespace}`, pingData); } else { this.logger.warn(`Не удалось отправить ping для ${namespace}`); } // Проверяем таймаут this.checkPingTimeout(namespace); }, this.pingInterval); // Сохраняем интервал this.pingIntervals.set(namespace, pingInterval); this.logger.info(`Включен механизм ping/pong для ${namespace} с интервалом ${this.pingInterval}ms`); return true; } /** * Проверяет таймаут для ping/pong * @param {WebSocketNamespace} namespace Пространство имен */ checkPingTimeout(namespace) { const timeouts = this.pingTimeouts.get(namespace) || 0; const stats = this.pingStats.get(namespace); if (!stats) return; // Если разница между отправленными и полученными превышает порог, // или последний pong был получен слишком давно if ((stats.pingSent - stats.pongReceived > this.pingTimeoutThreshold) || (Date.now() - stats.lastPongTimestamp > this.pingInterval * this.pingTimeoutThreshold)) { // Увеличиваем счетчик таймаутов this.pingTimeouts.set(namespace, timeouts + 1); if (timeouts + 1 >= this.pingTimeoutThreshold) { // Соединение потеряно this.logger.error(`Соединение потеряно (таймаут ping/pong) для ${namespace}`); // Установка флага неактивного соединения stats.isConnected = false; // Уведомляем о таймауте соединения this.notifyEventHandlers('connection_timeout', { namespace, socketId: stats.socketId, timeouts: timeouts + 1, threshold: this.pingTimeoutThreshold }); } } } /** * Отключает автоматическую отправку ping-сообщений * @param {WebSocketNamespace} namespace Пространство имен */ disablePingPong(namespace) { const interval = this.pingIntervals.get(namespace); if (interval) { clearInterval(interval); this.pingIntervals.delete(namespace); this.logger.info(`Отключен механизм ping/pong для ${namespace}`); } } /** * Отключает автоматическую отправку ping-сообщений для всех пространств имен */ disablePingPongAll() { for (const [namespace, interval] of this.pingIntervals.entries()) { clearInterval(interval); this.pingIntervals.delete(namespace); this.logger.info(`Отключен механизм ping/pong для ${namespace}`); } } /** * Получает статистику ping/pong для указанного пространства имен * @param {WebSocketNamespace} namespace Пространство имен * @returns {PingPongStats | null} Статистика ping/pong */ getPingStats(namespace) { return this.pingStats.get(namespace) || null; } /** * Получает статистику ping/pong для всех пространств имен * @returns {PingPongStats[]} Массив статистики ping/pong */ getAllPingStats() { return Array.from(this.pingStats.values()); } /** * Добавляет обработчик для событий ping/pong * @param {string} eventType Тип события (connection_timeout, connection_pong) * @param {PingPongEventHandler} handler Обработчик события */ onPingPongEvent(eventType, handler) { if (!this.pingPongEventHandlers.has(eventType)) { this.pingPongEventHandlers.set(eventType, []); } const handlers = this.pingPongEventHandlers.get(eventType); if (handlers) { handlers.push(handler); } } /** * Удаляет обработчик для событий ping/pong * @param {string} eventType Тип события * @param {PingPongEventHandler} [handler] Обработчик события (если не указан, удаляются все обработчики) */ offPingPongEvent(eventType, handler) { if (!handler) { // Если обработчик не указан, удаляем все обработчики для этого типа события this.pingPongEventHandlers.delete(eventType); } else { // Если обработчик указан, удаляем только его const handlers = this.pingPongEventHandlers.get(eventType); if (handlers) { const index = handlers.findIndex(h => h === handler); if (index !== -1) { handlers.splice(index, 1); } } } } /** * Проверяет, включен ли механизм ping/pong для указанного пространства имен * @param {WebSocketNamespace} namespace Пространство имен * @returns {boolean} true, если механизм включен */ isPingPongEnabled(namespace) { return this.pingIntervals.has(namespace); } } //# sourceMappingURL=ping-pong-manager.js.map