Repository navigation
Description
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 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; // :334
finishPostedRef.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.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. - Líder AFK trava mais ainda: o efeito de finish só roda para
isLeader. Se o próprio líder
travou, ninguém posta ofinish, 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_MSa partir destart_at, dimensionado pelo tamanho do snippet) e/ou
por inatividade (jogador sem novoprogresshá N segundos deixa de bloquear o fim). - Quem não terminou entra nos
resultscomo abandono — semplace, sem entrar no
leaderboard —, mantendo a regra atual de que abandono não ranqueia
(useRoom.ts:293-296etoResults,useRoom.ts:317-320). - O encerramento continua idempotente: a transição
racing→finishedsegue condicional
(route.ts—.eq("status","racing")), então corridas encerradas por timeout não
duplicammatches/scoresse dois clientes dispararem juntos. - Não há regressão de anti-cheat: o timeout não vira caminho novo para
resultsforjados —
passa pelo mesmosanitizeResults. 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:330 um
segundo gatilho: um setTimeout/tick que dispara quando Date.now() - startMs > limite ou
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.
Activity
caioross commentedon Jul 23, 2026
Resolvida em #60 — #60
A decisão de encerramento virou função pura (
shouldFinishRaceemsrc/lib/room.ts) com trêscritérios: todos terminaram · quem falta está inativo há 30s e alguém já terminou · teto de
duração (
60s + 600ms/char, máx 10min). NouseRoom.ts, o líder ganhou um tick de 1s que lêrefs e não re-renderiza — o caminho imediato dirigido por
playerscontinua igual, entãonada de latência nova na área sagrada.
Gate verde: typecheck ✅ · build ✅ ·
pnpm test45 ✅ (13 novos) · validate-persistence 49 ✅ ·validate-metrics 37 ✅. PR pede quórum (§7.2) por tocar o caminho do
finish.Um ponto para o revisor: o critério 2 fala em incluir quem não terminou nos
results, maspersistMatchgrava uma linha emscorespor item deresults— incluí-lo o colocaria noleaderboard, contra a segunda metade do próprio critério. Mantive a exclusão atual; detalhes no PR.
fix(multiplayer): encerra a corrida por inatividade e por teto de dur…