From 1153f574a41160e16e95afd4648f0b30d3a9908f Mon Sep 17 00:00:00 2001 From: jessy-david-dev Date: Sat, 11 Apr 2026 22:12:39 +0200 Subject: [PATCH] fix(multi): prevent player kick on slow load and background tab throttling --- app/api/rooms/[code]/route.ts | 4 ++-- lib/useMultiGame.ts | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/api/rooms/[code]/route.ts b/app/api/rooms/[code]/route.ts index 462271d..1907259 100644 --- a/app/api/rooms/[code]/route.ts +++ b/app/api/rooms/[code]/route.ts @@ -44,8 +44,8 @@ function generatePlayerId(): string { return crypto.randomUUID(); } -// Timeout joueur inactif : 15s -const PLAYER_TIMEOUT_MS = 15_000; +// Timeout joueur inactif : 45s (heartbeat toutes les 2s, on laisse large pour les tabs en arrière-plan) +const PLAYER_TIMEOUT_MS = 45_000; function prunePlayers(players: Player[]): Player[] { const now = Date.now(); diff --git a/lib/useMultiGame.ts b/lib/useMultiGame.ts index d54ee57..115df23 100644 --- a/lib/useMultiGame.ts +++ b/lib/useMultiGame.ts @@ -74,11 +74,29 @@ export function useMultiGame() { } catch { /* ignore */ } } + const pollCodeRef = useRef(null); + const pollPidRef = useRef(null); + function startPolling(code: string, pid: string) { stopPolling(); + pollCodeRef.current = code; + pollPidRef.current = pid; pollRef.current = setInterval(() => poll(code, pid), POLL_INTERVAL); } + // Relance le polling quand le tab redevient visible (les setInterval sont throttlés en arrière-plan) + useEffect(() => { + function onVisible() { + if (document.visibilityState === "visible" && pollCodeRef.current && pollPidRef.current) { + poll(pollCodeRef.current, pollPidRef.current); + startPolling(pollCodeRef.current, pollPidRef.current); + } + } + document.addEventListener("visibilitychange", onVisible); + return () => document.removeEventListener("visibilitychange", onVisible); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Phase sync useEffect(() => { @@ -131,6 +149,13 @@ export function useMultiGame() { clicksRef.current += 1; setClicksDisplay(clicksRef.current); if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; } + // Heartbeat optimiste pour éviter le kick pendant le chargement de l'article + fetch(`/api/rooms/${room.code}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "heartbeat", playerId }), + }).catch(() => {}); + const canonical = await loadArticle(t); if (!canonical) return;