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