UNPKG

solver-sdk

Version:

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

233 lines 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SseClient = void 0; const event_source_polyfill_1 = require("event-source-polyfill"); // В браузере будем использовать нативный EventSource или полифилл const BrowserEventSource = typeof EventSource !== 'undefined' ? EventSource : event_source_polyfill_1.EventSourcePolyfill; // Проверяем, находимся ли мы в браузере const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; /** * Клиент для работы с Server-Sent Events (SSE) * Поддерживает работу как в браузере, так и в Node.js */ class SseClient { /** * Создает новый SSE клиент * @param {string} url URL для подключения * @param {SseClientOptions} [options] Опции клиента */ constructor(url, options = {}) { /** Экземпляр EventSource */ this.eventSource = null; /** Счетчик попыток переподключения */ this.retryCount = 0; /** Флаг, указывающий, что соединение было закрыто намеренно */ this.intentionallyClosed = false; /** Таймер переподключения */ this.reconnectTimer = null; /** Таймер таймаута соединения */ this.connectionTimeoutTimer = null; /** Обработчики событий */ this.eventHandlers = {}; this.url = url; this.options = { headers: options.headers || {}, connectionTimeout: options.connectionTimeout || 30000, maxRetries: options.maxRetries || 5, retryDelay: options.retryDelay || 1000, maxRetryDelay: options.maxRetryDelay || 30000 }; } /** * Подключается к SSE эндпоинту * @returns {Promise<void>} */ connect() { // Если соединение уже установлено, возвращаем Promise.resolve if (this.eventSource && this.eventSource.readyState === 1) { return Promise.resolve(); } // Сбрасываем флаг намеренного закрытия this.intentionallyClosed = false; return new Promise((resolve, reject) => { try { // Создаем новый экземпляр EventSource const EventSourceImpl = isBrowser ? BrowserEventSource : require('eventsource'); this.eventSource = new EventSourceImpl(this.url, { headers: this.options.headers, withCredentials: true }); // Устанавливаем таймаут соединения this.connectionTimeoutTimer = setTimeout(() => { if (this.eventSource && this.eventSource.readyState !== 1) { reject(new Error('Таймаут подключения SSE')); this.close(); } }, this.options.connectionTimeout); // Проверяем, что this.eventSource не null if (!this.eventSource) { reject(new Error('Не удалось создать EventSource')); return; } // Обработчик открытия соединения this.eventSource.onopen = () => { clearTimeout(this.connectionTimeoutTimer); this.retryCount = 0; resolve(); this.dispatchEvent('open', {}); }; // Обработчик сообщений this.eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); this.dispatchEvent('message', data); // Также вызываем специальный обработчик для конкретного типа события, // если такой тип указан в данных if (data && data.type) { this.dispatchEvent(data.type, data); } } catch (error) { this.dispatchEvent('error', { error, message: 'Ошибка при обработке сообщения SSE' }); } }; // Обработчик ошибок this.eventSource.onerror = (error) => { clearTimeout(this.connectionTimeoutTimer); if (this.intentionallyClosed) { return; } // Если соединение уже установлено и произошла ошибка, просто сообщаем об ошибке if (this.eventSource && this.eventSource.readyState === 1) { this.dispatchEvent('error', { error, message: 'Ошибка SSE соединения' }); return; } // Если это первая попытка подключения, отклоняем Promise if (this.retryCount === 0) { reject(new Error('Ошибка подключения SSE')); } // Запускаем процесс переподключения this.reconnect(); }; // Настраиваем обработчики для всех добавленных событий for (const eventType in this.eventHandlers) { if (eventType !== 'message' && eventType !== 'open' && eventType !== 'error') { this.eventSource.addEventListener(eventType, (event) => { try { const data = JSON.parse(event.data); this.dispatchEvent(eventType, data); } catch (e) { this.dispatchEvent(eventType, event.data); } }); } } } catch (error) { reject(error); } }); } /** * Закрывает SSE соединение */ close() { this.intentionallyClosed = true; // Очищаем таймеры clearTimeout(this.reconnectTimer); clearTimeout(this.connectionTimeoutTimer); // Закрываем соединение if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } this.dispatchEvent('close', {}); } /** * Добавляет обработчик события * @param {string} eventType Тип события * @param {SseEventHandler} handler Обработчик события */ on(eventType, handler) { if (!this.eventHandlers[eventType]) { this.eventHandlers[eventType] = []; // Если соединение уже установлено, добавляем обработчик события if (this.eventSource && this.eventSource.readyState === 1 && eventType !== 'message' && eventType !== 'open' && eventType !== 'error') { this.eventSource.addEventListener(eventType, (event) => { try { const data = JSON.parse(event.data); this.dispatchEvent(eventType, data); } catch (e) { this.dispatchEvent(eventType, event.data); } }); } } this.eventHandlers[eventType].push(handler); } /** * Удаляет обработчик события * @param {string} eventType Тип события * @param {SseEventHandler} [handler] Обработчик события (если не указан, удаляются все обработчики) */ off(eventType, handler) { if (!this.eventHandlers[eventType]) { return; } if (!handler) { // Если обработчик не указан, удаляем все обработчики для данного события delete this.eventHandlers[eventType]; } else { // Если обработчик указан, удаляем только его this.eventHandlers[eventType] = this.eventHandlers[eventType].filter(h => h !== handler); // Если обработчиков больше нет, удаляем массив if (this.eventHandlers[eventType].length === 0) { delete this.eventHandlers[eventType]; } } } /** * Вызывает обработчики для указанного события * @param {string} eventType Тип события * @param {any} data Данные события */ dispatchEvent(eventType, data) { if (!this.eventHandlers[eventType]) { return; } for (const handler of this.eventHandlers[eventType]) { try { handler(data); } catch (e) { console.error(`Ошибка в обработчике события ${eventType}:`, e); } } } /** * Переподключается к SSE эндпоинту с экспоненциальной задержкой * @private */ reconnect() { // Увеличиваем счетчик попыток this.retryCount++; // Если превышено максимальное количество попыток, прекращаем if (this.retryCount > (this.options.maxRetries || 5)) { this.dispatchEvent('maxRetries', { retries: this.retryCount }); return; } // Вычисляем задержку перед переподключением с экспоненциальным ростом const delay = Math.min((this.options.retryDelay || 1000) * Math.pow(2, this.retryCount - 1), this.options.maxRetryDelay || 30000); // Пытаемся переподключиться после задержки this.reconnectTimer = setTimeout(() => { this.dispatchEvent('reconnect', { attempt: this.retryCount }); this.connect().catch(() => { }); }, delay); } } exports.SseClient = SseClient; //# sourceMappingURL=sse-client.js.map