Action Registry

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

Librería Action Registry (Core)

Propósito: registrar y ejecutar acciones de módulos utilizando el patrón Command para desacoplar operaciones del sistema en aplicaciones web del lado del cliente con Angular 20.

Estado planificado

Arquitectura del patrón Command para Angular 20

La librería implementa el patrón Command aprovechando signals para estado reactivo, httpResource para comunicación HTTP declarativa, y la nueva API createComponent para ejecución dinámica de acciones en el navegador.

API pública planificada

Core Components

Common Actions

Module Contracts

Estructura del paquete

src/
├── lib/
│   ├── core/
│   │   ├── action-registry.service.ts
│   │   ├── action-handler.service.ts
│   │   ├── action-context.ts
│   │   └── action-result.ts
│   ├── actions/
│   │   ├── navigation-actions.ts
│   │   ├── ui-actions.ts
│   │   ├── form-actions.ts
│   │   └── data-actions.ts
│   ├── contracts/
│   │   ├── module-action-contract.ts
│   │   ├── action-discovery.ts
│   │   └── action-metadata.ts
│   ├── models/
│   │   ├── action-types.ts
│   │   └── execution-context.ts
│   └── utils/
│       ├── action-validators.ts
│       └── permission-checker.ts
└── index.ts

Funcionalidad clave con Angular 20

1. Registro dinámico con signals

// Servicio de registro usando signals de Angular 20
@Injectable({ providedIn: 'root' })
export class ActionRegistryService {
  // Signal para acciones registradas
  private readonly actionsMap = signal<Map<string, ActionMetadata>>(new Map());

  // Signal computed para acciones disponibles
  readonly availableActions = computed(() => Array.from(this.actionsMap().values()));

  // Signal para estado de ejecución
  readonly executionState = signal<ActionExecutionState>('idle');

  register(actionId: string, actionClass: ActionConstructor, metadata: ActionMetadata): void {
    const actions = this.actionsMap();
    actions.set(actionId, {
      id: actionId,
      actionClass,
      ...metadata,
      registeredAt: new Date(),
    });

    // Actualizar signal
    this.actionsMap.set(new Map(actions));

    // Emitir evento usando effect
    effect(() => {
      this.eventBus.emit(new ActionRegisteredEvent(actionId, metadata));
    });
  }
}

2. Ejecución con httpResource y createComponent

// Ejecutor de acciones con Angular 20 APIs
@Injectable({ providedIn: 'root' })
export class ActionHandlerService {
  private readonly componentRef = signal<ComponentRef<any> | null>(null);

  // httpResource para acciones remotas
  private readonly remoteActionResource = httpResource({
    url: computed(() => `/api/actions/${this.currentAction()?.id}`),
    loader: (params: ActionParams) => this.httpClient.post(this.buildActionUrl(), params),
  });

  constructor(private viewContainer: ViewContainerRef, private httpClient: HttpClient, private injector: Injector) {}

  async execute<T>(actionId: string, parameters: ActionParams, context: ActionContext): Promise<ActionResult<T>> {
    const metadata = this.getActionMetadata(actionId);

    if (metadata.type === 'component') {
      return this.executeComponentAction<T>(metadata, parameters, context);
    } else if (metadata.type === 'http') {
      return this.executeHttpAction<T>(metadata, parameters, context);
    } else {
      return this.executeLocalAction<T>(metadata, parameters, context);
    }
  }

  private async executeComponentAction<T>(metadata: ActionMetadata, parameters: ActionParams, context: ActionContext): Promise<ActionResult<T>> {
    try {
      // Usar createComponent API de Angular 20
      const componentRef = await createComponent(metadata.actionClass, {
        environmentInjector: this.injector,
        hostElement: this.viewContainer.element.nativeElement,
      });

      // Configurar inputs usando signals
      if (componentRef.setInput) {
        Object.entries(parameters).forEach(([key, value]) => {
          componentRef.setInput(key, value);
        });
      }

      // Ejecutar acción del componente
      const result = await componentRef.instance.execute(parameters, context);

      this.componentRef.set(componentRef);

      return {
        success: true,
        data: result,
        executedAt: new Date(),
        actionId: metadata.id,
      };
    } catch (error) {
      return {
        success: false,
        error: error as Error,
        executedAt: new Date(),
        actionId: metadata.id,
      };
    }
  }

