feat(multiplayer): desistência automática por inatividade na corrida (#65) - #81
Conversation
…65) Um jogador que abandona o teclado prendia a sala: a corrida esperava todos terminarem, então um único AFK segurava todo mundo por minutos. Agora, sem digitar por 30s, abre um aviso com contagem regressiva de 10s; qualquer tecla cancela e, ao zerar, o jogador desiste sozinho (`abandon`) e a sala segue. - Lógica em `TypingCore` (compartilhado com o Practice), ligada só no multiplayer via `idleForfeit` — o `Race` passa a prop; o Practice não (treino solo não tem ninguém esperando). - Detecção em refs + um único tick de 500ms: enquanto se digita não há setState novo por tecla (só um write de ref), preservando a latência de input (área sagrada §2). Qualquer tecla reancora o relógio de ociosidade e fecha o aviso. - Novo `IdleWarning` (aviso âmbar com contagem, `role="alert"`/`aria-live`, `prefers-reduced-motion` respeitado). Reusa o `abandon()` já existente — nenhum caminho novo de persistência/leaderboard.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
O tick de inatividade listava onAbandon nas deps. Como actions.abandon depende de progress no useRoom, sua identidade trocava a cada heartbeat (~1s), re-rodando o efeito e reancorando lastActivityRef: o aviso de 30s nunca abria (feature nascia morta) e o setInterval era recriado a cada tecla, competindo com a textarea. Uma ref estavel (onAbandonRef) desacopla o tick desse churn; deps do efeito passam a [idleForfeit, iFinished, startedAt]. Achado pelo quorum adversarial do PR Doctor.
🏛️ Quórum §7.2 — reparada e APROVADALente Performance/área-sagrada vetou (com razão): Reparo (commit Gate verde no worktree (typecheck ✓, test 34 ✓, build ✓) e CI + Vercel verdes no head reparado. |
Contexto
Um jogador que abandona o teclado prendia a sala: a corrida esperava todos terminarem,
então um único AFK segurava todo mundo por minutos (issue #65). Agora, sem digitar por
30s, abre um aviso com contagem regressiva de 10s; qualquer tecla cancela e, ao zerar,
o jogador desiste sozinho (
abandon) e a sala segue.O que mudou
Tudo em
src/components/TypingCore.tsx(+ 1 linha emRace.tsx):idleForfeit: oRace(multiplayer) liga; o Practice não(
PracticeViewnão passa a prop) — treino solo não tem ninguém esperando, então nada muda lá.write de ref (
markActivity) — nenhum setState novo por keystroke, então a latência deinput não muda (área sagrada, HANDBOOK §2). O
setStatesó ocorre quando o aviso está aberto(jogador parado, por definição).
IdleWarning: aviso âmbar com contagem,role="alert"+aria-live="assertive",prefers-reduced-motionrespeitado (sem animação de entrada/saída quando reduzido).abandon()já existente — nenhum caminho novo de persistência/leaderboard. Umjogador que desiste já fica marcado
abandonede fora do ranking; e como ganhafinishedAt,destrava o gate de fim que espera todos terminarem.
Como foi validado (resultado real)
Verificação da lógica em browser (rastreio instrumentado, depois removido): rodei o núcleo
com constantes reduzidas (2s/6s) e um trace por tick. O log confirmou o ciclo inteiro:
E que digitar cancela: após uma tecla, o
idlevolta a ~0.3s e o tick retorna aidle-ok(sem aviso). As constantes foram restauradas para 30s/10s e toda a instrumentação removida
antes do commit (diff final = só a feature).
Limitação: o overlay usa framer-motion; no browser headless desta rodada o rAF fica
suspenso (a Browser pane não compõe frames), então a opacidade do aviso ficou congelada em 0 —
artefato de renderização, não da lógica (que o trace acima confirma). Em browser visível a
animação toca normalmente.
Riscos
desejado da issue; 10s + qualquer tecla cancelam.
setStatenovo — só um writede ref por tecla.
api/rooms/**,supabase.ts, migrations, CI, nemadiciona dependência; reusa
abandon().Closes #65