|
1 | | -import React from "react"; |
| 1 | +import React, { useEffect, useRef } from "react"; |
| 2 | +import * as Sentry from "@sentry/react"; |
2 | 3 | import { |
3 | 4 | useGame, |
4 | 5 | useStage, |
5 | 6 | usePlayer, |
6 | 7 | useRound, |
7 | | - useStageTimer, |
8 | 8 | } from "@empirica/core/player/classic/react"; |
9 | 9 | import { Loading } from "@empirica/core/player/react"; |
10 | 10 | import { Profile } from "./Profile"; |
11 | 11 | import { Stage } from "./Stage"; |
12 | 12 | import { Lobby } from "./intro-exit/Lobby"; |
13 | 13 | import { ConfirmLeave } from "./components/ConfirmLeave"; |
14 | 14 |
|
| 15 | +const STALE_STATE_TIMEOUT = 5000; // 5 seconds |
| 16 | +const RELOAD_SESSION_KEY = "gameStaleStateReload"; |
| 17 | + |
15 | 18 | export function Game() { |
16 | 19 | const game = useGame(); |
17 | 20 | const stage = useStage(); |
18 | 21 | const player = usePlayer(); |
19 | 22 | const round = useRound(); |
| 23 | + const staleTimerRef = useRef(null); |
| 24 | + |
| 25 | + const assigned = player?.get("assigned"); |
| 26 | + const gameReady = !!(game && stage && round); |
| 27 | + |
| 28 | + // Detect stale state: player is assigned but game hooks haven't populated. |
| 29 | + // This can happen if the websocket misses a stage/round update from the server. |
| 30 | + // Report to Sentry and reload once to recover. |
| 31 | + useEffect(() => { |
| 32 | + if (!assigned) return undefined; |
| 33 | + |
| 34 | + if (gameReady) { |
| 35 | + // State arrived — clear any pending timer and reset the reload flag |
| 36 | + if (staleTimerRef.current) { |
| 37 | + clearTimeout(staleTimerRef.current); |
| 38 | + staleTimerRef.current = null; |
| 39 | + } |
| 40 | + sessionStorage.removeItem(RELOAD_SESSION_KEY); |
| 41 | + return undefined; |
| 42 | + } |
| 43 | + |
| 44 | + // Game state is missing — start a timer if one isn't already running |
| 45 | + if (!staleTimerRef.current) { |
| 46 | + staleTimerRef.current = setTimeout(() => { |
| 47 | + const alreadyReloaded = sessionStorage.getItem(RELOAD_SESSION_KEY); |
| 48 | + |
| 49 | + Sentry.captureMessage("Game state stale: stage/round not received", { |
| 50 | + level: "error", |
| 51 | + extra: { |
| 52 | + hasGame: !!game, |
| 53 | + hasStage: !!stage, |
| 54 | + hasRound: !!round, |
| 55 | + playerId: player?.id, |
| 56 | + gameId: game?.id, |
| 57 | + alreadyReloaded: !!alreadyReloaded, |
| 58 | + }, |
| 59 | + }); |
| 60 | + |
| 61 | + if (!alreadyReloaded) { |
| 62 | + sessionStorage.setItem(RELOAD_SESSION_KEY, Date.now().toString()); |
| 63 | + window.location.reload(); |
| 64 | + } |
| 65 | + // If we already reloaded once and it didn't help, stay on Loading |
| 66 | + // rather than looping. The Sentry report will alert us. |
| 67 | + }, STALE_STATE_TIMEOUT); |
| 68 | + } |
| 69 | + |
| 70 | + return () => { |
| 71 | + if (staleTimerRef.current) { |
| 72 | + clearTimeout(staleTimerRef.current); |
| 73 | + staleTimerRef.current = null; |
| 74 | + } |
| 75 | + }; |
| 76 | + }, [assigned, gameReady, game, stage, round, player]); |
20 | 77 |
|
21 | 78 | // if the player is not ready, we show a loading screen |
22 | 79 | if (!player) return <Loading />; |
23 | 80 |
|
24 | 81 | // game gets rendered after the player completes the intro steps, even if they haven't been |
25 | 82 | // assigned to a game. In that case, we show the lobby. |
26 | | - if (!player.get("assigned")) return <Lobby />; |
| 83 | + if (!assigned) return <Lobby />; |
27 | 84 |
|
28 | 85 | // with the unmanagedGame flag set on EmpiricaContext, we need |
29 | 86 | // to manually check that the game and stage are ready before rendering |
30 | | - if (!game || !stage || !round || !useStageTimer) return <Loading />; |
| 87 | + if (!gameReady) return <Loading />; |
31 | 88 |
|
32 | 89 | return ( |
33 | 90 | <> |
|
0 commit comments