@nodedaemon/core
Version:
Production-ready Node.js process manager with zero external dependencies
287 lines • 10.5 kB
JavaScript
"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