Module Integrator

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Librería Module Integrator (Core)

Propósito: orquestador central responsable del ciclo de vida completo de módulos, gestionando auto-registro, activación basada en configuración API, y coordinación de la integración entre módulos en aplicaciones web Angular.

Estado planificado

Arquitectura del Orquestador Principal

El Module Integrator actúa como el cerebro del sistema, coordinando el auto-registro de módulos, gestionando la activación basada en configuración del API, y orquestando el ciclo de vida completo de todos los módulos. Es responsable de la integración con todas las librerías core del ecosistema.

API pública planificada

Core Services

Local Module Loading Layer

Navigation & UI Integration Layer

Inter-Module Communication Layer

Browser Bootstrap Layer

Validation Layer

Browser Orchestration Layer

Performance Monitoring Layer

Estructura del paquete

src/
├── lib/
│   ├── core/
│   │   ├── integrator.service.ts
│   │   ├── module-registry.service.ts
│   │   ├── dependency-solver.service.ts
│   │   └── lifecycle-manager.service.ts
│   ├── loaders/
│   │   ├── local-module-loader.ts
│   │   ├── dynamic-import.service.ts
│   │   ├── module-bundle-manager.ts
│   │   └── module-route-resolver.ts
│   ├── navigation/
│   │   ├── navigation-manager.service.ts
│   │   ├── embeddable-component-registry.ts
│   │   ├── toolbar-manager.service.ts
│   │   └── breadcrumb.service.ts
│   ├── communication/
│   │   ├── module-provider-registry.ts
│   │   ├── module-consumer-resolver.ts
│   │   ├── cross-module-action.service.ts
│   │   └── asset-sharing.service.ts
│   ├── bootstrap/
│   │   ├── browser-bootstrapper.ts
│   │   ├── config-merger.service.ts
│   │   ├── startup-sequencer.ts
│   │   └── module-activator.ts
│   ├── validation/
│   │   ├── contract-validator.ts
│   │   ├── dependency-validator.ts
│   │   ├── browser-compatibility-checker.ts
│   │   └── security-validator.ts
│   ├── orchestration/
│   │   ├── workflow-coordinator.ts
│   │   ├── async-operation-coordinator.ts
│   │   ├── event-coordinator.ts
│   │   └── state-coordinator.ts
│   ├── monitoring/
│   │   ├── browser-performance-monitor.ts
│   │   ├── memory-usage-monitor.ts
│   │   ├── network-monitor.ts
│   │   └── ux-metrics.service.ts
│   ├── models/
│   │   ├── module-manifest.ts
│   │   ├── dependency-graph.ts
│   │   ├── browser-lifecycle-state.ts
│   │   ├── local-module-config.ts
│   │   ├── navigation-config.ts
│   │   ├── embeddable-component.ts
│   │   └── performance-metrics.ts
│   ├── workers/
│   │   ├── module-loader.worker.ts
│   │   ├── dependency-resolver.worker.ts
│   │   └── performance-analyzer.worker.ts
│   └── utils/
│       ├── local-module-resolver.ts
│       ├── browser-feature-detector.ts
│       ├── route-lazy-loader.ts
│       ├── component-embedder.ts
│       └── module-cache-manager.ts
└── index.ts

Funcionalidad clave

1. Carga dinámica de módulos locales

// Carga dinámica usando dynamic imports locales
@Injectable()
export class IntegratorService {
  async loadModule(moduleId: string): Promise<ModuleInstance> {
    // 1. Validar permisos y contratos del módulo
    await this.securityValidator.validateModulePermissions(moduleId);

    // 2. Resolver dependencias en tiempo de ejecución
    const dependencies = await this.dependencySolver.resolveLocalDependencies(moduleId);

    // 3. Cargar dependencias usando dynamic imports
    await this.loadDependenciesParallel(dependencies);

    // 4. Cargar módulo local via dynamic import
    const moduleInstance = await this.localModuleLoader.loadLocalModule(moduleId);

    // 5. Inicializar en el contexto del navegador
    await this.lifecycleManager.initializeInBrowser(moduleInstance);

    // 6. Registrar y configurar rutas lazy
    this.moduleRegistry.register(moduleInstance);
    await this.routerService.registerLazyRoutes(moduleInstance.routes);

    return moduleInstance;
  }
}

2. Local Module Loader

