fix: guard ALL saves behind ready — never save DEFAULT_STATE to server
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 17s

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.
This commit is contained in:
2026-03-24 14:47:23 +01:00
parent 79ac1b0659
commit 3145758747

View File

@@ -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<string | null>(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 });