FactuGES_ERP/apps/server/src/lib/modules/module-loader.ts

126 lines
3.8 KiB
TypeScript
Raw Normal View History

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>();
const visiting = new Set<string>(); // para detección de ciclos
2025-05-04 20:06:57 +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) {
if (!pkg?.name) {
throw new Error('❌ Invalid module: missing "name" property');
}
2025-06-11 13:13:21 +00:00
if (registeredModules.has(pkg.name)) {
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);
logger.info(`📦 Module enqueued: "${pkg.name}"`, { label: "moduleRegistry" });
2025-05-04 20:06:57 +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) {
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
});
}
/**
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;
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);
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
// 1) Resolver dependencias primero (en orden)
2025-06-11 13:13:21 +00:00
const deps = pkg.dependencies || [];
for (const dep of deps) {
await loadModule(dep, params, stack.slice());
}
2025-05-04 20:06:57 +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
// 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
// 4) Registrar modelos de Sequelize, si existen
2025-05-04 20:06:57 +00:00
if (pkgApi?.models) {
await withPhase(name, "registerModels", async () => {
await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: pkg.name }));
});
2025-05-04 20:06:57 +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);
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;
}
}