// Cargador de módulos locales compilados
@Injectable()
export class LocalModuleLoader {
  async loadLocalModule(moduleId: string): Promise<ModuleInstance> {
    const config = await this.getLocalModuleConfig(moduleId);

    try {
      // Cargar módulo local usando dynamic import
      const moduleEntry = await this.resolveLazyRoute(config.lazyRoute);
      const moduleFactory = await import(moduleEntry);

      // Crear instancia del módulo
      const moduleInstance = await moduleFactory.default();

      return this.wrapModuleInstance(moduleInstance, config);
    } catch (error) {
      // Log error y propagar
      this.logger.error(`Failed to load local module ${moduleId}:`, error);
      throw new ModuleLoadError(moduleId, error);
    }
  }

  private async resolveLazyRoute(lazyRoute: string): Promise<string> {
    // Resolver ruta lazy basada en configuración de routing
    const routeConfig = await this.routeResolver.resolve(lazyRoute);

    // Validar que el chunk existe en el bundle
    if (!(await this.bundleManager.chunkExists(routeConfig.chunkName))) {
      throw new Error(`Chunk ${routeConfig.chunkName} not found`);
    }

    return routeConfig.importPath;
  }
}

3. Configuración dinámica via API

// Configuración desde APIs REST
@Injectable()
export class ConfigMergerService {
  async loadAndMergeConfigs(moduleIds: string[]): Promise<ApplicationConfig> {
    // Cargar configuraciones desde API
    const configPromises = moduleIds.map((id) => this.apiClient.get<ModuleConfig>(`/api/modules/${id}/config`));

    const configs = await Promise.all(configPromises);

    // Merge inteligente con validación
    return configs.reduce((merged, config) => {
      return this.deepMergeWithValidation(merged, config, {
        conflictStrategy: 'api-priority',
        validateSchema: true,
        sanitizeInputs: true,
      });
    }, {} as ApplicationConfig);
  }

  private deepMergeWithValidation(target: any, source: any, options: MergeOptions): any {
    // Validar esquema antes de merge
    this.configValidator.validate(source, options.schema);

    // Sanitizar inputs para prevenir XSS
    const sanitizedSource = this.sanitizer.sanitize(source);

    // Merge recursivo con detección de conflictos
    return this.performDeepMerge(target, sanitizedSource, options);
  }
}

4. Coordinación con Web Workers

// Coordinación usando Web Workers para operaciones pesadas
@Injectable()
export class EventCoordinator {
  private workerPool: WorkerPool;

  async coordinateEvent(event: SystemEvent): Promise<void> {
    const interestedModules = this.findInterestedModules(event);

    // Para eventos pesados, usar Web Workers
    if (this.isHeavyEvent(event)) {
      return this.coordinateInWorker(event, interestedModules);
    }

    // Para eventos ligeros, ejecutar en main thread
    return this.coordinateInMainThread(event, interestedModules);
  }

  private async coordinateInWorker(event: SystemEvent, modules: ModuleInstance[]): Promise<void> {
    const worker = await this.workerPool.getAvailableWorker();

    return new Promise((resolve, reject) => {
      worker.postMessage({
        type: 'COORDINATE_EVENT',
        event: this.serializeEvent(event),
        modules: modules.map((m) => m.id),
      });

      worker.onmessage = (result) => {
        if (result.data.success) {
          resolve(result.data.results);
        } else {
          reject(new Error(result.data.error));
        }
      };
    });
  }

  private async coordinateInMainThread(event: SystemEvent, modules: ModuleInstance[]): Promise<void> {
    // Usar requestIdleCallback para no bloquear UI
    return new Promise((resolve) => {
      requestIdleCallback(async () => {
        const results = await Promise.allSettled(modules.map((module) => this.executeEventHandler(module, event)));

        await this.handleResults(results, event);
        resolve();
      });
    });
  }
}

5. Navegación y UI inter-módulo

// Gestor de navegación que integra múltiples módulos
@Injectable({ providedIn: 'root' })
export class NavigationManagerService {
  private readonly navigationItems = signal<NavigationMenuItem[]>([]);
  private readonly toolbarActions = signal<ToolbarAction[]>([]);

  // Computed signals para navegación organizada
  readonly mainMenuItems = computed(() =>
    this.navigationItems()
      .filter((item) => !item.parent)
      .sort((a, b) => a.order - b.order)
  );

