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:
-
Latência de Percepção: O operador podia demorar até 30 segundos para ser notificado de uma desconexão crítica.
-
Desperdício de Recursos de I/O: Requisições HTTP redundantes no gateway Nitro BFF e no PostgreSQL quando nenhum incidente havia ocorrido.
-
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:
-
Recepção do Evento: O canal WebSocket recebe a notificação de que houve
INSERT,UPDATEouDELETEna tabela. -
Debounce Centralizado: O temporizador de 150ms a 200ms agrupa rajadas concorrentes de eventos (ex.: desconexão simultânea de múltiplas instâncias).
-
Refetch Autoritativo: Ao expirar o debounce, o composable realiza uma requisição HTTP aos endpoints seguros do BFF (
/api/notifications/activee/api/notifications/markers). -
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 |
Realtime emite evento CDC → Feed da Home renderiza o aviso e marcadores vermelhos acendem em < 500ms. |
Instância WhatsApp Reconecta |
Trigger atualiza |
Realtime emite evento CDC → O aviso desaparece do feed e os marcadores vermelhos apagam instantaneamente. |
Operador Clica no "X" (Dispensar) |
Inserção em |
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:
-
Canal Global como Invalidador Concorrente: O canal
public:critical-alert-markersopera sem filtros restritivos no schemaconnect. Ao capturar eventos de mutação emsystem_notificationsouuser_notification_dismissals, ele agenda a execução defetchMarkers()e imediatamente invocauseHomeNotifications().refresh(). -
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.
-
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:
-
Flag de Intenção Local (
localSubscribed): Cada instância de chamada retém o estadolocalSubscribed = truee preservasubscriberCount > 0mesmo que a conexão física do WebSocket precise aguardar o identificador do inquilino. -
Guarda de Reatividade Aprimorada: O watcher em
useHomeNotificationsmonitora[selectedOrgId, currentScope, selectedUnitId]. Ao detectar a transição deselectedOrgIdpara um UUID válido sobsubscriberCount > 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) } } ) -
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
HomeNotificationsem < 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 0no Vitest, acompanhada de 0 erros na checagem estáticavue-tsc --noEmit.