solver-sdk
Version:
SDK для интеграции с Code Solver Backend API
302 lines • 14 kB
JavaScript
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