UNPKG

@nodedaemon/core

Version:

Production-ready Node.js process manager with zero external dependencies

287 lines 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StateManager = void 0; const fs_1 = require("fs"); const helpers_1 = require("../utils/helpers"); const constants_1 = require("../utils/constants"); class StateManager { logger; state; saveTimer = null; saveInterval = 5000; // 5 seconds constructor(logger) { this.logger = logger; this.state = this.createInitialState(); this.loadState(); this.startAutoSave(); } createInitialState() { return { processes: new Map(), version: '1.0.2', startedAt: Date.now(), pid: process.pid }; } loadState() { try { if (!(0, fs_1.existsSync)(constants_1.STATE_FILE)) { this.logger.info('No existing state file found, starting fresh'); return; } const stateData = (0, fs_1.readFileSync)(constants_1.STATE_FILE, 'utf8'); const parsedState = JSON.parse(stateData); // Convert processes array back to Map if (parsedState.processes && Array.isArray(parsedState.processes)) { this.state.processes = new Map(parsedState.processes); } this.state.version = parsedState.version || this.state.version; this.state.startedAt = parsedState.startedAt || Date.now(); // Update PID to current process this.state.pid = process.pid; this.logger.info(`State loaded successfully`, { processCount: this.state.processes.size, version: this.state.version, originalStartedAt: parsedState.startedAt }); // Clean up orphaned processes this.cleanupOrphanedProcesses(); } catch (error) { this.logger.error(`Failed to load state: ${error.message}`, { error }); this.state = this.createInitialState(); } } cleanupOrphanedProcesses() { let cleanedCount = 0; for (const [processId, processInfo] of this.state.processes.entries()) { let hasRunningInstances = false; // Check if any instances are still running for (const instance of processInfo.instances) { if (instance.pid && this.isProcessRunning(instance.pid)) { hasRunningInstances = true; instance.status = 'running'; } else { instance.status = 'stopped'; instance.pid = undefined; } } if (!hasRunningInstances && processInfo.status === 'running') { processInfo.status = 'stopped'; processInfo.updatedAt = Date.now(); cleanedCount++; } } if (cleanedCount > 0) { this.logger.info(`Cleaned up ${cleanedCount} orphaned processes`); } } isProcessRunning(pid) { try { process.kill(pid, 0); return true; } catch (error) { return false; } } saveState() { try { (0, helpers_1.ensureFileDir)(constants_1.STATE_FILE); const stateToSave = { ...this.state, processes: Array.from(this.state.processes.entries()), savedAt: Date.now() }; const stateData = JSON.stringify(stateToSave, null, 2); (0, fs_1.writeFileSync)(constants_1.STATE_FILE, stateData, 'utf8'); this.logger.debug('State saved successfully', { processCount: this.state.processes.size, filePath: constants_1.STATE_FILE }); } catch (error) { this.logger.error(`Failed to save state: ${error.message}`, { error }); } } setProcess(processId, processInfo) { this.state.processes.set(processId, processInfo); this.scheduleSave(); } deleteProcess(processId) { const deleted = this.state.processes.delete(processId); if (deleted) { this.scheduleSave(); } return deleted; } getProcess(processId) { return this.state.processes.get(processId); } getAllProcesses() { return Array.from(this.state.processes.values()); } getProcessCount() { return this.state.processes.size; } updateProcess(processId, updates) { const process = this.state.processes.get(processId); if (!process) { return false; } Object.assign(process, updates, { updatedAt: Date.now() }); this.scheduleSave(); return true; } getState() { return { ...this.state, processes: new Map(this.state.processes) }; } getStats() { const processes = Array.from(this.state.processes.values()); return { processCount: processes.length, runningProcesses: processes.filter(p => p.status === 'running').length, stoppedProcesses: processes.filter(p => p.status === 'stopped').length, erroredProcesses: processes.filter(p => p.status === 'errored').length, uptime: Date.now() - this.state.startedAt, version: this.state.version }; } findProcessByName(name) { return Array.from(this.state.processes.values()).find(p => p.name === name); } findProcessesByScript(script) { return Array.from(this.state.processes.values()).filter(p => p.script === script); } getProcessesByStatus(status) { return Array.from(this.state.processes.values()).filter(p => p.status === status); } startAutoSave() { if (this.saveTimer) { clearInterval(this.saveTimer); } this.saveTimer = setInterval(() => { this.saveState(); }, this.saveInterval); } scheduleSave() { // Debounced save - will save after a short delay if no more changes come in if (this.saveTimer) { clearTimeout(this.saveTimer); } this.saveTimer = setTimeout(() => { this.saveState(); this.startAutoSave(); }, 1000); } setSaveInterval(interval) { this.saveInterval = Math.max(1000, interval); // Minimum 1 second this.startAutoSave(); } forceSave() { this.saveState(); } backup(backupPath) { const targetPath = backupPath || `${constants_1.STATE_FILE}.backup.${Date.now()}`; try { (0, helpers_1.ensureFileDir)(targetPath); const currentState = { ...this.state, processes: Array.from(this.state.processes.entries()), backedUpAt: Date.now() }; const stateData = JSON.stringify(currentState, null, 2); (0, fs_1.writeFileSync)(targetPath, stateData, 'utf8'); this.logger.info(`State backed up successfully`, { backupPath: targetPath }); } catch (error) { this.logger.error(`Failed to backup state: ${error.message}`, { error, backupPath: targetPath }); throw error; } } restore(backupPath) { try { if (!(0, fs_1.existsSync)(backupPath)) { throw new Error(`Backup file not found: ${backupPath}`); } const backupData = (0, fs_1.readFileSync)(backupPath, 'utf8'); const parsedBackup = JSON.parse(backupData); // Validate backup data if (!parsedBackup.processes || !Array.isArray(parsedBackup.processes)) { throw new Error('Invalid backup data: missing processes'); } // Create new state from backup const restoredState = { processes: new Map(parsedBackup.processes), version: parsedBackup.version || this.state.version, startedAt: Date.now(), // Use current time as new start time pid: process.pid }; this.state = restoredState; this.saveState(); this.logger.info(`State restored successfully`, { processCount: this.state.processes.size, backupPath, originalBackupTime: parsedBackup.backedUpAt }); // Clean up any processes that are no longer running this.cleanupOrphanedProcesses(); } catch (error) { this.logger.error(`Failed to restore state: ${error.message}`, { error, backupPath }); throw error; } } reset() { this.logger.warn('Resetting daemon state - all process information will be lost'); this.state = this.createInitialState(); this.saveState(); this.logger.info('Daemon state reset completed'); } shutdown() { if (this.saveTimer) { clearInterval(this.saveTimer); this.saveTimer = null; } // Final save on shutdown this.saveState(); this.logger.info('StateManager shutdown completed'); } validate() { try { // Validate state structure if (!this.state || typeof this.state !== 'object') { return false; } if (!(this.state.processes instanceof Map)) { return false; } // Validate each process for (const [processId, processInfo] of this.state.processes.entries()) { if (!processId || typeof processId !== 'string') { return false; } if (!processInfo || typeof processInfo !== 'object') { return false; } if (!processInfo.id || !processInfo.name || !processInfo.script) { return false; } if (!Array.isArray(processInfo.instances)) { return false; } } return true; } catch (error) { this.logger.error(`State validation failed: ${error.message}`, { error }); return false; } } } exports.StateManager = StateManager; //# sourceMappingURL=StateManager.js.map