Module Base

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

Librería Module Base (Core)

Propósito: definir contratos, estructura y API base para el sistema de módulos dinámicos en aplicaciones web del lado del cliente usando Angular 20, estableciendo estándares arquitectónicos con signals, standalone components y nueva sintaxis de control de flujo.

Estado actual

Rol en el sistema de módulos dinámicos para navegador

Module Base actúa como el ADN del sistema en el navegador, definiendo la estructura genética que seguirán todos los módulos para garantizar consistencia, interoperabilidad, carga dinámica eficiente y mantenibilidad en el entorno web.

API pública planificada

Contratos y Interfaces para navegador

Public Actions

State Management

Component Architecture

Service Layer

Estructura del paquete actualizada

src/
├── lib/
│   ├── contracts/
│   │   ├── module-contract.ts
│   │   ├── module-manifest.ts
│   │   ├── action-contract.ts
│   │   ├── state-contract.ts
│   │   ├── component-contract.ts
│   │   └── service-contract.ts
│   ├── actions/
│   │   ├── module-actions.ts
│   │   ├── action-metadata.ts
│   │   └── public-actions.base.ts
│   ├── state/
│   │   ├── base-module-state.ts
│   │   ├── module-state-actions.ts
│   │   ├── module-state.service.ts
│   │   └── state-selectors.base.ts
│   ├── components/
│   │   ├── base-module.component.ts
│   │   ├── module-layout.component.ts
│   │   ├── module-router-outlet.component.ts
│   │   └── component-base.ts
│   ├── services/
│   │   ├── base-module.service.ts
│   │   ├── module-config.service.ts
│   │   ├── module-communication.service.ts
│   │   └── service-base.ts
│   ├── models/
│   │   ├── module-types.ts
│   │   ├── module-events.ts
│   │   ├── module-permissions.ts
│   │   └── lifecycle-states.ts
│   ├── decorators/
│   │   ├── module.decorator.ts
│   │   ├── public-action.decorator.ts
│   │   ├── module-service.decorator.ts
│   │   └── injectable-module.decorator.ts
│   └── utils/
│       ├── module-validator.ts
│       ├── contract-checker.ts
│       └── dependency-resolver.ts
└── index.ts

Funcionalidad clave

1. Contrato de módulo con Angular 20

// Contrato que debe implementar cada módulo usando signals
export interface BrowserModuleContract {
  readonly manifest: ModuleManifest;
  readonly actions: Signal<ModuleActionRegistry>;
  readonly state: Signal<ModuleStateDefinition>;
  readonly services: Signal<ModuleServiceRegistry>;
  readonly components: Signal<ModuleComponentRegistry>;

  // Signals para estado del módulo
  readonly isInitialized: Signal<boolean>;
  readonly isRunning: Signal<boolean>;
  readonly loadingState: Signal<ModuleLoadingState>;

  // Métodos de ciclo de vida
  initialize(context: ModuleInitializationContext): Promise<void>;
  configure(config: ModuleConfig): Promise<void>;
  start(): Promise<void>;
  stop(): Promise<void>;
  destroy(): Promise<void>;

  // Métodos específicos para Angular 20
  createComponent<T>(componentType: Type<T>): Promise<ComponentRef<T>>;
  registerStandaloneComponents(components: Type<any>[]): void;
}

// Implementación base usando signals
export abstract class BaseModuleContract implements BrowserModuleContract {
  // Signals del módulo
  readonly isInitialized = signal(false);
  readonly isRunning = signal(false);
  readonly loadingState = signal<ModuleLoadingState>('idle');
  readonly actions = signal<ModuleActionRegistry>({});
  readonly state = signal<ModuleStateDefinition>({});
  readonly services = signal<ModuleServiceRegistry>({});
  readonly components = signal<ModuleComponentRegistry>({});

  // Computed signals derivados
  readonly canStart = computed(() => this.isInitialized() && !this.isRunning());

  constructor(public readonly manifest: ModuleManifest, protected readonly injector: EnvironmentInjector) {}

  async createComponent<T>(componentType: Type<T>): Promise<ComponentRef<T>> {
    // Usar createComponent API de Angular 20
    return createComponent(componentType, {
      environmentInjector: this.injector,
    });
  }

  registerStandaloneComponents(components: Type<any>[]): void {
    // Registrar componentes standalone en el registro del módulo
    this.components.update((current) => ({
      ...current,
      ...components.reduce(
        (acc, comp) => ({
          ...acc,
          [comp.name]: comp,
        }),
        {}
      ),
    }));
  }
}

2. Manifest de módulo

