stock-bot/libs/core/shutdown/src/shutdown.ts
2025-06-26 15:44:52 -04:00

93 lines
No EOL
2.5 KiB
TypeScript

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<void> {
if (this.isShuttingDown) { return };
this.isShuttingDown = true;
const timeout = new Promise<never>((_, 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<void> {
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;
}
}