// Сервис управления подключением к серверу export type ConnectionStatus = 'connected' | 'connecting' | 'disconnected'; type ConnectionStatusCallback = (status: ConnectionStatus) => void; class ConnectionService { private status: ConnectionStatus = 'disconnected'; private listeners: Set = new Set(); private reconnectInterval: number | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = Infinity; // Бесконечные попытки private reconnectDelay = 3000; // 3 секунды между попытками private checkInterval: number | null = null; private apiBaseUrl: string; constructor() { this.apiBaseUrl = import.meta.env.VITE_API_BASE_URL || ''; // Если API не настроен, сразу устанавливаем статус connected if (!this.apiBaseUrl) { this.setStatus('connected'); return; } this.startConnectionCheck(); this.initializeConnection(); } // Инициализация подключения private async initializeConnection() { if (!this.apiBaseUrl) { // Если API не настроен, считаем что подключение не требуется (используется localStorage) this.setStatus('connected'); return; } this.setStatus('connecting'); await this.checkConnection(); } // Проверка подключения private async checkConnection(): Promise { if (!this.apiBaseUrl) { this.setStatus('connected'); return true; } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 секунды таймаут // Пробуем несколько endpoints для проверки подключения const endpoints = ['/health', '/districts', '/buildings']; let connected = false; let lastError: Error | null = null; for (const endpoint of endpoints) { try { const response = await fetch(`${this.apiBaseUrl}${endpoint}`, { method: 'GET', signal: controller.signal, cache: 'no-cache', headers: { 'Content-Type': 'application/json', }, }); // Если получили ответ (даже с ошибкой 404, 403 и т.д.), сервер доступен // 404 означает, что сервер работает, но endpoint не найден - это нормально if (response.status < 500) { connected = true; break; } } catch (e) { lastError = e instanceof Error ? e : new Error(String(e)); // Пробуем следующий endpoint continue; } } clearTimeout(timeoutId); if (connected) { this.setStatus('connected'); this.reconnectAttempts = 0; this.stopReconnect(); return true; } else { // Проверяем, не была ли это ошибка сети (CORS, таймаут и т.д.) const isNetworkError = lastError && ( lastError.name === 'AbortError' || lastError.message.includes('Failed to fetch') || lastError.message.includes('NetworkError') || lastError.message.includes('CORS') ); if (isNetworkError) { this.setStatus('disconnected'); this.startReconnect(); return false; } else { // Если это не ошибка сети, возможно сервер работает, но endpoint недоступен // В этом случае считаем что подключение есть this.setStatus('connected'); this.stopReconnect(); return true; } } } catch (error) { // Общая ошибка - считаем что нет подключения this.setStatus('disconnected'); this.startReconnect(); return false; } } // Установка статуса и уведомление слушателей private setStatus(newStatus: ConnectionStatus) { if (this.status !== newStatus) { this.status = newStatus; this.listeners.forEach(callback => callback(newStatus)); } } // Начало процесса переподключения private startReconnect() { if (this.reconnectInterval) { return; // Уже переподключаемся } if (this.reconnectAttempts >= this.maxReconnectAttempts) { return; // Достигнут лимит попыток } this.reconnectInterval = window.setInterval(async () => { this.reconnectAttempts++; this.setStatus('connecting'); const connected = await this.checkConnection(); if (connected) { this.stopReconnect(); } }, this.reconnectDelay); } // Остановка процесса переподключения private stopReconnect() { if (this.reconnectInterval) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } this.reconnectAttempts = 0; } // Периодическая проверка подключения private startConnectionCheck() { // Проверяем каждые 10 секунд this.checkInterval = window.setInterval(async () => { if (this.status === 'connected') { await this.checkConnection(); } }, 10000); } // Получить текущий статус getStatus(): ConnectionStatus { return this.status; } // Подписаться на изменения статуса subscribe(callback: ConnectionStatusCallback): () => void { this.listeners.add(callback); // Сразу вызываем callback с текущим статусом callback(this.status); // Возвращаем функцию отписки return () => { this.listeners.delete(callback); }; } // Принудительная проверка подключения async forceCheck(): Promise { if (!this.apiBaseUrl) { // Если API не настроен, считаем что подключение не требуется this.setStatus('connected'); return true; } this.setStatus('connecting'); return await this.checkConnection(); } // Очистка ресурсов destroy() { this.stopReconnect(); if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; } this.listeners.clear(); } } // Создаём единственный экземпляр сервиса export const connectionService = new ConnectionService();