solver-sdk
Version:
SDK для интеграции с Code Solver Backend API
286 lines • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseWebSocketClient = void 0;
const websocket_client_js_1 = require("../utils/websocket-client.js");
const websocket_namespaces_constants_js_1 = require("../constants/websocket-namespaces.constants.js");
const websocket_events_constants_js_1 = require("../constants/websocket-events.constants.js");
const logger_js_1 = require("../utils/logger.js");
const session_manager_js_1 = require("../utils/session-manager.js");
const ping_pong_manager_js_1 = require("../utils/ping-pong-manager.js");
const connection_state_manager_js_1 = require("../utils/connection-state-manager.js");
/**
* Базовый абстрактный класс для всех WebSocket клиентов
*/
class BaseWebSocketClient {
/**
* Создает новый базовый WebSocket клиент
* @param {WebSocketNamespace} namespace Пространство имен WebSocket
* @param {string} baseURL Базовый URL API
* @param {BaseWebSocketClientOptions} options Опции клиента
*/
constructor(namespace, baseURL, options = {}) {
/** WebSocket клиент */
this.client = null;
this.namespace = namespace;
this.baseURL = baseURL.replace(/^http/, 'ws');
// Добавляем заголовок авторизации, если задан API ключ
this.options = {
...options,
headers: {
...(options.headers || {}),
...(options.apiKey ? { 'Authorization': `Bearer ${options.apiKey}` } : {})
}
};
// Инициализируем логгер
this.logger = typeof options.logger === 'function'
? (0, logger_js_1.createWebSocketLogger)(`WebSocket:${namespace}`, options.logger)
: (options.logger || (0, logger_js_1.createWebSocketLogger)(`WebSocket:${namespace}`));
// Инициализируем менеджеры
this.sessionManager = new session_manager_js_1.SessionManager({
enableSessionPersistence: options.enableSessionPersistence,
logger: this.logger.withPrefix('SessionManager')
});
this.pingPongManager = new ping_pong_manager_js_1.PingPongManager({
pingInterval: options.pingInterval,
pingTimeoutThreshold: options.pingTimeoutThreshold,
logger: this.logger.withPrefix('PingPongManager')
});
this.connectionStateManager = new connection_state_manager_js_1.ConnectionStateManager({
reconnectStrategy: options.reconnectStrategy === 'fibonacci' ? 'exponential' : options.reconnectStrategy,
retryDelay: options.retryDelay,
maxRetryDelay: options.maxRetryDelay,
logger: this.logger.withPrefix('ConnectionStateManager')
});
}
/**
* Подключается к серверу WebSocket
* @param {Record<string, any>} [params={}] Дополнительные параметры для подключения
* @returns {Promise<boolean>} Успешность подключения
*/
async connect(params = {}) {
try {
// Если клиент уже существует, проверяем его состояние
if (this.client) {
if (this.client.isConnected()) {
this.logger.debug(`Уже подключен к ${this.namespace}`);
return true;
}
else {
// Закрываем существующее соединение
this.client.close();
this.client = null;
}
}
// Формируем URL для подключения
const url = this.buildConnectionUrl(params);
this.logger.debug(`Подключение к ${url}`, {
namespace: String(this.namespace),
params: Object.keys(params)
});
// Создаем новый WebSocket клиент
this.client = new websocket_client_js_1.WebSocketClient(url, {
...this.options,
namespace: String(this.namespace),
// Преобразуем объект Logger в функцию логирования для WebSocketClient
logger: (level, message, data) => {
if (level === 'error')
this.logger.error(message, data);
else if (level === 'warn')
this.logger.warn(message, data);
else if (level === 'info')
this.logger.info(message, data);
else
this.logger.debug(message, data);
}
});
// Устанавливаем обработчики событий
this.setupEventHandlers();
// Подключаемся к серверу
await this.client.connect();
// Регистрируем клиент в менеджере ping/pong
this.pingPongManager.registerClient(this.namespace, this.client);
// Обновляем состояние подключения
this.connectionStateManager.registerSuccessfulConnection(this.namespace);
// Включаем механизм ping/pong, если настроено
if (this.options.enableAutoPing !== false) {
this.pingPongManager.enablePingPong(this.namespace);
}
return true;
}
catch (error) {
this.logger.error(`Ошибка при подключении к ${this.namespace}`, error);
// Обновляем состояние подключения
this.connectionStateManager.registerDisconnection(this.namespace);
return false;
}
}
/**
* Формирует URL для подключения
* @param {Record<string, any>} params Параметры для подключения
* @returns {string} URL для подключения
*/
buildConnectionUrl(params) {
let baseUrl;
let namespaceStr = '';
// Добавляем namespace в URL путь (стандартный подход Socket.IO)
if (this.namespace !== websocket_namespaces_constants_js_1.WebSocketNamespace.DEFAULT) {
namespaceStr = String(this.namespace);
if (!namespaceStr.startsWith('/')) {
namespaceStr = '/' + namespaceStr;
}
}
// Формируем правильный URL для Socket.IO
if (this.baseURL.endsWith('/socket.io') || this.baseURL.endsWith('/socket.io/')) {
// Для случая когда URL заканчивается на /socket.io, убираем слеш
const cleanBaseUrl = this.baseURL.endsWith('/')
? this.baseURL.slice(0, -1)
: this.baseURL;
// Добавляем namespace к URL
baseUrl = cleanBaseUrl + namespaceStr;
}
else {
// Для случая когда URL не содержит /socket.io
baseUrl = this.baseURL + namespaceStr;
}
// Создаем URL объект с параметрами
const url = new URL(baseUrl);
// Добавляем обязательные параметры для Socket.IO
url.searchParams.append('EIO', '4');
url.searchParams.append('transport', 'websocket');
// Добавляем параметры к URL
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, String(value));
}
});
return url.toString();
}
/**
* Устанавливает обработчики событий для WebSocket клиента
*/
setupEventHandlers() {
if (!this.client)
return;
// Добавляем обработчик для успешного подключения
this.client.on('open', () => {
this.logger.debug(`Подключение к ${this.namespace} установлено`, {
socketId: this.client?.getSocketId()
});
// Отправляем сообщение аутентификации, если задан API ключ
if (this.options.apiKey) {
this.authenticate();
}
});
// Логирование ошибок
this.client.on('error', (error) => {
this.logger.error(`Ошибка соединения с ${this.namespace}`, error);
});
// Логирование разъединений
this.client.on('close', (event) => {
this.logger.debug(`Соединение с ${this.namespace} закрыто: ${event.code || 'нет кода'}, ${event.reason || 'Причина не указана'}`);
// Обновляем состояние подключения
this.connectionStateManager.registerDisconnection(this.namespace);
// Отключаем ping/pong
this.pingPongManager.disablePingPong(this.namespace);
});
}
/**
* Отправляет аутентификационные данные на сервер
*/
authenticate() {
if (!this.client || !this.options.apiKey)
return;
try {
this.logger.debug(`Отправка аутентификации для ${this.namespace}`);
// Отправляем сообщение аутентификации
this.send(websocket_events_constants_js_1.WebSocketEvents.AUTHENTICATE, {
token: this.options.apiKey
});
}
catch (error) {
this.logger.error(`Ошибка при отправке аутентификации: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Отключается от сервера WebSocket
*/
disconnect() {
// Отключаем ping/pong
this.pingPongManager.disablePingPong(this.namespace);
// Закрываем соединение
if (this.client) {
this.client.close();
this.client = null;
}
// Обновляем состояние подключения
this.connectionStateManager.registerDisconnection(this.namespace);
this.logger.debug(`Отключено от ${this.namespace}`);
}
/**
* Проверяет, подключен ли клиент к серверу
* @returns {boolean} true, если клиент подключен
*/
isConnected() {
return !!this.client && this.client.isConnected();
}
/**
* Получает ID сокета
* @returns {string | null} ID сокета или null, если не подключен
*/
getSocketId() {
return this.client ? this.client.getSocketId() : null;
}
/**
* Отправляет событие на сервер
* @param {string} eventType Тип события
* @param {any} [data] Данные события
* @returns {boolean} Успешность отправки
*/
send(eventType, data) {
if (!this.client) {
this.logger.warn(`Попытка отправить событие ${eventType}, но клиент не подключен`);
return false;
}
return this.client.send({
event: eventType,
data
});
}
/**
* Отправляет событие и ожидает ответа
* @param {string} eventType Тип события
* @param {any} data Данные события
* @param {number} [timeout=5000] Таймаут ожидания ответа в миллисекундах
* @returns {Promise<any>} Ответ сервера
*/
async emitWithAck(eventType, data, timeout = 5000) {
if (!this.client) {
throw new Error(`Попытка отправить событие ${eventType} с подтверждением, но клиент не подключен`);
}
return this.client.emitWithAck(eventType, data, timeout);
}
/**
* Добавляет обработчик события
* @param {string} eventType Тип события
* @param {(data: any) => void} handler Обработчик события
*/
on(eventType, handler) {
if (!this.client) {
this.logger.warn(`Попытка добавить обработчик для события ${eventType}, но клиент не подключен`);
return;
}
this.client.on(eventType, handler);
}
/**
* Удаляет обработчик события
* @param {string} eventType Тип события
* @param {(data: any) => void} [handler] Обработчик события (если не указан, удаляются все обработчики)
*/
off(eventType, handler) {
if (!this.client)
return;
this.client.off(eventType, handler);
}
}
exports.BaseWebSocketClient = BaseWebSocketClient;
//# sourceMappingURL=base-ws-client.js.map