import type { H3Event } from 'h3'; interface ServiceCheckResult { status: 'up' | 'down' | 'slow'; responseTime?: number; error?: string; } class HealthChecker { private static instance: HealthChecker; private isReady: boolean = false; private startupChecks: Map = new Map(); private requiredServices = ['nocodb', 'directus', 'minio']; private constructor() { this.initializeStartupChecks(); } static getInstance(): HealthChecker { if (!HealthChecker.instance) { HealthChecker.instance = new HealthChecker(); } return HealthChecker.instance; } private initializeStartupChecks() { this.requiredServices.forEach(service => { this.startupChecks.set(service, false); }); } async checkServiceWithRetry( serviceName: string, checkFunction: () => Promise, maxRetries: number = 3, retryDelay: number = 1000 ): Promise { let lastError: string | undefined; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const result = await checkFunction(); if (result.status === 'up' || result.status === 'slow') { this.startupChecks.set(serviceName, true); return result; } lastError = result.error; } catch (error) { lastError = error instanceof Error ? error.message : 'Unknown error'; } if (attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, attempt))); } } return { status: 'down', error: lastError || 'Max retries exceeded' }; } markServiceReady(serviceName: string) { this.startupChecks.set(serviceName, true); this.checkOverallReadiness(); } markServiceDown(serviceName: string) { this.startupChecks.set(serviceName, false); this.isReady = false; } private checkOverallReadiness() { const allRequired = this.requiredServices.every(service => this.startupChecks.get(service) === true ); if (allRequired) { this.isReady = true; console.log('[HealthChecker] All required services are ready'); } } isApplicationReady(): boolean { return this.isReady; } getStartupStatus(): { [key: string]: boolean } { const status: { [key: string]: boolean } = {}; this.startupChecks.forEach((value, key) => { status[key] = value; }); return status; } async performStartupHealthChecks(): Promise { console.log('[HealthChecker] Performing startup health checks...'); // Import check functions dynamically to avoid circular dependencies const { checkNocoDB, checkDirectus, checkMinIO } = await import('./service-checks'); // Check all services in parallel const checks = await Promise.all([ this.checkServiceWithRetry('nocodb', checkNocoDB, 5, 2000), this.checkServiceWithRetry('directus', checkDirectus, 5, 2000), this.checkServiceWithRetry('minio', checkMinIO, 5, 2000) ]); // Log results checks.forEach((result, index) => { const serviceName = ['nocodb', 'directus', 'minio'][index]; console.log(`[HealthChecker] ${serviceName}: ${result.status}${result.error ? ` - ${result.error}` : ''}`); }); this.checkOverallReadiness(); return this.isReady; } } export const healthChecker = HealthChecker.getInstance(); // Middleware to check if app is ready export function createReadinessMiddleware() { return defineEventHandler(async (event: H3Event) => { // Skip health check endpoint itself if (event.node.req.url === '/api/health') { return; } // During startup, return 503 for all requests except health if (!healthChecker.isApplicationReady()) { const startupStatus = healthChecker.getStartupStatus(); setResponseStatus(event, 503); return { error: 'Service Unavailable', message: 'Application is starting up. Please wait...', startupStatus }; } }); }