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
- Tipo: librería Nx de Angular 20.
- Ruta de importación:
@libs-core/action-registry. - Prioridad:
Alta- fundamental para la comunicación entre módulos en el navegador. - Entorno: Cliente web con signals, httpResource y standalone components de Angular 20.
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
- ActionRegistry: registro central de acciones disponibles
- ActionHandler: ejecutor de acciones con manejo de contexto y resultados
- ActionContext: contexto de ejecución con metadata y estado
- ActionResult: resultado de ejecución con estado, datos y errores
Common Actions
- NavigationActions: acciones de navegación del sistema
- UIActions: acciones de interfaz (modales, notificaciones, themes)
- FormActions: acciones de formularios (validación, envío, reset)
- DataActions: acciones de datos (CRUD, cache, sync)
Module Contracts
- ModuleActionContract: contrato que deben implementar los módulos
- ActionDiscovery: mecanismo de introspección para descubrir acciones de módulos
- ActionMetadata: metadata de acciones (permisos, descripción, parámetros)
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
- @libs-core/log-system: logging de ejecución de acciones
- @libs-core/event-system: eventos de ciclo de vida de acciones
- Seguridad (futura): validación de permisos para ejecutar acciones
Uso en Module Integrator
El module-integrator utilizará action-registry para:
- Descubrir y registrar acciones de módulos cargados dinámicamente
- Coordinar ejecución de acciones entre módulos
- Manejar dependencias entre acciones
Uso en módulos
Los módulos implementarán ModuleActionContract para:
- Exponer sus acciones públicas
- Definir metadata y permisos requeridos
- Integrarse con el sistema de descubrimiento
Comandos de mantenimiento
- Ejecutar pruebas:
nx test action-registry - Linting:
nx lint action-registry - Build:
nx build action-registry
Casos de uso principales
- Navegación entre módulos: módulo A ejecuta acción de navegación del módulo B
- Operaciones coordinadas: workflow que involucra acciones de múltiples módulos
- Extensibilidad: nuevos módulos pueden registrar acciones sin modificar código existente
- Auditoría: todas las acciones son logeadas y pueden ser monitoreadas
Consideraciones de diseño
- Desacoplamiento: módulos no se conocen directamente, solo a través de acciones
- Extensibilidad: fácil agregar nuevas acciones sin impacto en código existente
- Seguridad: validación de permisos antes de ejecutar acciones
- Performance: registro lazy y cache de acciones frecuentes
- Debugging: trazabilidad completa de ejecución de acciones
Notas de implementación
- Utilizar RxJS para manejo asíncrono de acciones
- Implementar retry y circuit breaker para acciones críticas
- Soporte para acciones batch y transaccionales
- Integración con NGXS para acciones que modifiquen estado global