Sincronização Reativa em Tempo Real e Invalidação CDC

Esta especificação técnica formaliza a arquitetura e os contratos de sincronização em tempo real (Realtime Synchronization Pipeline) no ecossistema yzex-connect-web-app sob o Change Set CS-20260913-003.


1. Visão Geral e Problema Arquitetural

No ecossistema operacional de atendimento omni-channel, incidentes críticos como desconexão de instâncias de WhatsApp demandam reação imediata do operador.

Historicamente, o frontend realizava requisições periódicas via setInterval a cada 30 segundos no composable useHomeNotifications. Esta abordagem gerava:

  1. Latência de Percepção: O operador podia demorar até 30 segundos para ser notificado de uma desconexão crítica.

  2. Desperdício de Recursos de I/O: Requisições HTTP redundantes no gateway Nitro BFF e no PostgreSQL quando nenhum incidente havia ocorrido.

  3. Descompasso Entre Abas e Dispositivos: Uma notificação dispensada em uma aba permanecia visível em outras abas até o próximo ciclo de polling.

Para mitigar tais fragilidades com garantia de integridade multi-tenant, a engenharia adotou o padrão Debounced Cache Invalidation ancorado no Supabase Realtime (PostgreSQL WAL / CDC).


2. Topologia do Pipeline de Tempo Real

A arquitetura opera em 4 estágios determinísticos:

+-------------------------------------------------------------------------+
| ESTÁGIO 1: DETECÇÃO RELACIONAL E SSOT                                   |
| (connect.whatsapp_instances UPDATE connection_status)                   |
|   │                                                                     |
|   ▼ trg_whatsapp_instance_connection_detect                             |
| connect.resource_alert_states (SSOT do Incidente)                       |
|   │                                                                     |
|   ▼ reconcile_incident_buckets                                          |
| connect.system_notifications (Read Model Agregado com bucket_key)       |
+-------------------------------------------------------------------------+
                                    │
                                    ▼ WAL (Write-Ahead Log)
+-------------------------------------------------------------------------+
| ESTÁGIO 2: ENGINE DE PUBLICAÇÃO CDC (PostgreSQL)                        |
| - Publicação: supabase_realtime                                         |
| - Tabelas: connect.system_notifications & user_notification_dismissals  |
| - Identidade: REPLICA IDENTITY FULL (garante payloads completos no WAL) |
+-------------------------------------------------------------------------+
                                    │
                                    ▼ WebSocket (Phoenix Channels / WSS)
+-------------------------------------------------------------------------+
| ESTÁGIO 3: GATEWAY DE WEBSOCKET (Supabase Realtime)                     |
| - Protocolo: phx_join / postgres_changes                                |
| - Autenticação: JWT com claims auth.uid() e políticas RLS               |
+-------------------------------------------------------------------------+
                                    │
                                    ▼ Invalidação Leve (Event Signals)
+-------------------------------------------------------------------------+
| ESTÁGIO 4: CONSUMO REATIVO NO FRONTEND (Nuxt 3 Composables)             |
|                                                                         |
| [useHomeNotifications]                                                  |
| - Escuta: system_notifications & user_notification_dismissals           |
| - Ação: Debounce 150ms -> $fetch('/api/notifications/active')           |
|                                                                         |
| [useCriticalAlertMarkers]                                               |
| - Escuta: system_notifications & user_notification_dismissals           |
| - Ação: Debounce 150ms -> $fetch('/api/notifications/markers')          |
+-------------------------------------------------------------------------+

3. Requisitos Mandatórios na Camada de Dados

Para que o motor de CDC do Supabase propague mutações de forma confiável e com respeito às regras de Row-Level Security (RLS), o banco de dados exige duas diretrizes:

3.1 Publicação Explícita de Tabelas

Por padrão, a publicação supabase_realtime monitora apenas as tabelas declaradas. A tabela connect.user_notification_dismissals deve ser compulsoriamente incluída na publicação para emitir eventos quando um usuário dispensa um aviso:

