Pular para o conteúdo

Eventos de domínio

Eventos de domínio são o mecanismo de desacoplamento que permite às features se comunicarem sem dependências diretas. Em vez de a feature A chamar a feature B, a feature A publica um evento e a feature B o assina.

O DomainEventBus é um barramento de publicação/assinatura por difusão que roda em processo. Os publicadores chamam publish(event); os assinantes consomem um stream tipado on<T>(). Todo evento implementa DomainEvent e carrega occurredAt.

O barramento roda dentro do cc_server. Todo publicador e todo assinante ativo ficam lá. Nenhum cliente vê este barramento. O que um cliente vê é um subconjunto curado que o servidor reemite como frames JSON-RPC notifications/*.

Sem eventos, toda reação entre features é um import. O caminho das notificações teria de importar a feature de agentes, a feature de pull requests, a de tickets e a de reuniões só para saber quando exibir um toast — e cada uma dessas teria então de saber que as notificações existem. A parte de tickets importaria os pipelines para que um ticket concluído pudesse avançar uma execução. O grafo de dependências fecha um nó, e o kernel compartilhado deixa de ser compartilhado.

Publicar um evento rompe isso. O caminho das notificações assina vinte e uma classes de evento e não importa nenhuma das features que os emitem. O dispatcher de gatilhos de pipeline assina o barramento inteiro e não sabe nada sobre pull requests, tickets ou reuniões. A observabilidade agrega resultados de execução sem alcançar o despacho.

O custo é o de sempre: um publicador não consegue saber se alguém está escutando, e um ouvinte que nunca é construído fica silenciosamente inerte. O TicketRemoteSyncHandler é o exemplo permanente: ele é declarado, coberto por testes e nunca é conectado em produção.

Vale ser preciso sobre uma coisa que o barramento não possui.

O registro de auditoria não é uma projeção do fluxo de eventos. Toda operação RPC que modifica algo é auditada por padrão — RepoOp.audited tem como padrão true, e uma operação só abre mão disso ao declarar audited: false, um ato deliberado e visível na própria operação. O dispatcher acrescenta a linha “quem fez o quê, de onde” logo depois de uma chamada bem-sucedida. Não existe DomainEventAuditBridge. O ActivityLogPersister grava linhas em activity_log a partir de eventos ActivityLogged; esse é um feed separado da auditoria RPC.

O barramento de eventos é o transporte das notificações e da automação. Ele não é o sistema de registro contábil.

  • Notificações. Vinte e uma classes de evento são transformadas em frames notifications/* pelo servidor, gravadas em um feed durável por espaço de trabalho e transmitidas às sessões conectadas. Um frame cujo workspace_id nomeia um espaço de trabalho do qual o usuário da sessão não é membro é descartado no servidor; o cliente então filtra o restante para o espaço de trabalho que está exibindo.
  • Automação orientada a eventos. Os gatilhos de pipeline assinam o barramento e iniciam automaticamente os pipelines correspondentes (veja abaixo).
  • Reações de ciclo de vida. Criar um espaço de trabalho semeia seus agentes CEO e especialistas, os modelos de pipeline integrados e as suítes de avaliação iniciais. Uma execução de agente concluída retoma uma etapa de pipeline suspensa, fecha uma tarefa, alimenta o supervisor de metas, pode acordar um verificador nomeado e pode disparar novamente um líder de equipe. Uma PR mesclada ou um espaço excluído dispara a coleta de lixo do worktree.
  • Sincronização entre fornecedores. As mudanças locais de tickets são enviadas aos fornecedores configurados por um coordenador que escuta cinco eventos de ticket.

Os eventos cobrem toda a superfície do produto. Em linhas gerais:

  • Espaços de trabalho, agentes e repos — criação de espaço de trabalho, conclusão de execução, registro de repo, atualizações de skill
  • Pull requests — publicação, mudanças de status, merges, pedidos de revisão, menções, acompanhamento de PRs de autoria própria, PRs detectadas externamente
  • Mensagens — mensagens, espaços e o progresso de provisionamento de espaços
  • Tickets — criação, atribuição, reatribuição, detalhes, status e os três desfechos terminais
  • Tarefas — frames de ciclo de vida sequenciados para uma execução despachada, para que os clientes possam ordená-los e desduplicá-los
  • Pipelines — apenas eventos terminais de execução (concluída, cancelada, com falha)
  • Observabilidade — entradas do log de atividades e limites de orçamento
  • Identidade e associação — membro adicionado, removido, papel alterado
  • Calendário e reuniões — expiração de autenticação, início de reuniões e conclusão de gravações
  • Rigs — tomada de controle, reap e fechamento inesperado

Memória, orquestração, documentos de plano, artefatos, aprovações, ingestão de sincronização de tickets, atualização de calendário, merges de worktree, criação de usuários, convites e revogação de dispositivos não são eventos de domínio; consulte a referência.

Para o catálogo completo — todas as 53 classes de evento concretas, seus payloads e os 24 assinantes que as consomem — veja a Referência de eventos de domínio. Aquela página é o inventário; esta é o modelo.

O controle de acesso ao vivo usa dois mecanismos diferentes

Seção intitulada “O controle de acesso ao vivo usa dois mecanismos diferentes”

Os eventos de associação e de dispositivo parecem simétricos e não são, o que importa se você estiver raciocinando sobre a rapidez com que uma revogação faz efeito.

Revogar um dispositivo de fato encerra as sessões dele. O servidor observa a tabela de dispositivos emparelhados e derruba qualquer sessão ativa cujo dispositivo saiu do conjunto ativo, em questão de segundos.

Remover um membro não fecha o socket. O WorkspaceMemberRemoved é consumido: o LocalRpcServer e o RemoteRelayHost derrubam as assinaturas de espaço de trabalho anexadas àquele usuário, e o RemoteEventForwarder invalida seu cache de associação para que novos frames daquele espaço de trabalho não sejam enviados. O que nega a próxima RPC ainda é o portão de papel, que resolve a associação novamente a cada chamada — então a próxima solicitação deles é recusada com “Not a member of this workspace”. O efeito é imediato para qualquer coisa que eles tentem fazer; só não é obtido desligando a conexão deles.

Os gatilhos de pipeline assinam eventos de domínio e iniciam automaticamente os pipelines correspondentes. O PipelineTriggerDispatcher escuta o barramento inteiro e então:

  1. Faz um curto-circuito, a menos que o tipo de evento seja um que ele saiba mapear para um payload
  2. Localiza os gatilhos habilitados que correspondem a esse tipo de evento, em todos os espaços de trabalho
  3. Filtra cada um para o próprio espaço de trabalho e aplica seu filtro de correspondência de payload
  4. Inicia uma execução para cada gatilho que sobrevive

A restrição importante é a etapa 1. O barramento carrega 53 tipos de evento; o seletor de adicionar gatilho inicia uma execução para estes catorze (EventPayloadMapper ramos de payload): ExternalPrDetected, PullRequestPublished, PullRequestStatusChanged, PrMerged, MessageReceived, TicketAssigned, TicketCompleted, TicketFailed, TicketCancelled, BudgetThresholdCrossed, RepoAdded, MeetingRecordingStopped, SkillUpdated e SpaceDeleted. Nada mais pode iniciar um pipeline.

O RemoteEventForwarder traduz os eventos da classe de notificação em frames e os envia por RPC às sessões conectadas, onde um mapeador de frames os transforma em notificações dentro do app. Um cliente fino, portanto, vê uma projeção do fluxo de eventos do servidor sem ser dono de nenhuma execução — que é justamente o ponto da divisão de cliente fino.

Nem todo frame enviado é uma notificação, porém. O fluxo de ciclo de vida de tarefas e a reatribuição de tickets são encaminhados ao vivo para movimentar a interface, mas não são gravados no feed de notificações nem exibem toast.