fix(multi): prevent host pruning and increase timeout to 90s

- Exempt host from inactivity pruning to prevent room corruption
- Increase player timeout from 45s to 90s for slower connections
- Reduce heartbeat interval from 20s to 15s for better coverage
- Refresh all players' lastSeen on nextRound/resetGame to avoid immediate pruning
This commit is contained in:
jessy-david-dev
2026-04-17 15:35:13 +02:00
parent 2e2d22fc39
commit 08f67ee4f5
4 changed files with 20 additions and 15 deletions
+7 -3
View File
@@ -47,12 +47,12 @@ function generatePlayerId(): string {
return crypto.randomUUID(); return crypto.randomUUID();
} }
// Timeout joueur inactif : 45s (heartbeat toutes les 2s, on laisse large pour les tabs en arrière-plan) // Timeout joueur inactif : 90s (heartbeat toutes les 15s, x6 de marge pour les connexions lentes)
const PLAYER_TIMEOUT_MS = 45_000; const PLAYER_TIMEOUT_MS = 90_000;
function prunePlayers(players: Player[]): Player[] { function prunePlayers(players: Player[]): Player[] {
const now = Date.now(); const now = Date.now();
return players.filter((p) => now - p.lastSeen < PLAYER_TIMEOUT_MS); return players.filter((p) => p.isHost || now - p.lastSeen < PLAYER_TIMEOUT_MS);
} }
// Calcule les points d'un joueur selon ses clics et son temps // Calcule les points d'un joueur selon ses clics et son temps
@@ -455,11 +455,13 @@ export async function PATCH(
room.roundWinner = null; room.roundWinner = null;
room.countdownStart = null; room.countdownStart = null;
room.roundStart = null; room.roundStart = null;
const now = Date.now();
for (const p of room.players) { for (const p of room.players) {
p.hasWon = false; p.hasWon = false;
p.hasSurrendered = false; p.hasSurrendered = false;
p.currentArticle = ""; p.currentArticle = "";
p.wonAt = null; p.wonAt = null;
p.lastSeen = now; // rafraîchit tous les joueurs pour éviter le pruning immédiat
} }
await saveRoom(room); await saveRoom(room);
return Response.json({ room }); return Response.json({ room });
@@ -479,12 +481,14 @@ export async function PATCH(
room.roundWinner = null; room.roundWinner = null;
room.countdownStart = null; room.countdownStart = null;
room.roundStart = null; room.roundStart = null;
const nowReset = Date.now();
for (const p of room.players) { for (const p of room.players) {
p.score = 0; p.score = 0;
p.hasWon = false; p.hasWon = false;
p.hasSurrendered = false; p.hasSurrendered = false;
p.currentArticle = ""; p.currentArticle = "";
p.wonAt = null; p.wonAt = null;
p.lastSeen = nowReset; // rafraîchit tous les joueurs pour éviter le pruning immédiat
} }
await saveRoom(room); await saveRoom(room);
return Response.json({ room }); return Response.json({ room });
+2 -1
View File
@@ -61,7 +61,8 @@ export function useGameEffects({
multi.restore(saved.multiRoomCode, saved.multiPlayerId).then((ok) => { multi.restore(saved.multiRoomCode, saved.multiPlayerId).then((ok) => {
// si une nouvelle room a été rejointe pendant le restore, on laisse // si une nouvelle room a été rejointe pendant le restore, on laisse
const current = loadSession(); const current = loadSession();
if (current?.multiRoomCode && current.multiRoomCode !== savedCode) return; if (current?.multiRoomCode && current.multiRoomCode !== savedCode)
return;
if (ok) setScreen("lobby"); if (ok) setScreen("lobby");
else { else {
clearSession(); clearSession();
+1 -1
View File
@@ -116,7 +116,7 @@ export function useMultiGame() {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "heartbeat", playerId: ssePidRef.current }), body: JSON.stringify({ action: "heartbeat", playerId: ssePidRef.current }),
}).catch(() => {}); }).catch(() => {});
}, 20_000); }, 15_000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);