UNPKG

solver-sdk

Version:

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

286 lines 13.6 kB
"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