// Descriptor completo de un módulo con navegación y embedding
export interface ModuleManifest {
  id: string;
  version: string;
  name: string;
  description: string;

  dependencies: ModuleDependency[];
  permissions: ModulePermission[];
  routes: ModuleRoute[];

  // Navegación y UI
  navigation: ModuleNavigationConfig;
  embeddableComponents: ModuleEmbeddableComponent[];
  publicActions: ModulePublicAction[];

  config: ModuleConfigSchema;
  assets: ModuleAssets;

  // Introspección y comunicación inter-módulo
  exports: ModuleExports;
  imports: ModuleImports;
  provides: ModuleProvider[];
  consumes: ModuleConsumer[];

  // Metadata
  author: string;
  license: string;
  tags: string[];
}

// Configuración de navegación del módulo
export interface ModuleNavigationConfig {
  menuItems: NavigationMenuItem[];
  breadcrumbs?: BreadcrumbConfig[];
  toolbar?: ToolbarConfig[];
}

export interface NavigationMenuItem {
  id: string;
  label: string;
  icon?: string;
  route: string;
  permissions?: string[];
  parent?: string;
  order: number;
  badge?: () => Observable<number>;
}

// Componentes que pueden ser embebidos en otros módulos
export interface ModuleEmbeddableComponent {
  id: string;
  componentType: Type<any>;
  displayName: string;
  description: string;
  inputs?: ComponentInput[];
  permissions?: string[];
  category: 'widget' | 'list' | 'form' | 'dashboard' | 'custom';
}

// Acciones públicas disponibles para otros módulos
export interface ModulePublicAction {
  id: string;
  actionClass: Type<any>;
  displayName: string;
  description: string;
  permissions?: string[];
  category: 'navigation' | 'crud' | 'workflow' | 'integration';
}

// Servicios/datos que el módulo proporciona a otros
export interface ModuleProvider {
  id: string;
  type: 'service' | 'data' | 'component' | 'asset';
  interface: string;
  implementation: Type<any> | string;
  permissions?: string[];
}

// Servicios/datos que el módulo consume de otros
export interface ModuleConsumer {
  moduleId: string;
  providerId: string;
  required: boolean;
  fallback?: any;
}

3. Ejemplos de implementación de módulos

// Ejemplo: Módulo de Productos
export const ProductsModuleManifest: ModuleManifest = {
  id: 'products',
  name: 'Gestión de Productos',
  version: '1.0.0',
  description: 'Sistema de gestión de productos con inventario',

  routes: [
    { path: 'products', component: ProductListComponent, permissions: ['products:read'] },
    { path: 'products/create', component: ProductCreateComponent, permissions: ['products:write'] },
    { path: 'products/:id', component: ProductDetailComponent, permissions: ['products:read'] },
    { path: 'products/:id/edit', component: ProductEditComponent, permissions: ['products:write'] },
  ],

  navigation: {
    menuItems: [
      {
        id: 'products-main',
        label: 'Productos',
        icon: 'inventory',
        route: '/products',
        order: 10,
        permissions: ['products:read'],
      },
      {
        id: 'products-create',
        label: 'Nuevo Producto',
        icon: 'add',
        route: '/products/create',
        parent: 'products-main',
        order: 1,
        permissions: ['products:write'],
      },
    ],
  },

  embeddableComponents: [
    {
      id: 'product-list-widget',
      componentType: ProductListWidgetComponent,
      displayName: 'Lista de Productos',
      description: 'Widget que muestra lista de productos filtrable',
      category: 'widget',
      inputs: [
        { name: 'category', type: 'string', required: false },
        { name: 'limit', type: 'number', required: false, default: 10 },
      ],
      permissions: ['products:read'],
    },
    {
      id: 'product-stats-dashboard',
      componentType: ProductStatsDashboardComponent,
      displayName: 'Estadísticas de Productos',
      description: 'Dashboard con métricas de productos',
      category: 'dashboard',
      permissions: ['products:analytics'],
    },
  ],

  publicActions: [
    {
      id: 'purchase-product',
      actionClass: PurchaseProductAction,
      displayName: 'Comprar Producto',
      description: 'Acción para comprar un producto',
      category: 'workflow',
      permissions: ['products:purchase'],
    },
    {
      id: 'add-to-cart',
      actionClass: AddToCartAction,
      displayName: 'Agregar al Carrito',
      description: 'Añadir producto al carrito de compras',
      category: 'workflow',
      permissions: ['products:read'],
    },
  ],

  provides: [
    {
      id: 'product-data-service',
      type: 'service',
      interface: 'IProductDataService',
      implementation: ProductDataService,
      permissions: ['products:read'],
    },
    {
      id: 'product-images',
      type: 'asset',
      interface: 'IAssetProvider',
      implementation: 'products/assets',
      permissions: ['products:read'],
    },
  ],

  consumes: [
    {
      moduleId: 'media-assets',
      providerId: 'image-service',
      required: true,
    },
    {
      moduleId: 'security',
      providerId: 'auth-service',
      required: true,
    },
  ],

  dependencies: ['media-assets', 'security'],
  permissions: ['products:read', 'products:write', 'products:purchase', 'products:analytics'],
};

