// useSaveSync.ts — Auto-save game state to backend every 30s // 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"; import { migrateSave } from "../core/migrateSave"; const SAVE_INTERVAL_MS = 30_000; // 30 seconds const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3310"; interface SaveSyncOptions { getGameState: () => GameState; onLoad: (state: GameState) => void; playTimeSeconds: number; } async function apiRequest(path: string, options: RequestInit = {}) { const res = await fetch(`${BACKEND_URL}/api${path}`, { credentials: "include", headers: { "Content-Type": "application/json", ...options.headers, }, ...options, }); if (!res.ok) { const body = await res.json().catch(() => ({})); console.warn(`[SaveSync] ${path} failed:`, res.status, body); return null; } return res.json(); } 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); // Load save from server on mount (once, only if logged in) useEffect(() => { if (loadedRef.current || !user) { if (!user) setServerLoaded(true); return; } loadedRef.current = true; apiRequest("/save").then((data) => { if (data?.gameState) { const migrated = migrateSave(data.gameState); onLoad(migrated); lastSaveRef.current = data.lastSave; console.info("[SaveSync] Loaded save from server — server is authority (v%d)", migrated.saveVersion); } else { console.info("[SaveSync] No server save found — starting fresh"); } setServerLoaded(true); }).catch(() => { console.warn("[SaveSync] Server unreachable — falling back to local"); setServerLoaded(true); }); }, [onLoad, user]); // Save function — GUARDED by ready (never save DEFAULT_STATE) const saveToServer = useCallback(async () => { if (!user || !useGameStore.getState().ready) return; const gameState = getGameState(); const result = await apiRequest("/save", { method: "POST", body: JSON.stringify({ gameState, playTimeSeconds }), }); if (result?.lastSave) { lastSaveRef.current = result.lastSave; } }, [getGameState, playTimeSeconds, user]); // Auto-save interval — only when ready useEffect(() => { if (!user || !ready) return undefined; const interval = setInterval(() => { saveToServer(); }, SAVE_INTERVAL_MS); return () => clearInterval(interval); }, [saveToServer, user, ready]); // Multi-tab sync: save on blur, reload on focus — only when ready useEffect(() => { if (!user) return undefined; const handleFocus = () => { // Small delay to let the other tab's blur save complete setTimeout(() => apiRequest("/save").then((data) => { if (data?.gameState && data.lastSave) { if (!lastSaveRef.current || new Date(data.lastSave) > new Date(lastSaveRef.current)) { const migrated = migrateSave(data.gameState); onLoad(migrated); lastSaveRef.current = data.lastSave; console.info("[SaveSync] Reloaded from server on focus"); } } }), 500); }; const handleBlur = () => { if (!useGameStore.getState().ready) return; saveToServer(); }; window.addEventListener("focus", handleFocus); window.addEventListener("blur", handleBlur); return () => { window.removeEventListener("focus", handleFocus); window.removeEventListener("blur", handleBlur); }; }, [user, onLoad, saveToServer]); // Save on page unload — GUARDED by ready useEffect(() => { const handleUnload = () => { if (!user || !useGameStore.getState().ready) return; const gameState = getGameState(); const payload = JSON.stringify({ gameState, playTimeSeconds }); fetch(`${BACKEND_URL}/api/save`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: payload, keepalive: true, }).catch(() => {}); }; window.addEventListener("beforeunload", handleUnload); return () => window.removeEventListener("beforeunload", handleUnload); }, [getGameState, playTimeSeconds, user]); return { saveToServer, lastSave: lastSaveRef.current, serverLoaded }; }