DO $$
BEGIN
  IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'supabase_realtime') THEN
    IF NOT EXISTS (
      SELECT 1 FROM pg_publication_tables
      WHERE pubname = 'supabase_realtime'
        AND schemaname = 'connect'
        AND tablename = 'user_notification_dismissals'
    ) THEN
      ALTER PUBLICATION supabase_realtime ADD TABLE connect.user_notification_dismissals;
    END IF;
  END IF;
END $$;

3.2 Identidade de Réplica Completa (REPLICA IDENTITY FULL)

O PostgreSQL tradicional grava apenas a chave primária no log WAL durante comandos UPDATE e DELETE. Como as políticas RLS de connect.system_notifications e connect.user_notification_dismissals inspecionam colunas como organization_id, location_id e user_id, a ausência de réplica completa faz com que o Realtime descarte silenciosamente os eventos.

ALTER TABLE connect.system_notifications REPLICA IDENTITY FULL;
ALTER TABLE connect.user_notification_dismissals REPLICA IDENTITY FULL;

4. Padrão Debounced Cache Invalidation

Em sistemas corporativos com isolamento multi-tenant, processar diretamente no cliente os payloads brutos do CDC gera alto risco de corrupção de estado local e brechas de visualização.

A abordagem canônica consiste em tratar o evento Realtime como um gatilho assíncrono de invalidação:

  1. Recepção do Evento: O canal WebSocket recebe a notificação de que houve INSERT, UPDATE ou DELETE na tabela.

  2. Debounce Centralizado: O temporizador de 150ms a 200ms agrupa rajadas concorrentes de eventos (ex.: desconexão simultânea de múltiplas instâncias).

  3. Refetch Autoritativo: Ao expirar o debounce, o composable realiza uma requisição HTTP aos endpoints seguros do BFF (/api/notifications/active e /api/notifications/markers).

  4. Sanitização no Backend: O backend reavalia as permissões RLS, aplica a deduplicação de rollups e entrega a árvore de dados 100% canônica e pronta para renderização.


5. Ciclo de Vida e Resiliência de Sockets

5.1 Teardown Assíncrono Idempotente (isTearingDown)

O encerramento de canais Phoenix (supabase.removeChannel) é assíncrono. Para evitar condições de corrida (race conditions) quando o operador alterna rapidamente entre organizações no cabeçalho, o composable implementa uma trava booleana de teardown que aguarda o fechamento do canal anterior antes de subscrever o novo tópico.

5.2 Detecção de Sockets Zumbis (Page Visibility API)

Navegadores móveis e abas em segundo plano frequentemente congelam o temporizador e as conexões TCP de WebSockets para economia de energia.

Ao restaurar o foco na aba (document.visibilityState === 'visible'), o composable: 1. Avalia se o canal atual está no estado SUBSCRIBED. 2. Se o estado estiver inconsistente ou desconectado, encerra o socket zumbi e reconecta automaticamente. 3. Se o socket estiver saudável mas o tempo desde a última sincronização exceder 5 segundos, dispara uma verificação leve com debounce.

5.3 Renovação Dinâmica de Sessão JWT

Conexões WebSocket de longa duração podem expirar caso o token de autenticação Supabase seja rotacionado pelo SDK. O listener supabase.auth.onAuthStateChange monitora a emissão de novos tokens e atualiza o socket em tempo de execução via supabase.realtime.setAuth(token).


6. Matriz de Resposta a Eventos em Tempo Real

Evento Operacional Disparo na Camada de Dados Comportamento Reativo no Frontend

Instância WhatsApp Desconecta

Trigger atualiza resource_alert_states e system_notifications (status = 'ACTIVE').

Realtime emite evento CDC → Feed da Home renderiza o aviso e marcadores vermelhos acendem em < 500ms.

Instância WhatsApp Reconecta

Trigger atualiza system_notifications (status = 'AUTO_RESOLVED').