// Ejemplo: Módulo CMS
export const CmsModuleManifest: ModuleManifest = {
  id: 'cms',
  name: 'Sistema de Gestión de Contenidos',
  version: '1.0.0',
  description: 'CMS para crear y gestionar contenido',

  routes: [
    { path: 'cms', component: CmsHomeComponent, permissions: ['cms:read'] },
    { path: 'cms/posts', component: PostListComponent, permissions: ['cms:read'] },
    { path: 'cms/posts/create', component: PostCreateComponent, permissions: ['cms:write'] },
    { path: 'cms/posts/:id/edit', component: PostEditComponent, permissions: ['cms:write'] },
  ],

  navigation: {
    menuItems: [
      {
        id: 'cms-main',
        label: 'Contenido',
        icon: 'article',
        route: '/cms',
        order: 20,
        permissions: ['cms:read'],
      },
      {
        id: 'cms-create',
        label: 'Nueva Entrada',
        icon: 'create',
        route: '/cms/posts/create',
        parent: 'cms-main',
        order: 1,
        permissions: ['cms:write'],
      },
    ],
    toolbar: [
      {
        id: 'quick-post',
        label: 'Entrada Rápida',
        icon: 'edit',
        action: 'cms.create-quick-post',
        position: 'global',
        permissions: ['cms:write'],
      },
    ],
  },

  embeddableComponents: [
    {
      id: 'post-creator-widget',
      componentType: QuickPostCreatorComponent,
      displayName: 'Creador de Entradas Rápido',
      description: 'Widget para crear entradas desde cualquier módulo',
      category: 'form',
      permissions: ['cms:write'],
    },
    {
      id: 'recent-posts-widget',
      componentType: RecentPostsComponent,
      displayName: 'Entradas Recientes',
      description: 'Lista de entradas más recientes',
      category: 'widget',
      inputs: [
        { name: 'limit', type: 'number', default: 5 },
        { name: 'category', type: 'string', required: false },
      ],
      permissions: ['cms:read'],
    },
  ],

  publicActions: [
    {
      id: 'create-quick-post',
      actionClass: CreateQuickPostAction,
      displayName: 'Crear Entrada Rápida',
      description: 'Crear una nueva entrada desde cualquier lugar',
      category: 'crud',
      permissions: ['cms:write'],
    },
    {
      id: 'embed-product-reference',
      actionClass: EmbedProductReferenceAction,
      displayName: 'Referenciar Producto',
      description: 'Insertar referencia a producto en contenido',
      category: 'integration',
      permissions: ['cms:write'],
    },
  ],

  consumes: [
    {
      moduleId: 'products',
      providerId: 'product-data-service',
      required: false,
    },
    {
      moduleId: 'products',
      providerId: 'purchase-product',
      required: false,
    },
    {
      moduleId: 'media-assets',
      providerId: 'asset-manager',
      required: true,
    },
  ],

  dependencies: ['media-assets'],
  permissions: ['cms:read', 'cms:write', 'cms:publish'],
};

4. Componente embebible para inter-módulos

// Componente de productos embebible en dashboard
@Component({
  selector: 'app-product-list-widget',
  standalone: true,
  template: `
    <div class="product-widget">
      <h3>{{ title }}</h3>
      @if (loading()) {
      <div class="loading">Cargando productos...</div>
      } @for (product of displayedProducts(); track product.id) {
      <div class="product-item" (click)="selectProduct(product)">
        <img [src]="getProductImage(product)" [alt]="product.name" />
        <div class="product-info">
          <h4>{{ product.name }}</h4>
          <p class="price">{{ product.price | currency }}</p>
          <button (click)="executeAction('purchase-product', product)" [disabled]="!canPurchase()">Comprar</button>
        </div>
      </div>
      }
    </div>
  `,
})
export class ProductListWidgetComponent implements OnInit {
  @Input() category?: string;
  @Input() limit = 10;
  @Input() title = 'Productos';

  // Signals reactivos
  readonly products = signal<Product[]>([]);
  readonly loading = signal(false);

