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