mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
fix(crm-vivo): o carimbo de entrega atravessa o hook como ref, nao como valor congelado
O `useRealtimeChannel` devolvia `ultimaEntrega.current` — leitura de ref DENTRO do render. O React 19 recusa isso por lint, e a recusa estava certa: o numero sai congelado naquele render, a ref muda depois e nada redesenha. Quem recebia so via o valor novo se ALGUMA OUTRA COISA causasse render. Funcionava por acidente — o TanStack Query redesenha ao invalidar. E falharia exatamente na janela entre a entrega e esse redesenho, que e onde o refetch de seguranca dispara: ele leria carimbo velho, concluiria "o canal nao trouxe nada" e acusaria divergencia numa mudanca legitima. O mesmo comparador sempre-verdadeiro que ja tinha me pegado uma vez, chegando por outra porta. Virar useState resolveria a propagacao e traria pior: o valor entra nas dependencias do efeito e o canal RE-ASSINA a cada evento, perdendo eventos na reassinatura. A ref atravessa a fronteira do hook e quem le e o timer — que roda fora do render e enxerga `.current` sempre fresco. De quebra, o consumidor perde um ref inteiro (`entregaRef`, que so existia para copiar o valor de volta para uma ref) e a escrita de `assinaturaRef` sai do corpo do render para um efeito. typecheck 0 · lint 0 errors (148 warnings, o baseline do repo) · 424 testes unitarios verdes em 70 arquivos.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { useEffect, useId, useRef, useState, type RefObject } from "react";
|
||||
import { createClient } from "@/lib/supabase/browser";
|
||||
import type { RealtimeChannel } from "@supabase/supabase-js";
|
||||
|
||||
@@ -120,8 +120,22 @@ function esperarAuth(supabase: ReturnType<typeof createClient>): Promise<void> {
|
||||
|
||||
export function useRealtimeChannel(opts: UseRealtimeChannelOpts): {
|
||||
status: RealtimeStatus;
|
||||
/** Instante da última entrega deste canal, ou null se nunca entregou nada. */
|
||||
ultimaEntrega: number | null;
|
||||
/**
|
||||
* Instante da última entrega deste canal (`.current` é null se nunca entregou).
|
||||
*
|
||||
* ⚠️ DEVOLVE A REF, NÃO O VALOR, e isso é correção e não estilo: ler
|
||||
* `.current` aqui no render entregaria um número CONGELADO naquele render —
|
||||
* a ref muda depois e nada redesenha, então quem recebeu ficaria com carimbo
|
||||
* velho até algo mais causar um render. Funcionava por acidente (a query
|
||||
* redesenha ao invalidar), e falharia justamente na janela entre a entrega e
|
||||
* esse redesenho, que é onde o detector de perda dispara.
|
||||
*
|
||||
* Virar `useState` resolveria a propagação e criaria pior: o valor entra nas
|
||||
* dependências do efeito e o canal RE-ASSINA a cada evento, perdendo eventos
|
||||
* na reassinatura. Quem lê isto é um timer — roda fora do render e enxerga
|
||||
* `.current` sempre fresco.
|
||||
*/
|
||||
ultimaEntrega: RefObject<number | null>;
|
||||
} {
|
||||
const { name, postgresChanges, broadcast, onChange, enabled = true } = opts;
|
||||
|
||||
@@ -139,8 +153,10 @@ export function useRealtimeChannel(opts: UseRealtimeChannelOpts): {
|
||||
* que a tela mostra é indistinguível de "nada aconteceu no intervalo", e a
|
||||
* checagem só consegue REPROVAR, nunca aprovar.
|
||||
*
|
||||
* `useRef` e não `useState`: isto não redesenha nada, e virar dependência de
|
||||
* efeito faria o canal re-assinar a cada evento — perdendo eventos na janela.
|
||||
* `useRef` e não `useState` porque virar dependência de efeito faria o canal
|
||||
* re-assinar a cada evento, perdendo eventos na janela da reassinatura. A
|
||||
* ref ATRAVESSA a fronteira do hook em vez de ser lida aqui — ver o tipo de
|
||||
* retorno, onde está por que ler `.current` no render seria defeito.
|
||||
*/
|
||||
const ultimaEntrega = useRef<number | null>(null);
|
||||
|
||||
@@ -222,5 +238,5 @@ export function useRealtimeChannel(opts: UseRealtimeChannelOpts): {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [name, enabled, instanceId, postgresChanges?.event, postgresChanges?.table, postgresChanges?.filter, postgresChanges?.schema, broadcast?.event]);
|
||||
|
||||
return { status, ultimaEntrega: ultimaEntrega.current };
|
||||
return { status, ultimaEntrega };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||
|
||||
/**
|
||||
* O REFETCH DE SEGURANÇA — uma peça com três papéis.
|
||||
@@ -42,8 +42,15 @@ interface Opts<T> {
|
||||
* sensível ao que o realtime deveria ter trazido.
|
||||
*/
|
||||
assinatura: (dado: T | undefined) => string;
|
||||
/** Instante da última entrega do canal (de `useRealtimeChannel`). */
|
||||
ultimaEntrega: number | null;
|
||||
/**
|
||||
* A REF do instante da última entrega do canal (de `useRealtimeChannel`).
|
||||
*
|
||||
* Ref e não valor porque a leitura acontece dentro do timer, e o timer roda
|
||||
* fora do render: um valor capturado no render estaria congelado no instante
|
||||
* errado — exatamente na janela entre a entrega e o redesenho, que é quando
|
||||
* este detector dispara.
|
||||
*/
|
||||
ultimaEntrega: RefObject<number | null>;
|
||||
/** De quanto em quanto tempo conferir. */
|
||||
intervaloMs?: number;
|
||||
enabled?: boolean;
|
||||
@@ -71,12 +78,17 @@ export function useRefetchDeSeguranca<T>({
|
||||
ultimaDivergencia: null,
|
||||
ultimaVerificacao: null,
|
||||
});
|
||||
// Refs para o efeito não re-montar a cada render e o intervalo não reiniciar
|
||||
// Ref para o efeito não re-montar a cada render e o intervalo não reiniciar
|
||||
// — reiniciar adiaria a verificação para sempre numa tela que redesenha.
|
||||
//
|
||||
// A escrita mora num efeito, não no corpo do render: render pode ser
|
||||
// descartado ou reexecutado pelo React 19, e escrever ali é efeito colateral
|
||||
// que ninguém rastreia. Como quem lê é o timer (depois do render, sempre), a
|
||||
// ref já está atualizada quando importa.
|
||||
const assinaturaRef = useRef(assinatura);
|
||||
assinaturaRef.current = assinatura;
|
||||
const entregaRef = useRef(ultimaEntrega);
|
||||
entregaRef.current = ultimaEntrega;
|
||||
useEffect(() => {
|
||||
assinaturaRef.current = assinatura;
|
||||
}, [assinatura]);
|
||||
|
||||
const verificar = useCallback(async () => {
|
||||
const antes = assinaturaRef.current(qc.getQueryData<T>(queryKey));
|
||||
@@ -98,9 +110,9 @@ export function useRefetchDeSeguranca<T>({
|
||||
// divergência em toda mudança legítima, e um detector que grita sempre é
|
||||
// desligado na primeira semana.
|
||||
const mudou = depois !== antes;
|
||||
const entrega = ultimaEntrega.current;
|
||||
const canalTrouxe =
|
||||
entregaRef.current !== null &&
|
||||
(prev.ultimaVerificacao === null || entregaRef.current > prev.ultimaVerificacao);
|
||||
entrega !== null && (prev.ultimaVerificacao === null || entrega > prev.ultimaVerificacao);
|
||||
const perdeu = mudou && !canalTrouxe;
|
||||
return {
|
||||
divergencias: perdeu ? prev.divergencias + 1 : prev.divergencias,
|
||||
@@ -108,7 +120,9 @@ export function useRefetchDeSeguranca<T>({
|
||||
ultimaVerificacao: Date.now(),
|
||||
};
|
||||
});
|
||||
}, [qc, queryKey]);
|
||||
// `ultimaEntrega` é ref (identidade estável): entra na lista por higiene,
|
||||
// sem recriar o callback nem reiniciar o intervalo.
|
||||
}, [qc, queryKey, ultimaEntrega]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
Reference in New Issue
Block a user