Files
mkd/services/connectionService.ts
2026-02-04 00:17:04 +05:00

207 lines
6.8 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Сервис управления подключением к серверу
export type ConnectionStatus = 'connected' | 'connecting' | 'disconnected';
type ConnectionStatusCallback = (status: ConnectionStatus) => void;
class ConnectionService {
private status: ConnectionStatus = 'disconnected';
private listeners: Set<ConnectionStatusCallback> = 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<boolean> {
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<boolean> {
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();