Skip to content

fix(leaderboard): persistMatch lê os erros de insert e não deixa matches órfã (#112) - #127

Open
caioross wants to merge 1 commit into
mainfrom
auto/issue-112-persist-errors
Open

fix(leaderboard): persistMatch lê os erros de insert e não deixa matches órfã (#112)#127
caioross wants to merge 1 commit into
mainfrom
auto/issue-112-persist-errors

Conversation

@caioross

@caioross caioross commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Contexto

persistMatch é quem decide se uma corrida terminada existe para o mundo — ela alimenta
matches (Partidas recentes) e scores (a view leaderboard). O supabase-js não lança
em erro de query: devolve { data, error }. Os dois inserts descartavam esse error, então:

  1. Falha no insert de matches fazia match virar null, caía no return e nem passava
    pelo catch
    — zero linhas de log. A partida simplesmente não aconteceu.
  2. Falha no insert de scores deixava a matches já gravada: "Partidas recentes" exibia
    a corrida com vencedor e player_count enquanto nenhum jogador entrava no ranking.

Nos dois casos o finish respondia ok: true e o pódio aparecia normalmente.

O que mudou

  • persistMatch saiu do route file para src/lib/persistMatch.ts, assinatura inalterada
    (sb, room, results). Motivo: um route module do App Router não pode exportar função
    arbitrária (o Next trata esses exports como configuração de rota), e sem import não há como
    cobrir os caminhos de erro com um sb dublê. route.ts só importa e chama.
  • Erro do insert de matches lido e logado com console.error + código do PostgREST,
    identificando a sala; falhou → não tenta scores. Ramo separado para "sem erro e sem
    linha" (.single() devolve erro quando a contagem ≠ 1 — PGRST116), que a issue pede para
    distinguir de "não escreveu".
  • Erro do insert de scores lido e logado da mesma forma.
  • AC 3 — escolhi (a), deletar a matches recém-criada. O schema decide: scores.match_id
    referencia matches(id) on delete cascade (0001_coderacer_init.sql:24), então apagar
    pelo id é um statement só, sem deixar filho órfão na direção oposta, e o insert roda com
    service_role (ignora RLS), sem precisar de policy de DELETE. A variante (b) — "tornar a
    ausência de scores impossível" — exige transação, que o PostgREST não oferece: viraria
    função Postgres + migração nova + deploy-ordering para o que (a) resolve em 4 linhas.
  • O guarda tem guarda: o delete compensatório também pode falhar. Se falhar, a órfã que a
    AC 3 proíbe existe de fato — esse caso tem console.error próprio ([persistMatch:rollback]),
    nunca um catch mudo.
  • console.warn do catch externo virou console.error (só exceção real chega lá).
  • Nada muda para o cliente (AC 4): finish continua { ok: true } em qualquer desfecho da
    persistência — a linha da sala já flipou e o broadcast já saiu quando persistMatch roda;
    devolver 500 aqui faria o jogador crer que a corrida não terminou.
  • Log não vaza nick: só código + mensagem do erro, nunca a linha (que carrega os nomes).
    Há teste garantindo isso.

Fora de escopo de propósito, como o parecer orienta: nenhum retry (o broadcast já saiu; um
retry sobre resposta perdida geraria uma segunda matches para a mesma corrida) e nenhuma
sinalização do erro ao cliente (a AC 4 proíbe; fica como issue futura de UX).

Validação (resultado real)

pnpm install --frozen-lockfile   OK
pnpm typecheck                   OK
pnpm build                       OK (todas as rotas compiladas)
pnpm test                        181 passaram / 8 arquivos  (8 novos em persistMatch.test.ts)
node scripts/validate-persistence.mjs   72 passaram, 0 falharam
node .claude/skills/cr-typing-engine/scripts/validate-metrics.mjs   45 passaram, 0 falharam

Lint = N/A (não há config ESLint no repo; a CI não roda lint).

Os 8 testes novos cobrem, com um sb dublê que devolve { data: null, error } por tabela:
matches falha → scores nunca é chamada · matches sem linha (PGRST116) → para e loga ·
scores falha → delete chamado com o id certo · delete também falha → não lança e loga a
órfã · caminho feliz → 2 inserts, 0 deletes · results vazio → 0 inserts (regressão do
buildMatchRow → null) · exceção real não escapa · log sem nick de jogador.

Riscos

  • Baixo. O caminho feliz é idêntico ao anterior (mesmos 2 inserts, mesma ordem); o delete
    extra só existe no caminho de falha, depois do broadcast, fora da área sagrada.
  • O delete apaga apenas o id que esta mesma chamada acabou de criar — nunca uma partida
    preexistente. Não é migração nem operação destrutiva sobre dados existentes (§7.1 não se
    aplica; ver nota de escopo na AC 3 da issue).
  • Se a produção estiver hoje falhando em silêncio (ex.: deploy sem SUPABASE_SERVICE_ROLE_KEY,
    em que getServerSupabase cai para a chave anon e matches/scores só têm policy de SELECT),
    este PR faz esse modo de falha aparecer no log — é o objetivo, não um efeito colateral.

Segui o plano do Parecer do Conselho de 2026-07-31 na íntegra, incluindo a extração para
src/lib/ e a escolha (a) justificada pelo on delete cascade. Confirmei contra origin/main
as três premissas que ele pediu para verificar: o on delete cascade em 0001:24, o
buildMatchRow → null com results vazio (src/lib/room.ts:382) e a inexistência de teste
importando o route file.

Solicito quórum (HANDBOOK §7)

Closes #112

…hes órfã (#112)

O supabase-js não lança em erro de query — devolve `{ data, error }` —, e a
persistência da partida descartava esse `error` nos dois inserts. Uma falha em
`matches` caía no `return` sem passar pelo `catch` (zero log), e uma falha em
`scores` deixava a `matches` já gravada: "Partidas recentes" mostrava a corrida
com vencedor enquanto nenhum jogador entrava no ranking.

- Extrai `persistMatch` do route file para `src/lib/persistMatch.ts` — um module
  de rota do App Router não pode exportar função arbitrária, e sem import não há
  como cobrir os caminhos de erro com um `sb` dublê.
- Lê o `error` dos dois inserts e loga com `console.error` + código do PostgREST,
  distinguindo "não escreveu" de "escreveu e não voltou linha" (`.single()`).
- `scores` falhou → apaga a `matches` que esta chamada acabou de criar, pelo id.
  O erro desse delete também é logado: é a órfã que a AC 3 proíbe.
- `finish` intocado: continua `{ ok: true }` em qualquer desfecho da persistência.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
code-racer Ready Ready Preview Aug 3, 2026 5:08pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant