/** * API Connection Status Monitor and Health Checks */ import { EventEmitter } from 'events'; import { ApiError } from './ApiError'; import { CircuitBreakerRegistry } from './RetryHandler'; export type ConnectionStatus = 'connected' | 'disconnected' | 'degraded' | 'checking'; export interface ConnectionHealth { status: ConnectionStatus; lastCheck: Date | null; lastSuccess: Date | null; lastFailure: Date | null; consecutiveFailures: number; totalRequests: number; successfulRequests: number; failedRequests: number; averageResponseTime: number; } export interface HealthCheckResult { healthy: boolean; responseTime: number; error?: string; timestamp: Date; } export class ApiConnectionMonitor extends EventEmitter { private static instance: ApiConnectionMonitor; private health: ConnectionHealth; private isChecking = false; private checkInterval: NodeJS.Timeout | null = null; private healthCheckEndpoint: string; private readonly CHECK_INTERVAL = 30000; // 30 seconds private readonly DEGRADATION_THRESHOLD = 0.7; // 70% failure rate private constructor(healthCheckEndpoint: string = '/health') { super(); this.healthCheckEndpoint = healthCheckEndpoint; this.health = { status: 'disconnected', lastCheck: null, lastSuccess: null, lastFailure: null, consecutiveFailures: 0, totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, }; } static getInstance(healthCheckEndpoint?: string): ApiConnectionMonitor { if (!ApiConnectionMonitor.instance) { ApiConnectionMonitor.instance = new ApiConnectionMonitor(healthCheckEndpoint); } return ApiConnectionMonitor.instance; } /** * Start automatic health monitoring */ startMonitoring(intervalMs?: number): void { if (this.checkInterval) { clearInterval(this.checkInterval); } const interval = intervalMs || this.CHECK_INTERVAL; this.checkInterval = setInterval(() => { this.performHealthCheck(); }, interval); // Initial check this.performHealthCheck(); } /** * Stop automatic health monitoring */ stopMonitoring(): void { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; } } /** * Perform a manual health check */ async performHealthCheck(): Promise { if (this.isChecking) { return { healthy: false, responseTime: 0, error: 'Check already in progress', timestamp: new Date(), }; } this.isChecking = true; const startTime = Date.now(); try { // Try multiple endpoints to determine actual connectivity const baseUrl = this.getBaseUrl(); const endpointsToTry = [ `${baseUrl}${this.healthCheckEndpoint}`, `${baseUrl}/api/health`, `${baseUrl}/status`, baseUrl, // Root endpoint ]; let lastError: Error | null = null; let successfulResponse: Response | null = null; for (const endpoint of endpointsToTry) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); const response = await fetch(endpoint, { method: 'GET', signal: controller.signal, cache: 'no-store', // Add credentials to handle auth credentials: 'include', }); clearTimeout(timeoutId); // Consider any response (even 404) as connectivity success if (response.ok || response.status === 404 || response.status === 401) { successfulResponse = response; break; } } catch (endpointError) { lastError = endpointError as Error; // Try next endpoint continue; } } const responseTime = Date.now() - startTime; if (successfulResponse) { this.recordSuccess(responseTime); this.isChecking = false; return { healthy: true, responseTime, timestamp: new Date(), }; } else { // If we got here, all endpoints failed const errorMessage = lastError?.message || 'All endpoints failed to respond'; this.recordFailure(errorMessage); this.isChecking = false; return { healthy: false, responseTime, error: errorMessage, timestamp: new Date(), }; } } catch (error) { const responseTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.recordFailure(errorMessage); this.isChecking = false; return { healthy: false, responseTime, error: errorMessage, timestamp: new Date(), }; } } /** * Record a successful API request */ recordSuccess(responseTime: number = 0): void { this.health.totalRequests++; this.health.successfulRequests++; this.health.consecutiveFailures = 0; this.health.lastSuccess = new Date(); this.health.lastCheck = new Date(); // Update average response time const total = this.health.successfulRequests; this.health.averageResponseTime = ((this.health.averageResponseTime * (total - 1)) + responseTime) / total; this.updateStatus(); this.emit('success', { responseTime }); } /** * Record a failed API request */ recordFailure(error: string | Error): void { this.health.totalRequests++; this.health.failedRequests++; this.health.consecutiveFailures++; this.health.lastFailure = new Date(); this.health.lastCheck = new Date(); this.updateStatus(); this.emit('failure', { error: typeof error === 'string' ? error : error.message, consecutiveFailures: this.health.consecutiveFailures }); } /** * Get current connection health */ getHealth(): ConnectionHealth { return { ...this.health }; } /** * Get current connection status */ getStatus(): ConnectionStatus { return this.health.status; } /** * Check if API is currently available */ isAvailable(): boolean { return this.health.status === 'connected' || this.health.status === 'degraded'; } /** * Get reliability percentage */ getReliability(): number { if (this.health.totalRequests === 0) return 0; return (this.health.successfulRequests / this.health.totalRequests) * 100; } /** * Reset all statistics */ reset(): void { this.health = { status: 'disconnected', lastCheck: null, lastSuccess: null, lastFailure: null, consecutiveFailures: 0, totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, }; this.emit('reset'); } /** * Get detailed status report for development */ getDebugReport(): string { const reliability = this.getReliability().toFixed(2); const avgTime = this.health.averageResponseTime.toFixed(2); return `API Connection Status: Status: ${this.health.status} Reliability: ${reliability}% Total Requests: ${this.health.totalRequests} Successful: ${this.health.successfulRequests} Failed: ${this.health.failedRequests} Consecutive Failures: ${this.health.consecutiveFailures} Avg Response Time: ${avgTime}ms Last Check: ${this.health.lastCheck?.toISOString() || 'never'} Last Success: ${this.health.lastSuccess?.toISOString() || 'never'} Last Failure: ${this.health.lastFailure?.toISOString() || 'never'}`; } private updateStatus(): void { const reliability = this.health.totalRequests > 0 ? this.health.successfulRequests / this.health.totalRequests : 0; // More nuanced status determination if (this.health.totalRequests === 0) { // No requests yet - don't assume disconnected this.health.status = 'checking'; } else if (this.health.consecutiveFailures >= 3) { // Multiple consecutive failures indicates real connectivity issue this.health.status = 'disconnected'; } else if (reliability < this.DEGRADATION_THRESHOLD && this.health.totalRequests >= 5) { // Only degrade if we have enough samples and reliability is low this.health.status = 'degraded'; } else if (reliability >= this.DEGRADATION_THRESHOLD || this.health.successfulRequests > 0) { // If we have any successes, we're connected this.health.status = 'connected'; } else { // Default to checking if uncertain this.health.status = 'checking'; } // Emit status change events (only on actual changes) if (this.health.status === 'disconnected') { this.emit('disconnected'); } else if (this.health.status === 'degraded') { this.emit('degraded'); } else if (this.health.status === 'connected') { this.emit('connected'); } else if (this.health.status === 'checking') { this.emit('checking'); } } private getBaseUrl(): string { // Try to get base URL from environment or fallback if (typeof window !== 'undefined') { return process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; } return process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; } } /** * Global connection status utility */ export const connectionMonitor = ApiConnectionMonitor.getInstance(); /** * Hook for React components to monitor connection status */ export function useConnectionStatus() { const monitor = ApiConnectionMonitor.getInstance(); return { status: monitor.getStatus(), health: monitor.getHealth(), isAvailable: monitor.isAvailable(), reliability: monitor.getReliability(), checkHealth: () => monitor.performHealthCheck(), getDebugReport: () => monitor.getDebugReport(), }; }