From 3145758747e617015f2576f532e449542cac6eef Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Tue, 24 Mar 2026 14:47:23 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20guard=20ALL=20saves=20behind=20ready=20?= =?UTF-8?q?=E2=80=94=20never=20save=20DEFAULT=5FSTATE=20to=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: on refresh, store starts with DEFAULT_STATE (resources: 0). Blur/interval/unload handlers saved this zero state before server load. Fix: every save path checks useGameStore.getState().ready before writing. --- Frontend/src/hooks/useSaveSync.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Frontend/src/hooks/useSaveSync.ts b/Frontend/src/hooks/useSaveSync.ts index 80fc8c2..da00b81 100644 --- a/Frontend/src/hooks/useSaveSync.ts +++ b/Frontend/src/hooks/useSaveSync.ts @@ -1,8 +1,9 @@ // useSaveSync.ts — Auto-save game state to backend every 30s -// Serveur = autorité. Cookie-based auth — credentials sent automatically. +// Serveur = autorité. NEVER save before server state is loaded (ready guard). import { useEffect, useRef, useCallback, useState } from "react"; import { useAuth } from "../context/AuthContext"; +import { useGameStore } from "../store/useGameStore"; import type { GameState } from "../core/economy"; const SAVE_INTERVAL_MS = 30_000; // 30 seconds @@ -35,6 +36,7 @@ async function apiRequest(path: string, options: RequestInit = {}) { export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncOptions) { const { user } = useAuth(); + const ready = useGameStore((s) => s.ready); const lastSaveRef = useRef(null); const loadedRef = useRef(false); const [serverLoaded, setServerLoaded] = useState(false); @@ -42,7 +44,6 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO // Load save from server on mount (once, only if logged in) useEffect(() => { if (loadedRef.current || !user) { - // Not logged in → signal immediately if (!user) setServerLoaded(true); return; } @@ -63,9 +64,9 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO }); }, [onLoad, user]); - // Save function + // Save function — GUARDED by ready (never save DEFAULT_STATE) const saveToServer = useCallback(async () => { - if (!user) return; + if (!user || !useGameStore.getState().ready) return; const gameState = getGameState(); const result = await apiRequest("/save", { @@ -78,25 +79,24 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO } }, [getGameState, playTimeSeconds, user]); - // Auto-save interval + // Auto-save interval — only when ready useEffect(() => { - if (!user) return undefined; + if (!user || !ready) return undefined; const interval = setInterval(() => { saveToServer(); }, SAVE_INTERVAL_MS); return () => clearInterval(interval); - }, [saveToServer, user]); + }, [saveToServer, user, ready]); - // Reload from server on tab focus (multi-tab/multi-browser sync) + // Multi-tab sync: save on blur, reload on focus — only when ready useEffect(() => { if (!user) return undefined; const handleFocus = () => { apiRequest("/save").then((data) => { if (data?.gameState && data.lastSave) { - // Only load if server save is newer than our last known save if (!lastSaveRef.current || new Date(data.lastSave) > new Date(lastSaveRef.current)) { onLoad(data.gameState); lastSaveRef.current = data.lastSave; @@ -107,8 +107,8 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO }; const handleBlur = () => { + if (!useGameStore.getState().ready) return; saveToServer(); - console.info("[SaveSync] Saved on blur — other tabs will get latest"); }; window.addEventListener("focus", handleFocus); @@ -117,12 +117,12 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO window.removeEventListener("focus", handleFocus); window.removeEventListener("blur", handleBlur); }; - }, [user, onLoad]); + }, [user, onLoad, saveToServer]); - // Save on page unload + // Save on page unload — GUARDED by ready useEffect(() => { const handleUnload = () => { - if (!user) return; + if (!user || !useGameStore.getState().ready) return; const gameState = getGameState(); const payload = JSON.stringify({ gameState, playTimeSeconds });