/** * Shutdown management for Node.js applications * * Features: * - Automatic signal handling (SIGTERM, SIGINT, etc.) * - Configurable shutdown timeout * - Multiple cleanup callbacks with error handling * - Platform-specific signal support (Windows/Unix) */ import type { ShutdownCallback, ShutdownOptions, ShutdownResult } from './types'; export class Shutdown { private static instance: Shutdown | null = null; private isShuttingDown = false; private shutdownTimeout = 30000; // 30 seconds default private callbacks: ShutdownCallback[] = []; private signalHandlersRegistered = false; constructor(options: ShutdownOptions = {}) { this.shutdownTimeout = options.timeout || 30000; if (options.autoRegister !== false) { this.setupSignalHandlers(); } } /** * Get or create singleton instance */ static getInstance(options?: ShutdownOptions): Shutdown { if (!Shutdown.instance) { Shutdown.instance = new Shutdown(options); } return Shutdown.instance; } /** * Reset singleton instance (mainly for testing) */ static reset(): void { Shutdown.instance = null; } /** * Register a cleanup callback */ onShutdown(callback: ShutdownCallback): void { if (this.isShuttingDown) { return; } this.callbacks.push(callback); } /** * Set shutdown timeout in milliseconds */ setTimeout(timeout: number): void { if (timeout <= 0) { throw new Error('Shutdown timeout must be positive'); } this.shutdownTimeout = timeout; } /** * Get current shutdown state */ isShutdownInProgress(): boolean { return this.isShuttingDown; } /** * Get number of registered callbacks */ getCallbackCount(): number { return this.callbacks.length; } /** * Initiate graceful shutdown */ async shutdown(_signal?: string): Promise { if (this.isShuttingDown) { return { success: false, callbacksExecuted: 0, callbacksFailed: 0, duration: 0, error: 'Shutdown already in progress', }; } this.isShuttingDown = true; const startTime = Date.now(); const shutdownPromise = this.executeCallbacks(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout); }); let result: ShutdownResult; try { const callbackResult = await Promise.race([shutdownPromise, timeoutPromise]); const duration = Date.now() - startTime; result = { success: true, callbacksExecuted: callbackResult.executed, callbacksFailed: callbackResult.failed, duration, error: callbackResult.failed > 0 ? `${callbackResult.failed} callbacks failed` : undefined, }; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); result = { success: false, callbacksExecuted: 0, callbacksFailed: 0, duration, error: errorMessage, }; } // Don't call process.exit here - let the caller decide return result; } /** * Initiate shutdown and exit process */ async shutdownAndExit(signal?: string, exitCode = 0): Promise { const result = await this.shutdown(signal); const finalExitCode = result.success ? exitCode : 1; process.exit(finalExitCode); } /** * Execute all registered callbacks */ private async executeCallbacks(): Promise<{ executed: number; failed: number }> { if (this.callbacks.length === 0) { return { executed: 0, failed: 0 }; } const results = await Promise.allSettled( this.callbacks.map(async callback => { await callback(); }) ); const failed = results.filter(result => result.status === 'rejected').length; const executed = results.length; return { executed, failed }; } /** * Setup signal handlers for graceful shutdown */ private setupSignalHandlers(): void { if (this.signalHandlersRegistered) { return; } // Platform-specific signals const signals: NodeJS.Signals[] = process.platform === 'win32' ? ['SIGINT', 'SIGTERM'] : ['SIGTERM', 'SIGINT', 'SIGUSR2']; signals.forEach(signal => { process.once(signal, () => { // Changed from 'on' to 'once' to prevent multiple handlers this.shutdownAndExit(signal).catch(() => { process.exit(1); }); }); }); // Handle uncaught exceptions process.on('uncaughtException', () => { this.shutdownAndExit('uncaughtException', 1).catch(() => { process.exit(1); }); }); // Handle unhandled promise rejections process.on('unhandledRejection', () => { this.shutdownAndExit('unhandledRejection', 1).catch(() => { process.exit(1); }); }); this.signalHandlersRegistered = true; } }