296 lines
9.8 KiB
TypeScript
Executable File
296 lines
9.8 KiB
TypeScript
Executable File
/**
|
|
* 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; |