  readonly globalToolbarActions = computed(() =>
    this.toolbarActions()
      .filter((action) => action.position === 'global')
      .sort((a, b) => a.order - b.order)
  );

  constructor(private router = inject(Router), private authService = inject(AuthService)) {}

  // Registrar navegación de un módulo
  registerModuleNavigation(moduleId: string, navigation: ModuleNavigationConfig): void {
    // Registrar items de menú
    navigation.menuItems.forEach((item) => {
      if (this.hasPermission(item.permissions)) {
        this.navigationItems.update((items) => [...items, { ...item, moduleId }]);
      }
    });

    // Registrar acciones de toolbar
    navigation.toolbar?.forEach((action) => {
      if (this.hasPermission(action.permissions)) {
        this.toolbarActions.update((actions) => [...actions, { ...action, moduleId }]);
      }
    });
  }

  // Ejemplo: Navegación del módulo de productos
  private registerProductsNavigation(): void {
    this.registerModuleNavigation('products', {
      menuItems: [
        {
          id: 'products-main',
          label: 'Productos',
          icon: 'inventory',
          route: '/products',
          order: 10,
          permissions: ['products:read'],
          badge: () => this.getProductCount(),
        },
        {
          id: 'products-create',
          label: 'Nuevo Producto',
          icon: 'add',
          route: '/products/create',
          parent: 'products-main',
          order: 1,
          permissions: ['products:write'],
        },
      ],
    });
  }

  private hasPermission(permissions?: string[]): boolean {
    if (!permissions) return true;
    return permissions.some((p) => this.authService.hasPermission(p));
  }

  private getProductCount(): Observable<number> {
    // Retorna observable con count de productos
    return this.moduleProviderRegistry.get('products', 'product-count') || of(0);
  }
}

6. Registro de componentes embebibles

// Registro de componentes que pueden ser embebidos entre módulos
@Injectable({ providedIn: 'root' })
export class EmbeddableComponentRegistry {
  private readonly registeredComponents = signal<Map<string, ModuleEmbeddableComponent>>(new Map());

  // Computed signals para componentes por categoría
  readonly widgetComponents = computed(() => Array.from(this.registeredComponents().values()).filter((comp) => comp.category === 'widget'));

  readonly dashboardComponents = computed(() => Array.from(this.registeredComponents().values()).filter((comp) => comp.category === 'dashboard'));

  // Registrar componente embebible
  registerComponent(moduleId: string, component: ModuleEmbeddableComponent): void {
    const key = `${moduleId}:${component.id}`;
    this.registeredComponents.update((components) => {
      const newMap = new Map(components);
      newMap.set(key, { ...component, moduleId });
      return newMap;
    });
  }

  // Crear instancia de componente embebible
  async createEmbeddableComponent(componentId: string, inputs?: Record<string, any>): Promise<ComponentRef<any>> {
    const component = this.getComponent(componentId);
    if (!component) {
      throw new Error(`Component ${componentId} not found`);
    }

    // Usar createComponent API de Angular 20
    const componentRef = await createComponent(component.componentType, {
      environmentInjector: this.injector,
    });

    // Configurar inputs si existen
    if (inputs && componentRef.setInput) {
      Object.entries(inputs).forEach(([key, value]) => {
        componentRef.setInput(key, value);
      });
    }

    return componentRef;
  }

  // Obtener componentes disponibles para un dashboard
  getAvailableWidgetsForDashboard(): ModuleEmbeddableComponent[] {
    return this.widgetComponents().filter((comp) => this.authService.hasPermissions(comp.permissions));
  }

  // Ejemplo: Usar widget de productos en dashboard
  async embedProductListInDashboard(dashboardContainer: ViewContainerRef, config: { category?: string; limit?: number }): Promise<ComponentRef<any>> {
    const productWidget = await this.createEmbeddableComponent('products:product-list-widget', {
      category: config.category,
      limit: config.limit || 5,
      title: 'Productos Destacados',
    });

    dashboardContainer.insert(productWidget.hostView);
    return productWidget;
  }
}

7. Comunicación cross-module

// Servicio para comunicación entre módulos
@Injectable({ providedIn: 'root' })
export class CrossModuleActionService {
  constructor(private actionRegistry = inject(ActionRegistryService), private moduleRegistry = inject(ModuleRegistryService), private assetSharing = inject(AssetSharingService)) {}

