import { getLogger } from '@stock-bot/logger'; import { SHUTDOWN_DEFAULTS } from './constants'; import type { ShutdownCallback, ShutdownOptions } from './types'; interface CallbackEntry { callback: ShutdownCallback; priority: number; name?: string; } export class Shutdown { private static instance: Shutdown | null = null; private readonly logger = getLogger('shutdown'); private isShuttingDown = false; private shutdownTimeout: number; private callbacks: CallbackEntry[] = []; private signalHandlersRegistered = false; constructor(options: ShutdownOptions = {}) { this.shutdownTimeout = options.timeout || SHUTDOWN_DEFAULTS.TIMEOUT; if (options.autoRegister !== false) { this.setupSignalHandlers(); } } static getInstance(options?: ShutdownOptions): Shutdown { if (!Shutdown.instance) { Shutdown.instance = new Shutdown(options); } return Shutdown.instance; } /** * Register a cleanup callback */ onShutdown(callback: ShutdownCallback, priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, name?: string): void { if (this.isShuttingDown) { return }; this.callbacks.push({ callback, priority, name }); } /** * Initiate graceful shutdown */ async shutdown(): Promise { if (this.isShuttingDown) { return }; this.isShuttingDown = true; const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout) ); try { await Promise.race([this.executeCallbacks(), timeout]); } catch (error) { this.logger.error('Shutdown failed', error); throw error; } } private async executeCallbacks(): Promise { const sorted = [...this.callbacks].sort((a, b) => a.priority - b.priority); for (const { callback, name } of sorted) { try { await callback(); } catch (error) { this.logger.error(`Shutdown callback failed: ${name || 'unnamed'}`, error); } } } private setupSignalHandlers(): void { if (this.signalHandlersRegistered) { return }; const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT']; signals.forEach(signal => { process.once(signal, async () => { if (!this.isShuttingDown) { try { await this.shutdown(); process.exit(0); } catch { process.exit(1); } } }); }); this.signalHandlersRegistered = true; } }