2025-05-28 14:21:09 +00:00
|
|
|
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
2025-05-09 10:45:32 +00:00
|
|
|
import { logger } from "../logger";
|
2025-05-04 20:06:57 +00:00
|
|
|
import { initModels, registerModels } from "./model-loader";
|
2025-09-24 17:30:35 +00:00
|
|
|
import { getService, listServices, registerService } from "./service-registry";
|
2025-05-04 20:06:57 +00:00
|
|
|
|
|
|
|
|
const registeredModules: Map<string, IModuleServer> = new Map();
|
|
|
|
|
const initializedModules = new Set<string>();
|
2025-08-12 15:09:04 +00:00
|
|
|
const visiting = new Set<string>(); // para detección de ciclos
|
2025-08-14 14:58:13 +00:00
|
|
|
const initializationOrder: string[] = []; // orden de init → para warmup
|
|
|
|
|
|
|
|
|
|
// Config opcional para warmup (valores por defecto seguros)
|
|
|
|
|
const WARMUP_TIMEOUT_MS = Number(process.env.WARMUP_TIMEOUT_MS) || 10_000;
|
|
|
|
|
const WARMUP_STRICT =
|
|
|
|
|
String(process.env.WARMUP_STRICT ?? "false")
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.trim() === "true";
|
2025-05-04 20:06:57 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
/**
|
2025-08-14 14:58:13 +00:00
|
|
|
Registra un módulo del servidor en el registry.
|
|
|
|
|
Lanza error si el nombre ya existe.
|
2025-08-12 15:09:04 +00:00
|
|
|
*/
|
2025-05-04 20:06:57 +00:00
|
|
|
export function registerModule(pkg: IModuleServer) {
|
2025-08-12 15:09:04 +00:00
|
|
|
if (!pkg?.name) {
|
|
|
|
|
throw new Error('❌ Invalid module: missing "name" property');
|
|
|
|
|
}
|
2025-06-11 13:13:21 +00:00
|
|
|
if (registeredModules.has(pkg.name)) {
|
2025-08-12 15:09:04 +00:00
|
|
|
throw new Error(`❌ Module "${pkg.name}" already registered`);
|
2025-05-04 20:06:57 +00:00
|
|
|
}
|
2025-06-11 13:13:21 +00:00
|
|
|
registeredModules.set(pkg.name, pkg);
|
2025-08-12 15:09:04 +00:00
|
|
|
logger.info(`📦 Module enqueued: "${pkg.name}"`, { label: "moduleRegistry" });
|
2025-05-04 20:06:57 +00:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
/**
|
2025-08-14 14:58:13 +00:00
|
|
|
Inicializa todos los módulos registrados (resolviendo dependencias),
|
|
|
|
|
luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
|
2025-08-12 15:09:04 +00:00
|
|
|
*/
|
2025-05-04 20:06:57 +00:00
|
|
|
export async function initModules(params: ModuleParams) {
|
2025-08-12 15:09:04 +00:00
|
|
|
for (const name of registeredModules.keys()) {
|
|
|
|
|
await loadModule(name, params, []); // secuencial para logs deterministas
|
|
|
|
|
}
|
2025-08-14 14:58:13 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
await withPhase("global", "initModels", async () => {
|
|
|
|
|
await initModels(params);
|
2025-05-04 20:06:57 +00:00
|
|
|
});
|
2025-08-14 14:58:13 +00:00
|
|
|
|
|
|
|
|
await warmupModules(params);
|
2025-05-04 20:06:57 +00:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
/**
|
2025-08-14 14:58:13 +00:00
|
|
|
Carga recursivamente un módulo y sus dependencias con detección de ciclos.
|
2025-08-12 15:09:04 +00:00
|
|
|
*/
|
|
|
|
|
async function loadModule(name: string, params: ModuleParams, stack: string[]) {
|
2025-05-04 20:06:57 +00:00
|
|
|
if (initializedModules.has(name)) return;
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
if (visiting.has(name)) {
|
|
|
|
|
// Ciclo detectado: construir traza legible
|
|
|
|
|
const cyclePath = [...stack, name].join(" -> ");
|
|
|
|
|
throw new Error(`❌ Cyclic dependency detected: ${cyclePath}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-04 20:06:57 +00:00
|
|
|
const pkg = registeredModules.get(name);
|
2025-08-12 15:09:04 +00:00
|
|
|
if (!pkg) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`❌ Module "${name}" not found (required by: ${stack[stack.length - 1] ?? "root"})`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visiting.add(name);
|
|
|
|
|
stack.push(name);
|
2025-05-04 20:06:57 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
// 1) Resolver dependencias primero (en orden)
|
2025-06-11 13:13:21 +00:00
|
|
|
const deps = pkg.dependencies || [];
|
2025-08-12 15:09:04 +00:00
|
|
|
for (const dep of deps) {
|
|
|
|
|
await loadModule(dep, params, stack.slice());
|
|
|
|
|
}
|
2025-05-04 20:06:57 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
// 2) Inicializar el módulo (permite async)
|
|
|
|
|
await withPhase(name, "init", async () => {
|
|
|
|
|
await Promise.resolve(pkg.init(params));
|
|
|
|
|
});
|
2025-05-04 20:06:57 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
// 3) Registrar dependencias que expone (permite async)
|
|
|
|
|
const pkgApi = await withPhase(name, "registerDependencies", async () => {
|
2025-09-24 17:30:35 +00:00
|
|
|
return await Promise.resolve(
|
|
|
|
|
pkg.registerDependencies?.({
|
|
|
|
|
...params,
|
|
|
|
|
listServices,
|
|
|
|
|
getService,
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-08-12 15:09:04 +00:00
|
|
|
});
|
2025-05-04 20:06:57 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
// 4) Registrar modelos de Sequelize, si existen
|
2025-05-04 20:06:57 +00:00
|
|
|
if (pkgApi?.models) {
|
2025-08-12 15:09:04 +00:00
|
|
|
await withPhase(name, "registerModels", async () => {
|
|
|
|
|
await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: pkg.name }));
|
|
|
|
|
});
|
2025-05-04 20:06:57 +00:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
// 5) Registrar servicios, si existen
|
|
|
|
|
if (pkgApi?.services && typeof pkgApi.services === "object") {
|
|
|
|
|
await withPhase(name, "registerServices", async () => {
|
|
|
|
|
await Promise.resolve(registerService(pkg.name, pkgApi.services));
|
|
|
|
|
});
|
2025-05-04 20:06:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initializedModules.add(name);
|
2025-08-14 14:58:13 +00:00
|
|
|
initializationOrder.push(name); // recordamos el orden para warmup
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
visiting.delete(name);
|
|
|
|
|
stack.pop();
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 14:58:13 +00:00
|
|
|
/**
|
|
|
|
|
Ejecuta warmup() opcional de cada módulo en orden de inicialización.
|
|
|
|
|
Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa.
|
|
|
|
|
*/
|
|
|
|
|
async function warmupModules(params: ModuleParams) {
|
|
|
|
|
logger.info("🌡️ Warmup: starting...", { label: "moduleRegistry" });
|
|
|
|
|
|
|
|
|
|
for (const name of initializationOrder) {
|
|
|
|
|
const pkg = registeredModules.get(name);
|
|
|
|
|
if (!pkg) continue;
|
|
|
|
|
|
|
|
|
|
const maybeWarmup = (pkg as unknown as { warmup?: (p: ModuleParams) => Promise<void> | void })
|
|
|
|
|
.warmup;
|
|
|
|
|
|
|
|
|
|
if (typeof maybeWarmup === "function") {
|
|
|
|
|
try {
|
|
|
|
|
await withPhase(name, "warmup", () =>
|
|
|
|
|
withTimeout(Promise.resolve(maybeWarmup(params)), WARMUP_TIMEOUT_MS, `${name}.warmup`)
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (WARMUP_STRICT) {
|
|
|
|
|
logger.error("⛔️ Warmup failed (strict=true). Aborting.", {
|
|
|
|
|
label: "moduleRegistry",
|
|
|
|
|
module: name,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn("⚠️ Warmup failed but continuing (strict=false)", {
|
|
|
|
|
label: "moduleRegistry",
|
|
|
|
|
module: name,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("🌡️ Warmup: done.", { label: "moduleRegistry" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
Helper para anotar fase y módulo en logs (inicio/fin/duración) y errores.
|
2025-08-12 15:09:04 +00:00
|
|
|
*/
|
|
|
|
|
async function withPhase<T>(
|
|
|
|
|
moduleName: string,
|
2025-08-14 14:58:13 +00:00
|
|
|
phase:
|
|
|
|
|
| "init"
|
|
|
|
|
| "registerDependencies"
|
|
|
|
|
| "registerModels"
|
|
|
|
|
| "registerServices"
|
|
|
|
|
| "initModels"
|
|
|
|
|
| "warmup",
|
2025-08-12 15:09:04 +00:00
|
|
|
fn: () => Promise<T> | T
|
|
|
|
|
): Promise<T> {
|
2025-08-14 14:58:13 +00:00
|
|
|
const startedAt = Date.now();
|
|
|
|
|
logger.info(`▶️ phase=start module=${moduleName} ${phase}`, {
|
|
|
|
|
label: "moduleRegistry",
|
|
|
|
|
module: moduleName,
|
|
|
|
|
phase,
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
try {
|
2025-08-14 14:58:13 +00:00
|
|
|
const out = await fn();
|
|
|
|
|
const duration = Date.now() - startedAt;
|
|
|
|
|
logger.info(`✅ phase=done module=${moduleName} ${phase} durationMs=${duration}`, {
|
|
|
|
|
label: "moduleRegistry",
|
|
|
|
|
module: moduleName,
|
|
|
|
|
phase,
|
|
|
|
|
durationMs: duration,
|
|
|
|
|
});
|
|
|
|
|
return out;
|
2025-08-12 15:09:04 +00:00
|
|
|
} catch (error: any) {
|
2025-08-14 14:58:13 +00:00
|
|
|
const duration = Date.now() - startedAt;
|
2025-08-12 15:09:04 +00:00
|
|
|
logger.error(
|
2025-08-14 14:58:13 +00:00
|
|
|
`⛔️ Module "${moduleName}" failed at phase="${phase}" after ${duration}ms: ${error?.message ?? error}`,
|
2025-08-12 15:09:04 +00:00
|
|
|
{
|
|
|
|
|
label: "moduleRegistry",
|
|
|
|
|
module: moduleName,
|
|
|
|
|
phase,
|
2025-08-14 14:58:13 +00:00
|
|
|
durationMs: duration,
|
2025-08-12 15:09:04 +00:00
|
|
|
stack: error?.stack,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
// Re-lanzamos con contexto preservado
|
|
|
|
|
const err = new Error(
|
|
|
|
|
`[module=${moduleName}] phase=${phase} failed: ${error?.message ?? "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
(err as any).cause = error;
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-14 14:58:13 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
Promesa con timeout; rechaza si excede el tiempo dado.
|
|
|
|
|
*/
|
|
|
|
|
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
|
|
|
|
let timer: NodeJS.Timeout | null = null;
|
|
|
|
|
const timeout = new Promise<T>((_, reject) => {
|
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
|
reject(new Error(`Timeout after ${ms}ms (${label})`));
|
|
|
|
|
}, ms).unref();
|
|
|
|
|
});
|
|
|
|
|
return Promise.race([p, timeout]).finally(() => {
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
}) as Promise<T>;
|
|
|
|
|
}
|