  // Computed signal para productos filtrados
  readonly displayedProducts = computed(() => {
    const allProducts = this.products();
    return this.category ? allProducts.filter((p) => p.category === this.category).slice(0, this.limit) : allProducts.slice(0, this.limit);
  });

  constructor(private productService = inject(ProductDataService), private actionRegistry = inject(ActionRegistryService), private assetService = inject(AssetService), private authService = inject(AuthService)) {}

  async ngOnInit() {
    await this.loadProducts();
  }

  private async loadProducts() {
    this.loading.set(true);
    try {
      const products = await this.productService.getProducts();
      this.products.set(products);
    } finally {
      this.loading.set(false);
    }
  }

  getProductImage(product: Product): string {
    // Usar servicio de assets para obtener imagen del módulo de productos
    return this.assetService.getAssetUrl('products', product.imageId);
  }

  async executeAction(actionId: string, product: Product) {
    // Ejecutar acción pública de otro módulo
    try {
      await this.actionRegistry.execute(actionId, { productId: product.id });
    } catch (error) {
      console.error('Error executing action:', error);
    }
  }

  canPurchase(): boolean {
    return this.authService.hasPermission('products:purchase');
  }

  selectProduct(product: Product) {
    // Emitir evento para navegación
    this.actionRegistry.execute('navigate-to-product', { productId: product.id });
  }
}

4. Estado base con logging integrado

// Estado base con funcionalidades comunes
export abstract class BaseModuleState<T> {
  constructor(protected logSystem: LogSystem, protected eventSystem: EventSystem) {}

  protected logStateChange(action: string, before: T, after: T): void {
    this.logSystem.info(`State change in ${this.constructor.name}`, {
      action,
      before,
      after,
    });
  }

  protected emitStateEvent(event: ModuleStateEvent): void {
    this.eventSystem.emit(event);
  }
}

Integración con el ecosistema

Dependencias fundamentales

Uso en Module Integrator

El module-integrator utilizará module-base para:

Uso en módulos específicos

Los módulos del sistema implementarán:

@ModuleDecorator({
  id: 'user-management',
  version: '1.0.0',
})
export class UserManagementModule implements ModuleContract {
  manifest = USER_MODULE_MANIFEST;

  async initialize(context: ModuleInitializationContext): Promise<void> {
    // Inicialización específica del módulo
  }

  // Implementar resto del contrato...
}

Patrones arquitectónicos

1. Inversión de dependencias

// Los módulos dependen de abstracciones, no de implementaciones
export abstract class BaseDataService<T> {
  abstract findById(id: string): Promise<T>;
  abstract create(entity: T): Promise<T>;
  abstract update(id: string, changes: Partial<T>): Promise<T>;
  abstract delete(id: string): Promise<void>;
}

2. Command Query Responsibility Segregation (CQRS)

// Separación de comandos y consultas
export interface ModuleCommandHandler<TCommand, TResult> {
  handle(command: TCommand): Promise<CommandResult<TResult>>;
}

export interface ModuleQueryHandler<TQuery, TResult> {
  handle(query: TQuery): Promise<QueryResult<TResult>>;
}

3. Domain Events

// Eventos de dominio tipados
export abstract class ModuleDomainEvent {
  readonly aggregateId: string;
  readonly occurredOn: Date;
  readonly eventVersion: number;

  constructor(aggregateId: string) {
    this.aggregateId = aggregateId;
    this.occurredOn = new Date();
    this.eventVersion = 1;
  }
}

Validación y cumplimiento

Contract Validation

// Validador de contratos de módulo
@Injectable()
export class ModuleContractValidator {
  validateContract(module: any): ValidationResult {
    const errors: string[] = [];

    if (!this.implementsModuleContract(module)) {
      errors.push('Module must implement ModuleContract');
    }

    if (!this.hasValidManifest(module.manifest)) {
      errors.push('Module manifest is invalid');
    }

    return { isValid: errors.length === 0, errors };
  }
}

Comandos de mantenimiento

Beneficios del diseño

  1. Consistencia: todos los módulos siguen la misma arquitectura
  2. Interoperabilidad: comunicación estándar entre módulos
  3. Mantenibilidad: cambios centralizados en la base afectan a todos
  4. Testabilidad: contratos claros facilitan testing y mocking
  5. Escalabilidad: nuevos módulos se integran automáticamente
  6. Documentación: contratos sirven como documentación viva

Consideraciones de migración

Para la implementación gradual:

  1. Definir contratos básicos primero
  2. Migrar módulos existentes uno a uno
  3. Mantener compatibilidad durante transición
  4. Validar cumplimiento en CI/CD

Notas de implementación para navegador