import { Inject, Injectable, Logger, type OnApplicationBootstrap, type OnApplicationShutdown, } from '@nestjs/common'; import type { SystemReloadPayload } from '@mosaicstack/types'; import { CommandRegistryService } from '../commands/command-registry.service.js'; import { isMosaicPlugin } from './mosaic-plugin.interface.js'; @Injectable() export class ReloadService implements OnApplicationBootstrap, OnApplicationShutdown { private readonly logger = new Logger(ReloadService.name); private readonly plugins: Map = new Map(); private shutdownHandlerAttached = false; constructor( @Inject(CommandRegistryService) private readonly commandRegistry: CommandRegistryService, ) {} onApplicationBootstrap(): void { if (!this.shutdownHandlerAttached) { process.on('SIGHUP', () => { this.logger.log('SIGHUP received — triggering soft reload'); this.reload('sighup').catch((err: unknown) => { this.logger.error(`SIGHUP reload failed: ${err}`); }); }); this.shutdownHandlerAttached = true; } } onApplicationShutdown(): void { process.removeAllListeners('SIGHUP'); } registerPlugin(name: string, plugin: unknown): void { this.plugins.set(name, plugin); this.logger.log(`Plugin registered: ${name}`); } /** * Soft reload — unload plugins, reload plugins, broadcast. * Does NOT restart the HTTP server or drop connections. */ async reload( trigger: 'command' | 'rest' | 'sighup' | 'file-watch', ): Promise { this.logger.log(`Soft reload triggered by: ${trigger}`); const reloaded: string[] = []; const errors: string[] = []; // 1. Unload all registered MosaicPlugin instances for (const [name, plugin] of this.plugins) { if (isMosaicPlugin(plugin)) { try { await plugin.onUnload(); reloaded.push(name); } catch (err) { errors.push(`${name}: unload failed — ${err}`); } } } // 2. Reload all MosaicPlugin instances for (const [name, plugin] of this.plugins) { if (isMosaicPlugin(plugin)) { try { await plugin.onLoad(); } catch (err) { errors.push(`${name}: load failed — ${err}`); } } } const manifest = this.commandRegistry.getManifest(); const errorSuffix = errors.length > 0 ? ` Errors: ${errors.join(', ')}` : ''; const payload: SystemReloadPayload = { commands: manifest.commands, skills: manifest.skills, providers: [], message: `Reload complete (trigger=${trigger}). Plugins reloaded: [${reloaded.join(', ')}].${errorSuffix}`, }; this.logger.log( `Reload complete. Reloaded: [${reloaded.join(', ')}]. Errors: ${errors.length}`, ); return payload; } }