  private async executeHttpAction<T>(metadata: ActionMetadata, parameters: ActionParams, context: ActionContext): Promise<ActionResult<T>> {
    try {
      // Usar httpResource para acciones HTTP
      const result = await this.remoteActionResource.load({
        ...parameters,
        context: context.toJSON(),
      });

      return {
        success: true,
        data: result,
        executedAt: new Date(),
        actionId: metadata.id,
      };
    } catch (error) {
      return {
        success: false,
        error: error as Error,
        executedAt: new Date(),
        actionId: metadata.id,
      };
    }
  }
}

3. Introspección reactiva con computed signals

// Servicio de descubrimiento con signals reactivos
@Injectable({ providedIn: 'root' })
export class ActionDiscoveryService {
  private readonly moduleActions = signal<Map<string, ActionMetadata[]>>(new Map());

  // Computed signal para acciones filtradas por módulo
  getModuleActions = (moduleId: string) => computed(() => this.moduleActions().get(moduleId) ?? []);

  // Computed signal para acciones por permisos del usuario
  readonly actionsForCurrentUser = computed(() => {
    const userPermissions = this.securityService.currentUserPermissions();
    const allActions = Array.from(this.moduleActions().values()).flat();

    return allActions.filter((action) => this.hasRequiredPermissions(action.permissions, userPermissions));
  });

  // Effect para monitorear cambios en acciones
  constructor() {
    effect(() => {
      const actions = this.actionsForCurrentUser();
      this.logger.debug(`Available actions updated: ${actions.length} actions`);
    });
  }

  discoverModuleActions(moduleId: string): void {
    // Usar reflection para descubrir acciones automáticamente
    const moduleMetadata = this.moduleRegistry.getModuleMetadata(moduleId);
    const actions: ActionMetadata[] = [];

    // Escanear decoradores @PublicAction en el módulo
    for (const component of moduleMetadata.components) {
      const actionMetadata = this.extractActionMetadata(component);
      if (actionMetadata) {
        actions.push(actionMetadata);
      }
    }

    // Actualizar signal
    const current = this.moduleActions();
    current.set(moduleId, actions);
    this.moduleActions.set(new Map(current));
  }
}

4. Componente de acción standalone

// Componente de acción standalone con nueva sintaxis
@Component({
  selector: 'app-action-executor',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <div class="action-executor">
      @if (executionState() === 'executing') {
      <div class="loading">Ejecutando acción...</div>
      } @if (executionState() === 'success') {
      <div class="success">
        Acción ejecutada exitosamente @if (result()?.data) {
        <pre>{{ result()?.data | json }}</pre>
        }
      </div>
      } @if (executionState() === 'error') {
      <div class="error">Error: {{ error()?.message }}</div>
      } @for (action of availableActions(); track action.id) {
      <button (click)="executeAction(action.id)" [disabled]="!canExecuteAction(action)">
        {{ action.description }}
      </button>
      }
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActionExecutorComponent {
  readonly actionHandler = inject(ActionHandlerService);
  readonly actionRegistry = inject(ActionRegistryService);

  // Signals del componente
  readonly executionState = signal<'idle' | 'executing' | 'success' | 'error'>('idle');
  readonly result = signal<ActionResult<any> | null>(null);
  readonly error = signal<Error | null>(null);

  // Computed signals
  readonly availableActions = this.actionRegistry.availableActions;
  readonly canExecuteAction = (action: ActionMetadata) => computed(() => this.hasPermissions(action.permissions));

  async executeAction(actionId: string): Promise<void> {
    this.executionState.set('executing');
    this.error.set(null);

    try {
      const result = await this.actionHandler.execute(actionId, {}, {});

      if (result.success) {
        this.result.set(result);
        this.executionState.set('success');
      } else {
        this.error.set(result.error);
        this.executionState.set('error');
      }
    } catch (error) {
      this.error.set(error as Error);
      this.executionState.set('error');
    }
  }
}

Integración con el sistema

Dependencias

Uso en Module Integrator

El module-integrator utilizará action-registry para:

Uso en módulos

Los módulos implementarán ModuleActionContract para:

Comandos de mantenimiento

Casos de uso principales

  1. Navegación entre módulos: módulo A ejecuta acción de navegación del módulo B
  2. Operaciones coordinadas: workflow que involucra acciones de múltiples módulos
  3. Extensibilidad: nuevos módulos pueden registrar acciones sin modificar código existente
  4. Auditoría: todas las acciones son logeadas y pueden ser monitoreadas

Consideraciones de diseño

Notas de implementación