Realtime emite evento CDC → O aviso desaparece do feed e os marcadores vermelhos apagam instantaneamente.

Operador Clica no "X" (Dispensar)

Inserção em connect.user_notification_dismissals.

0ms percebido: Remoção otimista local imediata. Concorrentemente, as outras abas recebem o evento CDC e removem o item.


7. Padrão de Disparo Cruzado Simbiótico (Symbiotic Cross-Dispatch)

Para eliminar assimetrias onde marcadores visuais (useCriticalAlertMarkers) reagiam ao CDC mas o feed (useHomeNotifications) permanecia desatualizado, estabeleceu-se um canal de comunicação bidirecional estritamente desacoplado:

  1. Canal Global como Invalidador Concorrente: O canal public:critical-alert-markers opera sem filtros restritivos no schema connect. Ao capturar eventos de mutação em system_notifications ou user_notification_dismissals, ele agenda a execução de fetchMarkers() e imediatamente invoca useHomeNotifications().refresh().

  2. Isolamento de Erros via Try-Catch Defensivo: A chamada cruzada é encapsulada em bloco defensivo para garantir que a ausência do contexto de notificações (ex.: páginas sem o feed ativo) não interrompa a sinalização dos marcadores críticos.

  3. Debounce Compartilhado em 150ms: Ambos os composables utilizam janelas de debounce de 150ms, coalescendo múltiplas mutações em um único par de requisições HTTP seguras aos endpoints BFF.


8. Prevenção de Condições de Corrida e Inicialização Assíncrona

8.1 Resolução Tardia do Contexto de Organização

Na inicialização do Nuxt 3, o contexto do operador (useAppContext) é resolvido assincronamente através de cookies e chamadas REST (/api/organizations).

Para prevenir que a montagem de HomeNotifications.vue falhe ao subscrever o canal quando selectedOrgId ainda estiver nulo:

  1. Flag de Intenção Local (localSubscribed): Cada instância de chamada retém o estado localSubscribed = true e preserva subscriberCount > 0 mesmo que a conexão física do WebSocket precise aguardar o identificador do inquilino.

  2. Guarda de Reatividade Aprimorada: O watcher em useHomeNotifications monitora [selectedOrgId, currentScope, selectedUnitId]. Ao detectar a transição de selectedOrgId para um UUID válido sob subscriberCount > 0, ele inicia a assinatura e dispara a busca inicial sem intervenção manual:

    watch(
      [selectedOrgId, currentScope, selectedUnitId],
      async ([newOrgId, newScope, newUnitId], [oldOrgId, oldScope, oldUnitId]) => {
        if (newOrgId && (currentChannel || subscriberCount > 0)) {
          if (!currentChannel || activeSubscribedOrgId !== newOrgId) {
            await subscribeRealtime()
          } else if (newScope !== oldScope || newUnitId !== oldUnitId) {
            await fetchNotifications()
          }
        } else if (!newOrgId && currentChannel) {
          await unsubscribeRealtime(true)
        }
      }
    )
  3. Reatividade a Trocas de Unidade e Escopo: Quando o operador alterna entre filiais no cabeçalho sem trocar a organização, o canal Realtime permanece conectado e o composable reexecuta exclusivamente fetchNotifications(), atualizando o feed para a nova filial em tempo de tela.


9. Validação Empírica em Tempo de Execução

A estabilidade da arquitetura foi comprovada em ambiente real:

  • Desconexão de Instância WhatsApp: Propagação relacional via trigger → emissão WAL CDC → atualização de marcadores e exibição do aviso em HomeNotifications em < 300ms.

  • Reconexão de Instância: Limpeza sincronizada do card e desativação dos marcadores visuais no mesmo intervalo.

  • Cobertura de Testes: Suíte completa com 48 arquivos de teste e 297 testes automatizados executando com exit code 0 no Vitest, acompanhada de 0 erros na checagem estática vue-tsc --noEmit.