UNPKG

solver-sdk

Version:
288 lines 12.6 kB
"use strict"; /** * 🔌 Project Sync WebSocket Client * * Клиент для работы с real-time уведомлениями о статусе синхронизации проектов. * Интегрируется с backend ProjectSyncGateway через Socket.io. * * @features * - Real-time push notifications о статусе индексации * - Auto-reconnection при разрыве соединения * - Graceful fallback к REST polling * - TypeScript поддержка для всех событий */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ProjectSyncClient = void 0; const socket_io_client_1 = require("socket.io-client"); /** * 🔌 WebSocket клиент для project sync уведомлений */ class ProjectSyncClient { constructor(options) { this.socket = null; this.connected = false; this.retryCount = 0; this.reconnectTimer = null; this.subscribedProjects = new Set(); // Event handlers this.eventHandlers = new Map(); this.baseURL = options.baseURL; this.options = { connectionTimeout: 10000, maxRetries: 5, retryDelay: 2000, debug: false, ...options }; // Инициализируем logger this.logger = { log: (msg) => this.options.debug && console.log(`[ProjectSyncClient] ${msg}`), warn: (msg) => this.options.debug && console.warn(`[ProjectSyncClient] ${msg}`), error: (msg) => console.error(`[ProjectSyncClient] ${msg}`), debug: (msg) => this.options.debug && console.debug(`[ProjectSyncClient] ${msg}`) }; // Инициализируем event handlers map const eventTypes = ['sync-status-update', 'sync-progress', 'sync-completed', 'error', 'connected', 'disconnected']; eventTypes.forEach(type => { this.eventHandlers.set(type, new Set()); }); } /** * 🔌 Подключение к WebSocket */ async connect() { return new Promise((resolve, reject) => { try { this.logger.debug(`Подключение к ${this.baseURL}/project-sync`); // Извлекаем токен из Authorization header для query параметра const authHeader = this.options.headers?.['Authorization'] || this.options.headers?.['authorization']; const token = authHeader?.replace(/^Bearer\s+/i, ''); // Создаем Socket.io соединение this.socket = (0, socket_io_client_1.io)(`${this.baseURL}/project-sync`, { query: token ? { token } : {}, // ✅ Передаем токен в query для авторизации transports: ['websocket', 'polling'], timeout: this.options.connectionTimeout, reconnectionAttempts: 0, // Отключаем автоматический reconnect Socket.io extraHeaders: this.options.headers || {}, }); // Обработчик успешного подключения this.socket.on('connect', () => { this.connected = true; this.retryCount = 0; this.logger.log('✅ WebSocket подключение установлено'); this.emit('connected', {}); resolve(); }); // Обработчик отключения this.socket.on('disconnect', (reason) => { this.connected = false; this.logger.warn(`🔌 WebSocket отключен: ${reason}`); this.emit('disconnected', { reason }); // Автоматический переподключение если отключение не намеренное if (reason === 'io server disconnect') { this.scheduleReconnect(); } }); // Обработчик ошибок подключения this.socket.on('connect_error', (error) => { this.logger.error(`❌ Ошибка подключения: ${error.message}`); reject(new Error(`WebSocket connection failed: ${error.message}`)); }); // 📊 Обработчики project sync событий this.setupEventHandlers(); // Таймаут подключения setTimeout(() => { if (!this.connected) { reject(new Error('WebSocket connection timeout')); } }, this.options.connectionTimeout); } catch (error) { reject(error); } }); } /** * ❌ Отключение от WebSocket */ disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.socket) { this.socket.disconnect(); this.socket = null; } this.connected = false; this.subscribedProjects.clear(); this.logger.log('🔌 WebSocket отключен вручную'); } /** * 📡 Подписка на проект для получения уведомлений */ subscribeToProject(projectId, userId) { if (!this.connected || !this.socket) { this.logger.warn(`⚠️ Попытка подписки на проект ${projectId} без соединения`); return; } const joinData = { projectId, userId }; this.socket.emit('join-project-sync', joinData); this.subscribedProjects.add(projectId); this.logger.debug(`📡 Подписка на проект: ${projectId}`); } /** * 📡 Отписка от проекта */ unsubscribeFromProject(projectId) { if (!this.connected || !this.socket) { return; } this.socket.emit('leave-project-sync', { projectId }); this.subscribedProjects.delete(projectId); this.logger.debug(`📡 Отписка от проекта: ${projectId}`); } /** * 👂 Подписка на события */ on(event, handler) { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.add(handler); } } /** * 👂 Отписка от событий */ off(event, handler) { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.delete(handler); } } /** * 📢 Эмиссия событий */ emit(event, data) { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.forEach(handler => { try { handler(data); } catch (error) { this.logger.error(`Error in event handler for ${event}: ${error}`); } }); } } /** * 🔧 Настройка обработчиков событий от backend */ setupEventHandlers() { if (!this.socket) return; // 📊 Обновление статуса синхронизации this.socket.on('sync-status-update', (data) => { const safeData = { projectId: data?.projectId || 'unknown', sessionId: data?.sessionId || 'unknown', status: data?.status || 'unknown', progress: data?.progress || 0, message: data?.message || '', timestamp: data?.timestamp || new Date(), chunksProcessed: data?.chunksProcessed || 0, totalChunks: data?.totalChunks || 0, embeddingsCreated: data?.embeddingsCreated || 0 }; this.logger.debug(`📊 Статус обновлен: ${safeData.status} для проекта ${safeData.projectId}`); this.emit('sync-status-update', safeData); }); // 📈 Прогресс синхронизации this.socket.on('sync-progress', (data) => { const safeData = { projectId: data?.projectId || 'unknown', sessionId: data?.sessionId || 'unknown', stage: data?.stage || 'unknown', progress: data?.progress || 0, currentChunk: data?.currentChunk || 0, totalChunks: data?.totalChunks || 0, estimatedTimeRemaining: data?.estimatedTimeRemaining || 0, details: data?.details || '' }; this.logger.debug(`📈 Прогресс: ${safeData.progress}% (${safeData.stage}) для проекта ${safeData.projectId}`); this.emit('sync-progress', safeData); }); // ✅ Синхронизация завершена this.socket.on('sync-completed', (data) => { const safeData = { projectId: data?.projectId || 'unknown', sessionId: data?.sessionId || 'unknown', success: data?.success !== false, // По умолчанию true message: data?.message || '', totalProcessed: data?.totalProcessed || 0, duration: data?.duration || 0, error: data?.error || '' }; this.logger.log(`✅ Синхронизация завершена: ${safeData.success ? 'SUCCESS' : 'FAILED'} для проекта ${safeData.projectId}`); this.emit('sync-completed', safeData); }); // ❌ Ошибки this.socket.on('error', (data) => { // Безопасная обработка undefined/неполных данных const safeData = { projectId: data?.projectId || 'unknown', sessionId: data?.sessionId || 'unknown', error: data?.error || (typeof data === 'object' ? JSON.stringify(data) : String(data)) || 'Unknown error', code: data?.code || 'UNKNOWN_ERROR', timestamp: data?.timestamp || new Date(), recoverable: data?.recoverable !== false // По умолчанию true }; this.logger.error(`❌ Ошибка для проекта ${safeData.projectId}: ${safeData.error}`); this.emit('error', safeData); }); } /** * 🔄 Планирование переподключения */ scheduleReconnect() { if (this.retryCount >= this.options.maxRetries) { this.logger.error(`❌ Превышено максимальное количество попыток переподключения (${this.options.maxRetries})`); return; } const delay = Math.min(this.options.retryDelay * Math.pow(2, this.retryCount), 30000); this.retryCount++; this.logger.warn(`🔄 Переподключение через ${delay}ms (попытка ${this.retryCount}/${this.options.maxRetries})`); this.reconnectTimer = setTimeout(async () => { try { await this.connect(); // Восстанавливаем подписки на проекты this.subscribedProjects.forEach(projectId => { this.subscribeToProject(projectId); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`❌ Ошибка переподключения: ${errorMessage}`); this.scheduleReconnect(); // Планируем следующую попытку } }, delay); } /** * 🔍 Проверка статуса подключения */ get isConnected() { return this.connected; } /** * 📋 Список подписанных проектов */ get subscribedProjectIds() { return Array.from(this.subscribedProjects); } } exports.ProjectSyncClient = ProjectSyncClient; //# sourceMappingURL=project-sync-client.js.map