  // Ejemplo: CMS ejecutando acción de productos
  async executeCrossModuleAction(fromModuleId: string, targetModuleId: string, actionId: string, parameters: any): Promise<any> {
    // Verificar que el módulo target esté cargado
    const targetModule = this.moduleRegistry.getModule(targetModuleId);
    if (!targetModule) {
      throw new Error(`Target module ${targetModuleId} not loaded`);
    }

    // Verificar permisos cross-module
    await this.validateCrossModulePermissions(fromModuleId, targetModuleId, actionId);

    // Ejecutar acción
    const fullActionId = `${targetModuleId}.${actionId}`;
    return this.actionRegistry.execute(fullActionId, parameters);
  }

  // Ejemplo específico: CMS usando acción de compra de productos
  async purchaseProductFromCms(productId: string, cmsPostId: string): Promise<any> {
    return this.executeCrossModuleAction('cms', 'products', 'purchase-product', {
      productId,
      referrer: `cms:${cmsPostId}`,
      context: 'cms-integration',
    });
  }

  // Obtener datos de otro módulo
  async getDataFromModule<T>(moduleId: string, providerId: string, parameters?: any): Promise<T> {
    const provider = this.moduleProviderRegistry.getProvider(moduleId, providerId);
    if (!provider) {
      throw new Error(`Provider ${providerId} not found in module ${moduleId}`);
    }

    if (provider.type === 'service') {
      const service = this.injector.get(provider.implementation);
      return service.getData(parameters);
    } else if (provider.type === 'data') {
      return provider.implementation(parameters);
    }

    throw new Error(`Unsupported provider type: ${provider.type}`);
  }
}

8. Manejo de assets entre módulos

// Servicio para compartir assets entre módulos
@Injectable({ providedIn: 'root' })
export class AssetSharingService {
  private readonly assetProviders = signal<Map<string, AssetProvider>>(new Map());

  // Registrar proveedor de assets de un módulo
  registerAssetProvider(moduleId: string, provider: AssetProvider): void {
    this.assetProviders.update((providers) => {
      const newMap = new Map(providers);
      newMap.set(moduleId, provider);
      return newMap;
    });
  }

  // Obtener URL de asset de cualquier módulo
  getAssetUrl(moduleId: string, assetId: string, options?: AssetOptions): string {
    const provider = this.assetProviders().get(moduleId);
    if (!provider) {
      throw new Error(`Asset provider for module ${moduleId} not found`);
    }

    return provider.getAssetUrl(assetId, options);
  }

  // Ejemplo: CMS usando imágenes del módulo de productos
  getProductImageForCms(productId: string, size: 'thumbnail' | 'medium' | 'large' = 'medium'): string {
    return this.getAssetUrl('products', `product-${productId}`, { size });
  }

  // Ejemplo: Dashboard mostrando assets de múltiples módulos
  getModuleAssets(moduleId: string, category?: string): Observable<Asset[]> {
    const provider = this.assetProviders().get(moduleId);
    if (!provider) {
      return of([]);
    }

    return provider.getAssets({ category });
  }
}

Integración con el ecosistema

Dependencias principales

Interacción con Action Registry

// Registro automático de acciones de módulos
async initializeModule(module: ModuleInstance): Promise<void> {
  // Descubrir y registrar acciones del módulo
  const actions = await this.actionDiscovery.discover(module);
  await this.actionRegistry.registerBatch(actions);

  // Configurar permisos y metadata
  await this.setupActionSecurity(actions, module.permissions);
}

Interacción con Data Access

// Configuración de contexto de datos por módulo
async setupDataContext(module: ModuleInstance): Promise<void> {
  // Configurar tenant context específico
  const tenantConfig = module.manifest.tenantConfig;
  await this.tenantContext.configure(module.id, tenantConfig);

  // Registrar repositorios y estados NGXS
  await this.dataAccess.registerModuleRepositories(module);
  await this.stateManager.registerModuleStates(module);
}

Ciclo de vida de módulos

Estados del ciclo de vida

  1. DISCOVERING: descubrimiento de módulo y manifest
  2. VALIDATING: validación de contratos y dependencias
  3. LOADING: carga de código del módulo
  4. INITIALIZING: inicialización de servicios
  5. CONFIGURING: configuración y merge de config
  6. STARTING: arranque de funcionalidades
  7. RUNNING: estado activo y operacional
  8. STOPPING: parada controlada
  9. UNLOADING: descarga de memoria
  10. ERROR: estado de error recuperable

