/** * 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 { PrioritizedShutdownCallback, ShutdownCallback, ShutdownOptions, ShutdownResult, } from './types'; // Global flag that works across all processes/workers declare global { var __SHUTDOWN_SIGNAL_RECEIVED__: boolean | undefined; } export class Shutdown { private static instance: Shutdown | null = null; private isShuttingDown = false; private signalReceived = false; // Track if shutdown signal was received private shutdownTimeout = 30000; // 30 seconds default private callbacks: PrioritizedShutdownCallback[] = []; 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 with priority (lower numbers = higher priority) */ onShutdown(callback: ShutdownCallback, priority: number = 50, name?: string): void { if (this.isShuttingDown) { return; } this.callbacks.push({ callback, priority, name }); } /** * Register a high priority shutdown callback (for queues, critical services) */ onShutdownHigh(callback: ShutdownCallback, name?: string): void { this.onShutdown(callback, 10, name); } /** * Register a medium priority shutdown callback (for databases, connections) */ onShutdownMedium(callback: ShutdownCallback, name?: string): void { this.onShutdown(callback, 50, name); } /** * Register a low priority shutdown callback (for loggers, cleanup) */ onShutdownLow(callback: ShutdownCallback, name?: string): void { this.onShutdown(callback, 90, name); } /** * 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; } /** * Check if shutdown signal was received (for quick checks in running jobs) */ isShutdownSignalReceived(): boolean { return this.signalReceived || 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 in priority order */ private async executeCallbacks(): Promise<{ executed: number; failed: number }> { if (this.callbacks.length === 0) { return { executed: 0, failed: 0 }; } // Sort callbacks by priority (lower numbers = higher priority = execute first) const sortedCallbacks = [...this.callbacks].sort((a, b) => a.priority - b.priority); let executed = 0; let failed = 0; // Execute callbacks in order by priority for (const { callback, name, priority } of sortedCallbacks) { try { await callback(); executed++; } catch (error) { failed++; if (name) { console.error(`✗ Shutdown failed: ${name} (priority: ${priority})`, error); } } } 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.on(signal, () => { // Only process if not already shutting down if (!this.isShuttingDown) { // Set signal flag immediately for quick checks this.signalReceived = true; // Also set global flag for workers/other processes globalThis.__SHUTDOWN_SIGNAL_RECEIVED__ = true; this.shutdownAndExit(signal).catch(() => { process.exit(1); }); } }); }); // Handle uncaught exceptions process.on('uncaughtException', () => { this.signalReceived = true; this.shutdownAndExit('uncaughtException', 1).catch(() => { process.exit(1); }); }); // Handle unhandled promise rejections process.on('unhandledRejection', () => { this.signalReceived = true; this.shutdownAndExit('unhandledRejection', 1).catch(() => { process.exit(1); }); }); this.signalHandlersRegistered = true; } }