You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Confiabilidade/Leaderboard: persistMatch descarta os erros de insert — corrida terminada some do ranking em silêncio e deixa matches órfã (rooms/[code]/route.ts:271-282) #112
A #56 consertou o error descartado das actions de sala — inclusive o flip do finish, que
hoje devolve 500 honesto quando o update falha (src/app/api/rooms/[code]/route.ts:175-181).
Mas a persistência que roda logo depois desse flip (:186) ficou no padrão antigo, e é
justamente ela que decide se a corrida existe para o mundo:
src/app/api/rooms/[code]/route.ts:271-282
// Persist a finished match + scores for the leaderboard (best-effort).asyncfunctionpersistMatch(sb: SupabaseClient,room: RoomRow,results: ResultRow[]){try{constmatchRow=buildMatchRow(room,results,newDate().toISOString());if(!matchRow)return;const{data: match}=awaitsb.from("matches").insert(matchRow).select("id").single();// :276if(!match)return;awaitsb.from("scores").insert(buildScoreRows(match.id,room,results));// :278}catch(e){console.warn("[persistMatch]",(easError).message);}}
Dois furos, ambos mudos:
:276 descarta error. O supabase-jsnão lança em erro de query — devolve { data: null, error }. Então uma falha (constraint NOT VALID da migration 0004, RLS,
rede, timeout) faz match virar null, cair no return de :277 e não passar nem
pelo catch. Zero linhas de log. A partida simplesmente não aconteceu.
:278 descarta o retorno inteiro — nem data, nem error. Se o insert de scores
falhar, a linha de matchesjá foi gravada: sobra uma partida órfã que aparece em
"Partidas recentes" (getRecentMatches, src/lib/supabase.ts) com player_count e
vencedor, enquanto nenhum dos jogadores entra no ranking — porque o leaderboard é
uma view sobre scores (0001_coderacer_init.sql:40-52). O placar mente e ninguém sabe.
Em qualquer dos dois casos a rota responde ok: true (:188) e o cliente mostra a corrida
encerrada com sucesso.
Escopo honesto — o que este pedido NÃO é: o finishnão deve passar a falhar por
causa da persistência. Quando persistMatch roda, a linha da sala já flipou e o broadcast já
saiu (:185-186); devolver 500 aqui faria o cliente acreditar que a corrida não terminou —
regressão pior que o bug. O pedido é observabilidade + integridade do par matches/scores,
não transação distribuída.
Acceptance criteria
O error do insert de matches (:276) é lido e registrado com console.error
(não um warn que nunca dispara), identificando a sala. Falhou → não tenta gravar scores.
O error do insert de scores (:278) é lido e registrado da mesma forma.
A matches não fica órfã. Se o insert de scores falhar, o par não pode sobreviver
pela metade — o Resolvedor escolhe entre (a) deletar, na mesma requisição, a linha de matches que essa própria chamada acabou de criar (pelo id retornado em :276), ou
(b) gravar o par de forma que a ausência de scores seja impossível. Justifique a
escolha no PR. Nota de escopo: a variante (a) é lógica de aplicação sobre uma linha
recém-criada no mesmo request — não é migração destrutiva, então não cai no §7.1; o
quórum do §7.2 (abaixo) é o crivo correto.
A resposta do finish continua { ok: true } mesmo com falha de persistência. Nenhuma
mudança de comportamento visível no cliente.
O arquivo já tem o padrão pronto a copiar: o finish em :175-181 mostra exatamente como
ler o error, logar com contexto e decidir. A diferença é o que se faz depois — aqui a
decisão é logar e seguir, nunca propagar 500.
Atenção a um detalhe do .single() em :276: ele devolve erro também quando a query não
retorna exatamente uma linha, então o log precisa distinguir "falhou a escrita" de "escreveu
e não voltou linha" para não virar ruído.
Classificação: §7.2 — área de quórum (toca src/app/api/rooms/**). PR non-draft com a
linha exata Solicito quórum (HANDBOOK §7).
Contexto
A #56 consertou o
errordescartado das actions de sala — inclusive o flip dofinish, quehoje devolve 500 honesto quando o
updatefalha (src/app/api/rooms/[code]/route.ts:175-181).Mas a persistência que roda logo depois desse flip (
:186) ficou no padrão antigo, e éjustamente ela que decide se a corrida existe para o mundo:
src/app/api/rooms/[code]/route.ts:271-282Dois furos, ambos mudos:
:276descartaerror. Osupabase-jsnão lança em erro de query — devolve{ data: null, error }. Então uma falha (constraintNOT VALIDda migration 0004, RLS,rede, timeout) faz
matchvirarnull, cair noreturnde:277e não passar nempelo
catch. Zero linhas de log. A partida simplesmente não aconteceu.:278descarta o retorno inteiro — nemdata, nemerror. Se o insert descoresfalhar, a linha de
matchesjá foi gravada: sobra uma partida órfã que aparece em"Partidas recentes" (
getRecentMatches,src/lib/supabase.ts) complayer_countevencedor, enquanto nenhum dos jogadores entra no ranking — porque o
leaderboardéuma view sobre
scores(0001_coderacer_init.sql:40-52). O placar mente e ninguém sabe.Em qualquer dos dois casos a rota responde
ok: true(:188) e o cliente mostra a corridaencerrada com sucesso.
Escopo honesto — o que este pedido NÃO é: o
finishnão deve passar a falhar porcausa da persistência. Quando
persistMatchroda, a linha da sala já flipou e o broadcast jásaiu (
:185-186); devolver 500 aqui faria o cliente acreditar que a corrida não terminou —regressão pior que o bug. O pedido é observabilidade + integridade do par matches/scores,
não transação distribuída.
Acceptance criteria
errordo insert dematches(:276) é lido e registrado comconsole.error(não um
warnque nunca dispara), identificando a sala. Falhou → não tenta gravarscores.errordo insert descores(:278) é lido e registrado da mesma forma.matchesnão fica órfã. Se o insert descoresfalhar, o par não pode sobreviverpela metade — o Resolvedor escolhe entre (a) deletar, na mesma requisição, a linha de
matchesque essa própria chamada acabou de criar (peloidretornado em:276), ou(b) gravar o par de forma que a ausência de
scoresseja impossível. Justifique aescolha no PR. Nota de escopo: a variante (a) é lógica de aplicação sobre uma linha
recém-criada no mesmo request — não é migração destrutiva, então não cai no §7.1; o
quórum do §7.2 (abaixo) é o crivo correto.
finishcontinua{ ok: true }mesmo com falha de persistência. Nenhumamudança de comportamento visível no cliente.
buildMatchRow/buildScoreRowspuros e testados — o que falta é o wrapper. Cubra "matches falha" e "scores falha" em
viteste/ouscripts/validate-persistence.mjs, com umsbdublê que devolve{ error }.pnpm typecheck && pnpm build,vitest,validate-metrics,validate-persistence.Dica de abordagem
O arquivo já tem o padrão pronto a copiar: o
finishem:175-181mostra exatamente comoler o
error, logar com contexto e decidir. A diferença é o que se faz depois — aqui adecisão é logar e seguir, nunca propagar 500.
Atenção a um detalhe do
.single()em:276: ele devolve erro também quando a query nãoretorna exatamente uma linha, então o log precisa distinguir "falhou a escrita" de "escreveu
e não voltou linha" para não virar ruído.
Classificação: §7.2 — área de quórum (toca
src/app/api/rooms/**). PR non-draft com alinha exata
Solicito quórum (HANDBOOK §7).