Ir al contenido

Eventos de dominio

Los eventos de dominio son el mecanismo de desacoplamiento que permite que las funcionalidades se comuniquen sin depender directamente unas de otras. En lugar de que la funcionalidad A llame a la funcionalidad B, la funcionalidad A publica un evento y la funcionalidad B se suscribe a él.

El DomainEventBus es un bus de publicación/suscripción con difusión y en proceso. Quien publica llama a publish(event); quien se suscribe consume un flujo tipado on<T>(). Todos los eventos implementan DomainEvent y llevan occurredAt.

El bus se ejecuta dentro de cc_server. Tanto quien publica como quien está suscrito en vivo residen ahí. Ningún cliente ve este bus. Lo que ve un cliente es un subconjunto seleccionado que el servidor reemite como tramas JSON-RPC notifications/*.

Sin eventos, cada reacción entre funcionalidades sería una importación. La ruta de notificaciones tendría que importar la funcionalidad de agentes, la de pull requests, la de tickets y la de reuniones solo para saber cuándo mostrar un aviso, y cada una de ellas tendría entonces que saber que existen las notificaciones. La de tickets importaría la de pipelines para que un ticket completado pudiera hacer avanzar una ejecución. El grafo de dependencias se cierra en un nudo y el kernel compartido deja de ser compartido.

Publicar un evento rompe esa dinámica. La ruta de notificaciones se suscribe a veintiuna clases de evento y no importa ninguna de las funcionalidades que los emiten. El despachador de disparadores de pipeline se suscribe a todo el bus y no sabe nada de pull requests, tickets ni reuniones. La observabilidad agrega resultados de ejecuciones sin meterse en el despacho.

El coste es el de siempre: quien publica no puede saber si alguien está escuchando, y un suscriptor que nunca se instancia queda inerte en silencio. TicketRemoteSyncHandler es el ejemplo permanente: está declarado, tiene cobertura de tests y nunca se conecta en producción.

Conviene ser precisos sobre una cosa que el bus no posee.

El registro de auditoría no es una proyección del flujo de eventos. Toda operación RPC que muta estado se audita de forma predeterminada: RepoOp.audited vale true por omisión y una operación solo queda exenta si declara audited: false, un acto deliberado y visible en la propia operación. El despachador añade la fila de «quién hizo qué y desde dónde» justo después de una llamada correcta. No existe ningún DomainEventAuditBridge. ActivityLogPersister escribe filas de activity_log a partir de eventos ActivityLogged; ese es un flujo distinto del de la auditoría RPC.

El bus de eventos sí es el transporte de las notificaciones y de la automatización. No es el sistema de registro contable.

  • Notificaciones. El servidor convierte veintiuna clases de evento en tramas notifications/*, las registra en un flujo duradero por espacio de trabajo y las difunde a las sesiones conectadas. Una trama cuyo workspace_id indique un espacio de trabajo del que el usuario de la sesión no es miembro se descarta en el servidor; el cliente filtra después el resto según el espacio de trabajo que esté viendo.
  • Automatización basada en eventos. Los disparadores de pipeline se suscriben al bus e inician automáticamente los pipelines que coinciden (ver más abajo).
  • Reacciones del ciclo de vida. Al crear un espacio de trabajo se aprovisionan sus agentes CEO y especialistas, las plantillas de pipeline integradas y las suites de evaluación iniciales. Una ejecución de agente completada reanuda un paso de pipeline suspendido, cierra una tarea, alimenta al supervisor de objetivos, puede despertar a un verificador con nombre y puede volver a activar a un líder de equipo. Una PR fusionada o un espacio eliminado activa la recolección de basura de los worktrees.
  • Sincronización entre proveedores. Un coordinador que escucha cinco eventos de tickets envía los cambios locales de tickets a los proveedores configurados.

Los eventos abarcan toda la superficie del producto. A grandes rasgos:

  • Espacios de trabajo, agentes y repos — creación de espacios de trabajo, finalización de ejecuciones, registro de repos, actualizaciones de skills
  • Pull requests — publicación, cambios de estado, fusiones, solicitudes de revisión, menciones, seguimiento de PRs propias, PRs detectadas externamente
  • Mensajería — mensajes, espacios y progreso de aprovisionamiento de espacios
  • Tickets — creación, asignación, reasignación, detalles, estado y los tres resultados terminales
  • Tareas — tramas secuenciadas del ciclo de vida para una ejecución despachada, para que los clientes puedan ordenarlas y eliminar duplicados
  • Pipelines — solo eventos terminales de ejecución (completado, cancelado, fallido)
  • Observabilidad — entradas del registro de actividad y umbrales de presupuesto
  • Identidad y pertenencia — miembro añadido, eliminado, rol cambiado
  • Calendario y reuniones — caducidad de la autenticación, inicio de reuniones y fin de grabaciones
  • Rigs — toma de control, recuperación y cierre inesperado

La memoria, la orquestación, los documentos de plan, los artefactos, las aprobaciones, la entrada de sincronización de tickets, la actualización del calendario, las fusiones de worktrees, la creación de usuarios, las invitaciones y la revocación de dispositivos no son eventos de dominio; consulta la referencia.

Para ver el catálogo completo —las 53 clases de evento concretas, sus cargas útiles y los 24 suscriptores que las consumen—, consulta la referencia de eventos de dominio. Esa página es el inventario; esta es el modelo.

El control de acceso en vivo usa dos mecanismos distintos

Sección titulada «El control de acceso en vivo usa dos mecanismos distintos»

Los eventos de pertenencia y los de dispositivos parecen simétricos y no lo son, algo que importa si analizas la rapidez con la que surte efecto una revocación.

Revocar un dispositivo sí termina con sus sesiones. El servidor vigila la tabla de dispositivos emparejados y descarta cualquier sesión activa cuyo dispositivo haya salido del conjunto activo, en cuestión de segundos.

Eliminar a un miembro no cierra el socket. WorkspaceMemberRemoved sí se consume: LocalRpcServer y RemoteRelayHost descartan las suscripciones a espacios de trabajo que ese usuario tenía enganchadas, y RemoteEventForwarder invalida su caché de pertenencia para que no se envíen más tramas de ese espacio de trabajo. Lo que deniega el siguiente RPC sigue siendo la comprobación de roles, que vuelve a resolver la pertenencia en cada llamada, así que su siguiente petición se rechaza con «Not a member of this workspace». El efecto es inmediato para cualquier cosa que intenten hacer; simplemente no se consigue colgándoles la conexión.

Los disparadores de pipeline se suscriben a los eventos de dominio e inician automáticamente los pipelines que coinciden. PipelineTriggerDispatcher escucha todo el bus y, a continuación:

  1. Cortocircuita salvo que el tipo de evento sea uno que sabe convertir en una carga útil
  2. Busca, en todos los espacios de trabajo, los disparadores habilitados que coinciden con ese tipo de evento
  3. Filtra cada uno a su propio espacio de trabajo y aplica su filtro de coincidencia de carga útil
  4. Inicia una ejecución por cada disparador que sobrevive

La restricción importante es el paso 1. El bus transporta 53 tipos de evento; el selector para añadir un disparador solo inicia una ejecución con estos catorce (ramas de carga útil de EventPayloadMapper): ExternalPrDetected, PullRequestPublished, PullRequestStatusChanged, PrMerged, MessageReceived, TicketAssigned, TicketCompleted, TicketFailed, TicketCancelled, BudgetThresholdCrossed, RepoAdded, MeetingRecordingStopped, SkillUpdated y SpaceDeleted. Ningún otro evento puede iniciar un pipeline.

RemoteEventForwarder traduce los eventos de la clase de notificaciones a tramas y las envía por RPC a las sesiones conectadas, donde un mapeador de tramas las convierte en notificaciones dentro de la aplicación. Un cliente ligero ve, por tanto, una proyección del flujo de eventos del servidor sin ser responsable de ninguna ejecución, que es justo la razón de ser de la separación de cliente ligero.

No obstante, no todas las tramas enviadas son una notificación. El flujo del ciclo de vida de las tareas y la reasignación de tickets se reenvían en vivo para alimentar la interfaz, pero no se registran en el flujo de notificaciones ni muestran ningún aviso.