import { DateTime } from "luxon"; import http from "node:http"; import os from "node:os"; import { z } from "zod/v4"; import { createApp } from "./app"; import { ENV } from "./config"; import { tryConnectToDatabase } from "./config/database"; import { registerHealthRoutes } from "./health"; import { listRoutes, logger } from "./lib"; import { initModules } from "./lib/modules"; import { registerModules } from "./register-modules"; const API_BASE_PATH = "/api/v1"; z.config(z.locales.es()); // Guardamos información del estado del servidor export const currentState = { launchedAt: DateTime.now(), appPath: process.cwd(), host: ENV.HOST, port: ENV.PORT, environment: ENV.NODE_ENV, connections: {} as Record, }; // ───────────────────────────────────────────────────────────────────────────── // 🔒 Bandera de apagado y promesa para idempotencia let isShuttingDown = false; let shutdownPromise: Promise | null = null; // 🔒 Bandera de readiness (se actualiza durante arranque y apagado) let isReady = false; // ───────────────────────────────────────────────────────────────────────────── // Manejo de cierre forzado del servidor (graceful shutdown + sockets) const serverStop = (server: http.Server) => { const forceTimeout = 30000; // Devolvemos siempre la misma promesa para evitar dobles cierres if (shutdownPromise) return shutdownPromise; shutdownPromise = new Promise((resolve, reject) => { logger.warn("⚡️ Shutting down server"); // Tras el timeout, destruimos sockets vivos y resolvemos const killer = setTimeout(() => { try { const sockets = Object.values(currentState.connections) as any[]; let destroyed = 0; for (const s of sockets) { try { // @ts-ignore - algunos runtimes exponen destroy() / end() if (s.destroy && !s.destroyed) { s.destroy(); destroyed++; } else if (s.end) { s.end(); } } catch { /* noop */ } } logger.error(`Force timeout reached. Destroyed ${destroyed} remaining connection(s).`); } finally { resolve(); } }, forceTimeout); // Evita mantener el event-loop vivo por el timeout killer.unref(); // Intentamos cerrar el servidor de forma limpia server.close((err) => { if (err) { logger.error("Error while closing server", { err, label: "serverStop.close" }); return reject(err); } logger.info("Closed out remaining connections."); logger.info("❎ Bye!"); clearTimeout(killer); resolve(); }); }); return shutdownPromise; }; // ───────────────────────────────────────────────────────────────────────────── // Manejo de errores al iniciar el servidor const serverError = (error: NodeJS.ErrnoException) => { logger.error("⛔️ Server wasn't able to start properly.", { label: "serverError0", error, host: ENV.HOST, port: ENV.PORT, }); if (error.code === "EADDRINUSE") { logger.error(`Port ${ENV.PORT} already in use`, { error, label: "serverError1" }); } else { logger.error(error.message, { error, label: "serverError2" }); } // Fallo crítico de arranque → salida con código 1 process.exit(1); }; // Almacena en "connections" cada nueva conexión const serverConnection = (conn: any) => { const key = `${conn.remoteAddress}:${conn.remotePort}`; currentState.connections[key] = conn; conn.on("close", () => { delete currentState.connections[key]; }); }; // Cargar paquetes de la aplicación: customers, invoices, routes... registerModules(); const app = createApp(); // ➕ Rutas de salud disponibles desde el inicio del proceso registerHealthRoutes(app, () => ({ ready: isReady })); // Crea el servidor HTTP const server = http.createServer(app); // ⚙️ Ajustes explícitos de timeouts HTTP (evita cuelgues en keep-alive) server.keepAliveTimeout = 70000; // el socket puede quedar vivo 70s server.headersTimeout = 75000; // debe ser > keepAliveTimeout // ───────────────────────────────────────────────────────────────────────────── // 🛎️ Señales del SO (registradas antes de listen) + apagado controlado // En los manejadores de señales/errores, marcar no-ready al iniciar shutdown const handleSignal = (signal: NodeJS.Signals) => async () => { if (isShuttingDown) { logger.warn(`Shutdown already in progress. Ignoring signal ${signal}.`); return; } isShuttingDown = true; // ⬇️ el servidor deja de estar listo inmediatamente isReady = false; logger.warn(`Received ${signal}. Starting graceful shutdown...`); try { await serverStop(server); process.exit(0); } catch (err) { logger.error("Error during shutdown, forcing exit(1)", { err, label: "handleSignal" }); process.exit(1); } }; process.on("SIGINT", handleSignal("SIGINT")); process.on("SIGTERM", handleSignal("SIGTERM")); // ───────────────────────────────────────────────────────────────────────────── // Eventos del servidor server .once("listening", () => { logger.info(`HTTP server is listening on port ${currentState.port}`); }) .on("close", () => logger.info(`Shutdown at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`) ) .on("connection", serverConnection) .on("error", serverError); // ───────────────────────────────────────────────────────────────────────────── // Manejo de promesas no capturadas: apagado controlado // ... unhandledRejection/uncaughtException: también set isReady = false; process.on("unhandledRejection", async (reason: any, promise: Promise) => { logger.error("❌ Unhandled rejection", { label: "unhandledRejection", reason, promise: String(promise), }); if (!isShuttingDown) { isShuttingDown = true; isReady = false; try { await serverStop(server); } finally { process.exit(1); } } }); process.on("uncaughtException", async (error: Error) => { logger.error(`❌ Uncaught exception: ${error.message}`, { stack: error.stack, label: "uncaughtException", }); if (!isShuttingDown) { isShuttingDown = true; isReady = false; try { await serverStop(server); } finally { process.exit(1); } } }); // ───────────────────────────────────────────────────────────────────────────── // Arranca el servidor si la conexión a la base de datos va bien (async () => { try { const now = DateTime.now(); logger.info(`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`); logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`); logger.info(`Environment: ${currentState.environment}`); logger.info(`Process PID: ${process.pid}`); const database = await tryConnectToDatabase(); // Lógica de inicialización de DB, si procede: // initStructure(sequelizeConn.connection); // insertUsers(); await initModules({ app, database, baseRoutePath: API_BASE_PATH, logger }); // ✅ El servidor ya está listo para recibir tráfico isReady = true; logger.info("✅ Server is READY (readiness=true)"); logger.info(`startup_duration_ms=${DateTime.now().diff(currentState.launchedAt).toMillis()}`); server.listen(currentState.port, () => { server.emit("listening"); const networkInterfaces = os.networkInterfaces(); const addresses: string[] = []; // Obtiene todas las direcciones IPv4 for (const interfaceName in networkInterfaces) { const networkInterface = networkInterfaces[interfaceName]; if (networkInterface) { for (const iface of networkInterface) { if (iface.family === "IPv4" && !iface.internal) { addresses.push(iface.address); } } } } // URLs de acceso útiles logger.info(`⚡️ Server accessible at: http://localhost:${currentState.port}`); logger.info(`⚡️ Server accessible at: http://${currentState.host}:${currentState.port}`); for (const address of addresses) { logger.info(`⚡️ Server accessible at: http://${address}:${currentState.port}`); } logger.info(`Server started at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`); logger.info(`Server PID: ${process.pid}`); logger.info( `Server launched in: ${DateTime.now().diff(currentState.launchedAt).toMillis()} ms` ); logger.info(`Server path: ${currentState.appPath}`); logger.info(`Server environment: ${currentState.environment}`); logger.info("To shut down your server, press + C at any time"); // En entornos de desarrollo puede ser muy verboso logger.info(JSON.stringify(listRoutes((app as any)._router, API_BASE_PATH), null, 3)); }); } catch (error) { // Arranque fallido → readiness sigue false isReady = false; serverError(error as NodeJS.ErrnoException); } })();