import { IModuleServer, ModuleParams } from "@erp/core/api"; import { logger } from "../logger"; import { initModels, registerModels } from "./model-loader"; import { registerService } from "./service-registry"; const registeredModules: Map = new Map(); const initializedModules = new Set(); const visiting = new Set(); // para detección de ciclos /** Registra un módulo del servidor en el registry. Lanza error si el nombre ya existe. */ export function registerModule(pkg: IModuleServer) { if (!pkg?.name) { throw new Error('❌ Invalid module: missing "name" property'); } if (registeredModules.has(pkg.name)) { throw new Error(`❌ Module "${pkg.name}" already registered`); } registeredModules.set(pkg.name, pkg); logger.info(`📦 Module enqueued: "${pkg.name}"`, { label: "moduleRegistry" }); } /** Inicializa todos los módulos registrados (resolviendo dependencias), y al final inicializa los modelos (Sequelize) en bloque. */ 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); }); } /** Carga recursivamente un módulo y sus dependencias con detección de ciclos. */ async function loadModule(name: string, params: ModuleParams, stack: string[]) { 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}`); } 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); // 1) Resolver dependencias primero (en orden) const deps = pkg.dependencies || []; for (const dep of deps) { await loadModule(dep, params, stack.slice()); } // 2) Inicializar el módulo (permite async) await withPhase(name, "init", async () => { await Promise.resolve(pkg.init(params)); }); // 3) Registrar dependencias que expone (permite async) const pkgApi = await withPhase(name, "registerDependencies", async () => { return await Promise.resolve(pkg.registerDependencies?.(params)); }); // 4) Registrar modelos de Sequelize, si existen if (pkgApi?.models) { await withPhase(name, "registerModels", async () => { await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: pkg.name })); }); } // 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)); }); } 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( moduleName: string, phase: "init" | "registerDependencies" | "registerModels" | "registerServices" | "initModels", fn: () => Promise | T ): Promise { 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; } }