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";
|
|
|
|
|
import { registerService } from "./service-registry";
|
|
|
|
|
|
|
|
|
|
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-05-04 20:06:57 +00:00
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
/**
|
|
|
|
|
Registra un módulo del servidor en el registry.
|
|
|
|
|
Lanza error si el nombre ya existe.
|
|
|
|
|
*/
|
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
|
|
|
/**
|
|
|
|
|
Inicializa todos los módulos registrados (resolviendo dependencias),
|
|
|
|
|
y al final inicializa los modelos (Sequelize) en bloque.
|
|
|
|
|
*/
|
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
|
|
|
|
|
}
|
|
|
|
|
await withPhase("global", "initModels", async () => {
|
|
|
|
|
await initModels(params);
|
2025-05-04 20:06:57 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 15:09:04 +00:00
|
|
|
/**
|
|
|
|
|
Carga recursivamente un módulo y sus dependencias con detección de ciclos.
|
|
|
|
|
*/
|
|
|
|
|
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 () => {
|
|
|
|
|
return await Promise.resolve(pkg.registerDependencies?.(params));
|
|
|
|
|
});
|
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-12 15:09:04 +00:00
|
|
|
visiting.delete(name);
|
|
|
|
|
stack.pop();
|
|
|
|
|
|
|
|
|
|
logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
Helper para anotar fase y módulo en errores y logs.
|
|
|
|
|
*/
|
|
|
|
|
async function withPhase<T>(
|
|
|
|
|
moduleName: string,
|
|
|
|
|
phase: "init" | "registerDependencies" | "registerModels" | "registerServices" | "initModels",
|
|
|
|
|
fn: () => Promise<T> | T
|
|
|
|
|
): Promise<T> {
|
|
|
|
|
try {
|
|
|
|
|
return await fn();
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// Log enriquecido con contexto
|
|
|
|
|
logger.error(
|
|
|
|
|
`⛔️ Module "${moduleName}" failed at phase="${phase}": ${error?.message ?? error}`,
|
|
|
|
|
{
|
|
|
|
|
label: "moduleRegistry",
|
|
|
|
|
module: moduleName,
|
|
|
|
|
phase,
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|