207 lines
6.8 KiB
TypeScript
207 lines
6.8 KiB
TypeScript
|
|
// Сервис управления подключением к серверу
|
|||
|
|
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();
|