solver-sdk
Version:
SDK for API integration
288 lines • 12.6 kB
JavaScript
"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