Transiciones de estado

@Injectable()
export class LifecycleManagerService {
  async transitionTo(moduleId: string, targetState: LifecycleState): Promise<void> {
    const currentState = this.getModuleState(moduleId);
    const transitions = this.getValidTransitions(currentState, targetState);

    for (const transition of transitions) {
      await this.executeTransition(moduleId, transition);
      await this.validateTransition(moduleId, transition);
    }
  }
}

Patrones de integración

1. Plugin Architecture

// Extensibilidad a través de plugins
interface IntegratorPlugin {
  name: string;
  hooks: {
    beforeModuleLoad?: (manifest: ModuleManifest) => Promise<void>;
    afterModuleLoad?: (module: ModuleInstance) => Promise<void>;
    onModuleError?: (error: ModuleError) => Promise<void>;
  };
}

2. Micro-frontend Support

// Soporte para micro-frontends
class MicrofrontendLoader implements ModuleLoader {
  async load(manifest: ModuleManifest): Promise<ModuleInstance> {
    if (manifest.type === 'microfrontend') {
      return this.loadRemoteModule(manifest.remoteEntry);
    }
    return this.loadLocalModule(manifest.entry);
  }
}

Monitoreo y observabilidad

Métricas clave

Health checks

@Injectable()
export class HealthCheckService {
  async checkModuleHealth(moduleId: string): Promise<HealthStatus> {
    const checks = [this.checkMemoryUsage(moduleId), this.checkResponseTime(moduleId), this.checkDependencies(moduleId), this.checkEventProcessing(moduleId)];

    return this.aggregateResults(await Promise.all(checks));
  }
}

Comandos de mantenimiento

Casos de uso principales

Casos generales:

  1. Carga dinámica: cargar módulos bajo demanda basado en rutas o permisos
  2. Hot reload: actualizar módulos sin reiniciar la aplicación
  3. A/B Testing: cargar diferentes versiones de módulos
  4. Feature flags: habilitar/deshabilitar módulos dinámicamente
  5. Multi-tenancy: configuración específica de módulos por tenant

Casos específicos implementados:

Módulo de Productos:
Módulo CMS:
Flujos de trabajo específicos:

Ejemplo 1: Dashboard con productos

// Dashboard carga widget de productos automáticamente
async loadDashboard() {
  const productWidget = await this.embeddableRegistry.embedProductListInDashboard(
    this.dashboardContainer,
    { category: 'featured', limit: 6 }
  );

  // Widget muestra productos con imágenes del módulo de assets
  // Incluye botones de compra que ejecutan acciones del módulo productos
}

Ejemplo 2: CMS referenciando productos

// Editor CMS puede insertar referencias a productos
async insertProductReference(productId: string) {
  // Obtener datos del producto desde el módulo productos
  const product = await this.crossModuleAction.getDataFromModule(
    'products',
    'product-data-service',
    { productId }
  );

  // Obtener imagen del producto
  const imageUrl = this.assetSharing.getProductImageForCms(productId, 'medium');

  // Insertar en editor con acción de compra
  const productHTML = `
    <div class="product-reference">
      <img src="${imageUrl}" alt="${product.name}" />
      <button onclick="purchaseProduct('${productId}')">Comprar ${product.name}</button>
    </div>
  `;
}

Ejemplo 3: Navegación contextual

// Navegación se actualiza automáticamente según módulos cargados
const navigation = {
  // Productos (si tiene permisos)
  Productos: [
    'Ver Productos', // /products
    'Nuevo Producto', // /products/create
    'Categorías', // /products/categories
  ],
  // CMS (si tiene permisos)
  Contenido: [
    'Entradas', // /cms/posts
    'Nueva Entrada', // /cms/posts/create
    'Páginas', // /cms/pages
  ],
};

Consideraciones de diseño para navegador

Estrategias avanzadas

Circuit Breaker Pattern

// Protección contra fallos en cascada
class ModuleCircuitBreaker {
  async execute<T>(moduleId: string, operation: () => Promise<T>): Promise<T> {
    const state = this.getCircuitState(moduleId);

    if (state === 'OPEN') {
      throw new CircuitOpenError(moduleId);
    }

    try {
      const result = await operation();
      this.recordSuccess(moduleId);
      return result;
    } catch (error) {
      this.recordFailure(moduleId);
      throw error;
    }
  }
}

Notas de implementación para navegador (módulos locales)