2025-05-20 10:08:24 +00:00
|
|
|
|
import { logger } from "@/lib/logger";
|
|
|
|
|
|
import { DateTime } from "luxon";
|
2025-05-09 10:45:32 +00:00
|
|
|
|
import http from "node:http";
|
|
|
|
|
|
import os from "node:os";
|
2025-04-22 15:09:57 +00:00
|
|
|
|
import { createApp } from "./app";
|
2025-02-03 13:12:36 +00:00
|
|
|
|
import { ENV } from "./config";
|
2025-05-02 21:43:51 +00:00
|
|
|
|
import { tryConnectToDatabase } from "./config/database";
|
2025-08-12 15:09:04 +00:00
|
|
|
|
import { registerHealthRoutes } from "./health";
|
2025-06-11 13:13:21 +00:00
|
|
|
|
import { listRoutes } from "./lib";
|
2025-05-20 10:08:24 +00:00
|
|
|
|
import { initModules } from "./lib/modules";
|
2025-05-09 10:45:32 +00:00
|
|
|
|
import { registerModules } from "./register-modules";
|
2025-01-28 14:01:02 +00:00
|
|
|
|
|
2025-06-11 13:13:21 +00:00
|
|
|
|
const API_BASE_PATH = "/api/v1";
|
|
|
|
|
|
|
2025-02-03 13:12:36 +00:00
|
|
|
|
// 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,
|
2025-09-12 16:23:36 +00:00
|
|
|
|
connections: {} as Record<string, unknown>,
|
2025-02-03 13:12:36 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 🔒 Bandera de apagado y promesa para idempotencia
|
|
|
|
|
|
let isShuttingDown = false;
|
|
|
|
|
|
let shutdownPromise: Promise<void> | null = null;
|
|
|
|
|
|
// 🔒 Bandera de readiness (se actualiza durante arranque y apagado)
|
|
|
|
|
|
let isReady = false;
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Manejo de cierre forzado del servidor (graceful shutdown + sockets)
|
2025-02-03 13:12:36 +00:00
|
|
|
|
const serverStop = (server: http.Server) => {
|
|
|
|
|
|
const forceTimeout = 30000;
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// Devolvemos siempre la misma promesa para evitar dobles cierres
|
|
|
|
|
|
if (shutdownPromise) return shutdownPromise;
|
|
|
|
|
|
|
|
|
|
|
|
shutdownPromise = new Promise<void>((resolve, reject) => {
|
2025-02-03 13:12:36 +00:00
|
|
|
|
logger.warn("⚡️ Shutting down server");
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// 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();
|
2025-02-03 13:12:36 +00:00
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// Intentamos cerrar el servidor de forma limpia
|
2025-02-03 13:12:36 +00:00
|
|
|
|
server.close((err) => {
|
|
|
|
|
|
if (err) {
|
2025-08-12 15:09:04 +00:00
|
|
|
|
logger.error("Error while closing server", { err, label: "serverStop.close" });
|
2025-02-03 13:12:36 +00:00
|
|
|
|
return reject(err);
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.info("Closed out remaining connections.");
|
2025-02-03 19:59:40 +00:00
|
|
|
|
logger.info("❎ Bye!");
|
2025-08-12 15:09:04 +00:00
|
|
|
|
clearTimeout(killer);
|
2025-02-03 13:12:36 +00:00
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-08-12 15:09:04 +00:00
|
|
|
|
|
|
|
|
|
|
return shutdownPromise;
|
2025-02-03 13:12:36 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
2025-02-03 13:12:36 +00:00
|
|
|
|
// Manejo de errores al iniciar el servidor
|
|
|
|
|
|
const serverError = (error: NodeJS.ErrnoException) => {
|
2025-08-12 15:09:04 +00:00
|
|
|
|
logger.error("⛔️ Server wasn't able to start properly.", {
|
2025-05-02 21:43:51 +00:00
|
|
|
|
label: "serverError0",
|
|
|
|
|
|
error,
|
2025-08-12 15:09:04 +00:00
|
|
|
|
host: ENV.HOST,
|
|
|
|
|
|
port: ENV.PORT,
|
2025-05-02 21:43:51 +00:00
|
|
|
|
});
|
2025-02-03 13:12:36 +00:00
|
|
|
|
|
|
|
|
|
|
if (error.code === "EADDRINUSE") {
|
2025-08-12 15:09:04 +00:00
|
|
|
|
logger.error(`Port ${ENV.PORT} already in use`, { error, label: "serverError1" });
|
2025-02-03 13:12:36 +00:00
|
|
|
|
} else {
|
2025-05-09 10:45:32 +00:00
|
|
|
|
logger.error(error.message, { error, label: "serverError2" });
|
2025-02-03 13:12:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// Fallo crítico de arranque → salida con código 1
|
2025-04-22 08:11:50 +00:00
|
|
|
|
process.exit(1);
|
2025-02-03 13:12:36 +00:00
|
|
|
|
};
|
2025-01-28 14:01:02 +00:00
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// Almacena en "connections" cada nueva conexión
|
2025-02-03 13:12:36 +00:00
|
|
|
|
const serverConnection = (conn: any) => {
|
|
|
|
|
|
const key = `${conn.remoteAddress}:${conn.remotePort}`;
|
|
|
|
|
|
currentState.connections[key] = conn;
|
2025-01-29 16:01:17 +00:00
|
|
|
|
|
2025-02-03 13:12:36 +00:00
|
|
|
|
conn.on("close", () => {
|
|
|
|
|
|
delete currentState.connections[key];
|
2025-01-29 16:01:17 +00:00
|
|
|
|
});
|
2025-02-03 13:12:36 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-12 11:23:50 +00:00
|
|
|
|
// Cargar paquetes de la aplicación: customers, invoices, routes...
|
2025-05-09 10:45:32 +00:00
|
|
|
|
registerModules();
|
|
|
|
|
|
|
|
|
|
|
|
const app = createApp();
|
2025-04-22 17:14:34 +00:00
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ➕ Rutas de salud disponibles desde el inicio del proceso
|
|
|
|
|
|
registerHealthRoutes(app, () => ({ ready: isReady }));
|
|
|
|
|
|
|
2025-02-03 13:12:36 +00:00
|
|
|
|
// Crea el servidor HTTP
|
2025-08-12 15:09:04 +00:00
|
|
|
|
const server = http.createServer(app);
|
2025-02-03 13:12:36 +00:00
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ⚙️ 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}`);
|
|
|
|
|
|
})
|
2025-02-03 13:12:36 +00:00
|
|
|
|
.on("close", () =>
|
2025-05-20 10:08:24 +00:00
|
|
|
|
logger.info(`Shutdown at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`)
|
2025-02-03 13:12:36 +00:00
|
|
|
|
)
|
|
|
|
|
|
.on("connection", serverConnection)
|
|
|
|
|
|
.on("error", serverError);
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Manejo de promesas no capturadas: apagado controlado
|
|
|
|
|
|
// ... unhandledRejection/uncaughtException: también set isReady = false;
|
|
|
|
|
|
process.on("unhandledRejection", async (reason: any, promise: Promise<any>) => {
|
|
|
|
|
|
logger.error("❌ Unhandled rejection", {
|
|
|
|
|
|
label: "unhandledRejection",
|
|
|
|
|
|
reason,
|
|
|
|
|
|
promise: String(promise),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!isShuttingDown) {
|
|
|
|
|
|
isShuttingDown = true;
|
|
|
|
|
|
isReady = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await serverStop(server);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-03 13:12:36 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-03 13:12:36 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
2025-02-03 13:12:36 +00:00
|
|
|
|
// Arranca el servidor si la conexión a la base de datos va bien
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
2025-02-03 19:50:16 +00:00
|
|
|
|
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}`);
|
|
|
|
|
|
|
2025-05-02 21:43:51 +00:00
|
|
|
|
const database = await tryConnectToDatabase();
|
2025-08-12 15:09:04 +00:00
|
|
|
|
|
2025-02-03 13:12:36 +00:00
|
|
|
|
// Lógica de inicialización de DB, si procede:
|
|
|
|
|
|
// initStructure(sequelizeConn.connection);
|
|
|
|
|
|
// insertUsers();
|
|
|
|
|
|
|
2025-06-11 13:13:21 +00:00
|
|
|
|
await initModules({ app, database, baseRoutePath: API_BASE_PATH, logger });
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// ✅ 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()}`);
|
|
|
|
|
|
|
2025-02-03 13:12:36 +00:00
|
|
|
|
server.listen(currentState.port, () => {
|
2025-05-02 21:43:51 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// URLs de acceso útiles
|
|
|
|
|
|
logger.info(`⚡️ Server accessible at: http://localhost:${currentState.port}`);
|
|
|
|
|
|
logger.info(`⚡️ Server accessible at: http://${currentState.host}:${currentState.port}`);
|
2025-05-20 10:08:24 +00:00
|
|
|
|
for (const address of addresses) {
|
2025-05-02 21:43:51 +00:00
|
|
|
|
logger.info(`⚡️ Server accessible at: http://${address}:${currentState.port}`);
|
2025-05-09 10:45:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 21:43:51 +00:00
|
|
|
|
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}`);
|
2025-02-03 13:12:36 +00:00
|
|
|
|
logger.info("To shut down your server, press <CTRL> + C at any time");
|
2025-08-12 15:09:04 +00:00
|
|
|
|
|
|
|
|
|
|
// En entornos de desarrollo puede ser muy verboso
|
|
|
|
|
|
logger.info(JSON.stringify(listRoutes((app as any)._router, API_BASE_PATH), null, 3));
|
2025-02-03 13:12:36 +00:00
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
2025-08-12 15:09:04 +00:00
|
|
|
|
// Arranque fallido → readiness sigue false
|
|
|
|
|
|
isReady = false;
|
2025-02-03 13:12:36 +00:00
|
|
|
|
serverError(error as NodeJS.ErrnoException);
|
|
|
|
|
|
}
|
2025-01-29 16:01:17 +00:00
|
|
|
|
})();
|