/** * Service intelligent pour monitoring serveur via Server-Sent Events * Avec fallback polling et reconnexion automatique */ import { API_CONFIG } from '../config/api'; export interface ServerStatusEvent { status: 'UP' | 'DOWN' | 'MAINTENANCE'; timestamp: string; message: string; system?: { version: string; uptimeSeconds: number; activeConnections: number; }; } export interface ServerStatusListener { (isOnline: boolean, event?: ServerStatusEvent): void; } export class ServerStatusService { private eventSource: EventSource | null = null; private listeners: ServerStatusListener[] = []; private isOnline: boolean = true; private reconnectAttempts: number = 0; private maxReconnectAttempts: number = 5; private reconnectDelay: number = 1000; private fallbackPolling: boolean = false; private fallbackInterval: NodeJS.Timeout | null = null; private lastEventTime: number = Date.now(); private heartbeatTimeout: NodeJS.Timeout | null = null; constructor() { this.handleVisibilityChange = this.handleVisibilityChange.bind(this); // Vérifier si on est côté client (évite les erreurs SSR) if (typeof window !== 'undefined' && typeof document !== 'undefined') { document.addEventListener('visibilitychange', this.handleVisibilityChange); } } /** * Démarrer le monitoring serveur */ public start(): void { console.log('🚀 Démarrage monitoring serveur SSE'); this.initSSE(); } /** * Arrêter le monitoring */ public stop(): void { console.log('🛑 Arrêt monitoring serveur'); this.cleanup(); // Vérifier si on est côté client avant de supprimer l'event listener if (typeof window !== 'undefined' && typeof document !== 'undefined') { document.removeEventListener('visibilitychange', this.handleVisibilityChange); } } /** * S'abonner aux changements de statut */ public onStatusChange(listener: ServerStatusListener): () => void { this.listeners.push(listener); // Retourner une fonction de désabonnement return () => { const index = this.listeners.indexOf(listener); if (index > -1) { this.listeners.splice(index, 1); } }; } /** * Obtenir le statut actuel */ public getCurrentStatus(): boolean { return this.isOnline; } /** * Initialiser Server-Sent Events */ private initSSE(): void { try { // Retirer /api/v1 de l'URL de base pour l'endpoint SSE const baseUrl = API_CONFIG.baseURL.replace('/api/v1', ''); const sseUrl = `${baseUrl}/api/server-status/stream`; console.log('📡 Connexion SSE:', sseUrl); this.eventSource = new EventSource(sseUrl); this.eventSource.onopen = (event) => { console.log('✅ SSE connecté avec succès'); this.reconnectAttempts = 0; this.fallbackPolling = false; this.stopFallbackPolling(); this.updateStatus(true); this.resetHeartbeatTimeout(); }; this.eventSource.onmessage = (event) => { try { const statusEvent: ServerStatusEvent = JSON.parse(event.data); console.log('📊 SSE reçu:', statusEvent); this.lastEventTime = Date.now(); this.updateStatus(statusEvent.status === 'UP', statusEvent); this.resetHeartbeatTimeout(); } catch (error) { console.error('❌ Erreur parsing SSE:', error); } }; this.eventSource.onerror = (event) => { console.warn('⚠️ Erreur SSE, tentative de reconnexion...'); this.handleSSEError(); }; } catch (error) { console.error('❌ Impossible d\'initialiser SSE:', error); this.startFallbackPolling(); } } /** * Gérer les erreurs SSE et reconnexion */ private handleSSEError(): void { this.updateStatus(false); if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // Backoff exponentiel console.log(`🔄 Reconnexion SSE dans ${delay}ms (tentative ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => { this.cleanup(); this.initSSE(); }, delay); } else { console.warn('❌ Échec reconnexion SSE, passage en mode polling'); this.startFallbackPolling(); } } /** * Démarrer le polling de secours */ private startFallbackPolling(): void { if (this.fallbackPolling) return; console.log('🔄 Démarrage polling de secours'); this.fallbackPolling = true; const poll = async () => { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // Retirer /api/v1 pour l'endpoint de fallback aussi const baseUrl = API_CONFIG.baseURL.replace('/api/v1', ''); const response = await fetch(`${baseUrl}/api/server-status/current`, { signal: controller.signal, headers: { 'Accept': 'application/json' } }); clearTimeout(timeoutId); if (response.ok) { const statusEvent: ServerStatusEvent = await response.json(); this.updateStatus(statusEvent.status === 'UP', statusEvent); // Essayer de rétablir SSE si le serveur est de nouveau UP if (statusEvent.status === 'UP' && this.reconnectAttempts >= this.maxReconnectAttempts) { console.log('🔄 Serveur récupéré, tentative de rétablissement SSE'); this.stopFallbackPolling(); this.reconnectAttempts = 0; setTimeout(() => this.initSSE(), 2000); } } else { this.updateStatus(false); } } catch (error) { console.warn('⚠️ Erreur polling de secours:', error); this.updateStatus(false); } }; // Poll immédiat puis toutes les 10 secondes poll(); this.fallbackInterval = setInterval(poll, 10000); } /** * Arrêter le polling de secours */ private stopFallbackPolling(): void { if (this.fallbackInterval) { clearInterval(this.fallbackInterval); this.fallbackInterval = null; } this.fallbackPolling = false; } /** * Timeout de heartbeat pour détecter les connexions perdues */ private resetHeartbeatTimeout(): void { if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); } // Si pas de message pendant 45 secondes (heartbeat = 30s), considérer comme déconnecté this.heartbeatTimeout = setTimeout(() => { console.warn('💔 Pas de heartbeat SSE, connexion probablement perdue'); this.handleSSEError(); }, 45000); } /** * Gérer la visibilité de l'onglet */ private handleVisibilityChange(): void { // Vérifier si on est côté client if (typeof document === 'undefined') return; if (!document.hidden) { console.log('👁️ Onglet redevenu visible, vérification connexion SSE'); // Vérifier si la connexion SSE est toujours active if (this.eventSource && this.eventSource.readyState === EventSource.CLOSED) { console.log('🔄 SSE fermé, reconnexion...'); this.reconnectAttempts = 0; this.initSSE(); } } } /** * Mettre à jour le statut et notifier les listeners */ private updateStatus(isOnline: boolean, event?: ServerStatusEvent): void { const wasOnline = this.isOnline; this.isOnline = isOnline; if (wasOnline !== isOnline) { console.log(`🔄 Statut serveur changé: ${isOnline ? 'ON' : 'OFF'}LINE`); } // Notifier tous les listeners this.listeners.forEach(listener => { try { listener(isOnline, event); } catch (error) { console.error('❌ Erreur dans listener status:', error); } }); } /** * Nettoyer les ressources */ private cleanup(): void { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } this.stopFallbackPolling(); if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = null; } } } // Instance singleton avec initialisation paresseuse let _serverStatusService: ServerStatusService | null = null; export const getServerStatusService = (): ServerStatusService => { if (!_serverStatusService && typeof window !== 'undefined') { _serverStatusService = new ServerStatusService(); } return _serverStatusService!; }; // Pour la compatibilité avec l'ancien code export const serverStatusService = typeof window !== 'undefined' ? getServerStatusService() : null;