neex
Version:
Neex - Modern Fullstack Framework Built on Express and Next.js. Fast to Start, Easy to Build, Ready to Deploy
480 lines (479 loc) • 19.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StartManager = void 0;
// src/start-manager.ts - Fixed production start manager
const child_process_1 = require("child_process");
const chokidar_1 = require("chokidar");
const logger_manager_js_1 = require("./logger-manager.js");
const chalk_1 = __importDefault(require("chalk"));
const figures_1 = __importDefault(require("figures"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const http_1 = __importDefault(require("http"));
const lodash_1 = require("lodash");
const os_1 = __importDefault(require("os"));
class StartManager {
constructor(options) {
this.workers = new Map();
this.watcher = null;
this.healthServer = null;
this.isShuttingDown = false;
this.totalRestarts = 0;
this.envLoaded = false;
this.masterProcess = null;
this.options = options;
this.startTime = new Date();
this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay);
}
log(message, level = 'info') {
logger_manager_js_1.loggerManager.printLine(message, level);
}
loadEnvFile() {
if (this.envLoaded)
return;
if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) {
try {
const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8');
const lines = envContent.split('\n');
let loadedCount = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...values] = trimmed.split('=');
if (key && values.length > 0) {
const value = values.join('=').trim();
const cleanValue = value.replace(/^["']|["']$/g, '');
if (!process.env[key.trim()]) {
process.env[key.trim()] = cleanValue;
loadedCount++;
}
}
}
}
if (this.options.verbose && loadedCount > 0) {
this.log(`Loaded ${loadedCount} environment variables from ${this.options.envFile}`);
}
this.envLoaded = true;
}
catch (error) {
if (this.options.verbose) {
this.log(`Failed to load environment file: ${error.message}`, 'warn');
}
}
}
}
parseMemoryLimit(limit) {
var _a;
if (!limit)
return undefined;
const match = limit.match(/^(\d+)([KMGT]?)$/i);
if (!match)
return undefined;
const value = parseInt(match[1]);
const unit = ((_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || '';
const multipliers = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024, T: 1024 * 1024 * 1024 * 1024 };
return value * (multipliers[unit] || 1);
}
getNodeArgs() {
const args = [];
if (this.options.memoryLimit) {
const memoryBytes = this.parseMemoryLimit(this.options.memoryLimit);
if (memoryBytes) {
const memoryMB = Math.floor(memoryBytes / (1024 * 1024));
args.push(`--max-old-space-size=${memoryMB}`);
}
}
if (this.options.inspect) {
args.push('--inspect');
}
if (this.options.inspectBrk) {
args.push('--inspect-brk');
}
if (this.options.nodeArgs) {
args.push(...this.options.nodeArgs.split(' ').filter(arg => arg.trim()));
}
return args;
}
async startSingleProcess() {
const nodeArgs = this.getNodeArgs();
const port = this.options.port || 8000;
const env = {
...process.env,
NODE_ENV: process.env.NODE_ENV || 'production',
PORT: port.toString(),
FORCE_COLOR: this.options.color ? '1' : '0',
NODE_OPTIONS: '--no-deprecation'
};
this.masterProcess = (0, child_process_1.fork)(this.options.file, [], {
cwd: this.options.workingDir,
env,
execArgv: nodeArgs,
silent: false // Let the process handle its own output
});
this.masterProcess.on('error', (error) => {
this.log(`Process error: ${error.message}`, 'error');
});
this.masterProcess.on('exit', (code, signal) => {
if (!this.isShuttingDown && code !== 0 && signal !== 'SIGTERM') {
this.log(`Process crashed (code: ${code}, signal: ${signal})`, 'error');
this.totalRestarts++;
if (this.totalRestarts < this.options.maxCrashes) {
setTimeout(() => {
this.startSingleProcess();
}, this.options.restartDelay);
}
else {
this.log(`Max crashes reached (${this.options.maxCrashes}), not restarting`, 'error');
}
}
});
// Wait for process to be ready
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
resolve(); // Don't reject, assume it's ready
}, 5000);
this.masterProcess.on('message', (message) => {
if (message && message.type === 'ready') {
clearTimeout(timeout);
resolve();
}
});
this.masterProcess.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
this.masterProcess.on('exit', (code) => {
clearTimeout(timeout);
if (code !== 0) {
reject(new Error(`Process exited with code ${code}`));
}
else {
resolve();
}
});
});
if (this.options.verbose) {
this.log(`Process started (PID: ${this.masterProcess.pid}, Port: ${port})`);
}
}
startWorker(workerId) {
return new Promise((resolve, reject) => {
var _a, _b;
const nodeArgs = this.getNodeArgs();
const port = this.options.port || 8000;
const env = {
...process.env,
NODE_ENV: process.env.NODE_ENV || 'production',
WORKER_ID: workerId.toString(),
PORT: port.toString(),
CLUSTER_WORKER: 'true',
FORCE_COLOR: this.options.color ? '1' : '0',
NODE_OPTIONS: '--no-deprecation'
};
const workerProcess = (0, child_process_1.fork)(this.options.file, [], {
cwd: this.options.workingDir,
env,
execArgv: nodeArgs,
silent: true
});
const workerInfo = {
process: workerProcess,
pid: workerProcess.pid,
restarts: 0,
startTime: new Date(),
id: workerId,
port: port
};
this.workers.set(workerId, workerInfo);
let isReady = false;
const readinessTimeout = setTimeout(() => {
if (!isReady) {
workerProcess.kill();
reject(new Error(`Worker ${workerId} failed to become ready within 15s.`));
}
}, 15000);
const cleanupReadinessListeners = () => {
var _a;
clearTimeout(readinessTimeout);
(_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.removeListener('data', onDataForReady);
workerProcess.removeListener('message', onMessageForReady);
};
const onReady = () => {
if (isReady)
return;
isReady = true;
cleanupReadinessListeners();
if (this.options.verbose) {
this.log(`Worker ${workerId} is ready (PID: ${workerProcess.pid})`);
}
resolve(workerInfo);
};
const onDataForReady = (data) => {
const message = data.toString();
const prefix = chalk_1.default.dim(`[Worker ${workerId}] `);
process.stdout.write(prefix + message);
if (/listening|ready|running on port|local:/i.test(message)) {
onReady();
}
};
const onMessageForReady = (message) => {
if (message && message.type === 'ready') {
onReady();
}
};
(_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', onDataForReady);
(_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
const prefix = chalk_1.default.red.dim(`[Worker ${workerId}] `);
process.stderr.write(prefix + data.toString());
});
workerProcess.on('message', onMessageForReady);
workerProcess.on('error', (error) => {
if (!isReady) {
cleanupReadinessListeners();
reject(error);
}
this.log(`Worker ${workerId} error: ${error.message}`, 'error');
});
workerProcess.on('exit', (code, signal) => {
if (!isReady) {
cleanupReadinessListeners();
reject(new Error(`Worker ${workerId} exited with code ${code} before becoming ready.`));
}
else {
this.workers.delete(workerId);
if (!this.isShuttingDown && code !== 0 && signal !== 'SIGTERM') {
this.log(`Worker ${workerId} crashed (code: ${code}, signal: ${signal})`, 'error');
this.restartWorker(workerId);
}
}
});
});
}
async restartWorker(workerId) {
const workerInfo = this.workers.get(workerId);
if (workerInfo) {
workerInfo.restarts++;
this.totalRestarts++;
if (workerInfo.restarts >= this.options.maxCrashes) {
this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error');
return;
}
if (this.options.verbose) {
this.log(`Restarting worker ${workerId} (attempt ${workerInfo.restarts})`);
}
try {
workerInfo.process.kill('SIGTERM');
await this.waitForProcessExit(workerInfo.process, 5000);
}
catch (error) {
workerInfo.process.kill('SIGKILL');
}
setTimeout(() => {
this.startWorker(workerId);
}, this.options.restartDelay);
}
}
async waitForProcessExit(process, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Process exit timeout'));
}, timeout);
process.on('exit', () => {
clearTimeout(timer);
resolve();
});
if (process.killed) {
clearTimeout(timer);
resolve();
}
});
}
async startCluster() {
if (this.options.workers === 1) {
// Single process mode
await this.startSingleProcess();
return;
}
// Multi-worker mode
this.log(`${chalk_1.default.blue(figures_1.default.play)} Starting production server (${this.options.workers} workers)`);
const startPromises = [];
for (let i = 0; i < this.options.workers; i++) {
startPromises.push(this.startWorker(i + 1));
}
try {
await Promise.all(startPromises);
this.log(`${chalk_1.default.green(figures_1.default.tick)} Server ready on port ${this.options.port || 8000} (${this.workers.size} workers)`);
}
catch (error) {
this.log(`Failed to start some workers: ${error.message}`, 'error');
if (this.workers.size > 0) {
this.log(`${chalk_1.default.yellow(figures_1.default.warning)} Server partially ready on port ${this.options.port || 8000} (${this.workers.size} workers)`);
}
}
}
setupHealthCheck() {
if (!this.options.healthCheck)
return;
this.healthServer = http_1.default.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.url === '/health') {
const stats = {
status: 'ok',
uptime: Date.now() - this.startTime.getTime(),
workers: this.workers.size,
activeWorkers: Array.from(this.workers.values()).map(w => ({
id: w.id,
pid: w.pid,
restarts: w.restarts,
uptime: Date.now() - w.startTime.getTime()
})),
totalRestarts: this.totalRestarts,
memory: process.memoryUsage(),
cpu: os_1.default.loadavg(),
port: this.options.port || 8000
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(stats, null, 2));
}
else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
this.healthServer.listen(this.options.healthPort, () => {
if (this.options.verbose) {
this.log(`Health endpoint: http://localhost:${this.options.healthPort}/health`);
}
});
}
setupWatcher() {
if (!this.options.watch)
return;
const watchPatterns = [
`${this.options.workingDir}/**/*.js`,
`${this.options.workingDir}/**/*.json`,
`${this.options.workingDir}/**/*.env*`
];
this.watcher = (0, chokidar_1.watch)(watchPatterns, {
ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**', '**/tmp/**'],
ignoreInitial: true,
atomic: 300,
awaitWriteFinish: {
stabilityThreshold: 2000,
pollInterval: 100
}
});
this.watcher.on('change', (filePath) => {
if (this.options.verbose) {
this.log(`File changed: ${path_1.default.relative(this.options.workingDir, filePath)}`);
}
this.debouncedRestart();
});
this.watcher.on('error', (error) => {
this.log(`Watcher error: ${error.message}`, 'error');
});
if (this.options.verbose) {
this.log('File watching enabled');
}
}
async restartAll() {
if (this.isShuttingDown)
return;
this.log('Restarting due to file changes...');
if (this.options.workers === 1 && this.masterProcess) {
// Single process restart
try {
this.masterProcess.kill('SIGTERM');
await this.waitForProcessExit(this.masterProcess, 5000);
}
catch (error) {
this.masterProcess.kill('SIGKILL');
}
setTimeout(() => {
this.startSingleProcess();
}, this.options.restartDelay);
}
else {
// Multi-worker restart
const restartPromises = [];
for (const [workerId, workerInfo] of this.workers.entries()) {
restartPromises.push((async () => {
try {
workerInfo.process.kill('SIGTERM');
await this.waitForProcessExit(workerInfo.process, 5000);
}
catch (error) {
workerInfo.process.kill('SIGKILL');
}
})());
}
await Promise.allSettled(restartPromises);
setTimeout(() => {
this.startCluster();
}, this.options.restartDelay);
}
}
async start() {
try {
// Load environment variables
this.loadEnvFile();
// Set up monitoring and health checks
this.setupHealthCheck();
this.setupWatcher();
// Start the application
await this.startCluster();
}
catch (error) {
this.log(`Failed to start server: ${error.message}`, 'error');
throw error;
}
}
async stop() {
if (this.isShuttingDown)
return;
this.isShuttingDown = true;
this.log(`${chalk_1.default.yellow('⏹')} Shutting down gracefully...`);
if (this.watcher) {
await this.watcher.close();
}
if (this.healthServer) {
this.healthServer.close();
}
// Stop single process
if (this.masterProcess) {
try {
this.masterProcess.kill('SIGTERM');
await this.waitForProcessExit(this.masterProcess, this.options.gracefulTimeout);
}
catch (error) {
this.masterProcess.kill('SIGKILL');
}
}
// Stop workers
const shutdownPromises = [];
for (const [workerId, workerInfo] of this.workers.entries()) {
shutdownPromises.push((async () => {
try {
workerInfo.process.kill('SIGTERM');
await this.waitForProcessExit(workerInfo.process, this.options.gracefulTimeout);
}
catch (error) {
workerInfo.process.kill('SIGKILL');
}
})());
}
await Promise.allSettled(shutdownPromises);
this.workers.clear();
}
}
exports.StartManager = StartManager;