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
Basta um jogador presente parar de digitar (AFK, celular bloqueado, distraiu-se) para a
corrida nunca terminar. Todo mundo que já cruzou a linha fica preso na tela de corrida,
com o cronômetro rodando, sem Results, sem pódio e sem nada persistido em matches/scores. A sala fica em status: "racing" indefinidamente no banco.
Causa (arquivo:linha, contra origin/main)
src/lib/useRoom.ts:330-336 — o líder só posta finish quando todos os presentes têm finishedAt:
// Leader finalizes the match when everyone present has finished.if(players.length===0||!players.every(p=>p.finishedAt))return;// :334finishPostedRef.current=true;postAction("finish",{results: toResults(players)});
Não existe nenhum critério alternativo de encerramento — sem tempo máximo de corrida,
sem timeout por inatividade, sem "todos os que ainda progridem já terminaram". Quem não
digita nunca ganha finishedAt (ele só é setado em broadcastProgress, useRoom.ts:373-375).
Agravantes:
Não há escape pela UI. Durante racing, Race.tsx só oferece onAbandon (Race.tsx:126)
para quem ainda não terminou. Quem já terminou não tem botão de sair nem de forçar o fim; resetToLobby só aparece na tela de Results, que nunca chega.
Líder AFK trava mais ainda: o efeito de finish só roda para isLeader. Se o próprio líder
travou, ninguém posta o finish, mesmo com todos os outros terminados.
Fechar a aba funciona (sai da presence → some de players → o gate passa), mas deixar a
aba aberta parada, não. O resultado depende de o jogador AFK fechar o navegador — o pior
contrato possível.
Acceptance criteria
Uma corrida com um jogador parado termina sozinha: existe um encerramento por tempo
(ex.: RACE_TIMEOUT_MS a partir de start_at, dimensionado pelo tamanho do snippet) e/ou
por inatividade (jogador sem novo progress há N segundos deixa de bloquear o fim).
Quem não terminou entra nos results como abandono — sem place, sem entrar no
leaderboard —, mantendo a regra atual de que abandono não ranqueia
(useRoom.ts:293-296 e toResults, useRoom.ts:317-320).
O encerramento continua idempotente: a transição racing→finished segue condicional
(route.ts — .eq("status","racing")), então corridas encerradas por timeout não
duplicam matches/scores se dois clientes dispararem juntos.
node scripts/validate-persistence.mjs verde. Se o encerramento virar função pura
(ex.: shouldFinish(players, startMs, now)), cobrir com Vitest.
Dica de abordagem
Manter a autoridade no líder (menor mudança) e adicionar ao efeito de useRoom.ts:330 um
segundo gatilho: um setTimeout/tick que dispara quando Date.now() - startMs > limiteou
quando todos os jogadores ativos terminaram (último progress de cada não-finalizado mais
velho que N s). Extrair a decisão para uma função pura em src/lib/room.ts é o que torna isso
testável sem sala real. Cuidado com a área sagrada (HANDBOOK §2): o tick de decisão é do
líder e roda em segundos, não pode virar mais um timer de 100ms competindo com a textarea.
Sintoma
Basta um jogador presente parar de digitar (AFK, celular bloqueado, distraiu-se) para a
corrida nunca terminar. Todo mundo que já cruzou a linha fica preso na tela de corrida,
com o cronômetro rodando, sem
Results, sem pódio e sem nada persistido emmatches/scores. A sala fica emstatus: "racing"indefinidamente no banco.Causa (arquivo:linha, contra
origin/main)src/lib/useRoom.ts:330-336— o líder só postafinishquando todos os presentes têmfinishedAt:Não existe nenhum critério alternativo de encerramento — sem tempo máximo de corrida,
sem timeout por inatividade, sem "todos os que ainda progridem já terminaram". Quem não
digita nunca ganha
finishedAt(ele só é setado embroadcastProgress,useRoom.ts:373-375).Agravantes:
racing,Race.tsxsó ofereceonAbandon(Race.tsx:126)para quem ainda não terminou. Quem já terminou não tem botão de sair nem de forçar o fim;
resetToLobbysó aparece na tela deResults, que nunca chega.isLeader. Se o próprio lídertravou, ninguém posta o
finish, mesmo com todos os outros terminados.players→ o gate passa), mas deixar aaba aberta parada, não. O resultado depende de o jogador AFK fechar o navegador — o pior
contrato possível.
Acceptance criteria
(ex.:
RACE_TIMEOUT_MSa partir destart_at, dimensionado pelo tamanho do snippet) e/oupor inatividade (jogador sem novo
progresshá N segundos deixa de bloquear o fim).resultscomo abandono — semplace, sem entrar noleaderboard —, mantendo a regra atual de que abandono não ranqueia
(
useRoom.ts:293-296etoResults,useRoom.ts:317-320).racing→finishedsegue condicional(
route.ts—.eq("status","racing")), então corridas encerradas por timeout nãoduplicam
matches/scoresse dois clientes dispararem juntos.resultsforjados —passa pelo mesmo
sanitizeResults. Coordenar com a Anti-cheat: finish sem coerência temporal — leaderboard forjável em 1 request e corrida alheia encerrável #34 (coerência temporal dofinish),que endurece a validação; esta issue trata da liveness (a corrida acabar), são
complementares e tocam a mesma action.
node scripts/validate-persistence.mjsverde. Se o encerramento virar função pura(ex.:
shouldFinish(players, startMs, now)), cobrir com Vitest.Dica de abordagem
Manter a autoridade no líder (menor mudança) e adicionar ao efeito de
useRoom.ts:330umsegundo gatilho: um
setTimeout/tick que dispara quandoDate.now() - startMs > limiteouquando todos os jogadores ativos terminaram (último
progressde cada não-finalizado maisvelho que N s). Extrair a decisão para uma função pura em
src/lib/room.tsé o que torna issotestável sem sala real. Cuidado com a área sagrada (HANDBOOK §2): o tick de decisão é do
líder e roda em segundos, não pode virar mais um timer de 100ms competindo com a
textarea.