Uecko_ERP/apps/server/src/index.ts

271 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, unknown>,
};
// ─────────────────────────────────────────────────────────────────────────────
// 🔒 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)
const serverStop = (server: http.Server) => {
const forceTimeout = 30000;
// Devolvemos siempre la misma promesa para evitar dobles cierres
if (shutdownPromise) return shutdownPromise;
shutdownPromise = new Promise<void>((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<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);
}
}
});
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 <CTRL> + 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);
}
})();