// useSaveSync.ts — Auto-save game state to backend every 30s // Serveur = autorité. Cookie-based auth — credentials sent automatically. import { useEffect, useRef, useCallback, useState } from "react"; import { useAuth } from "../context/AuthContext"; import type { GameState } from "../core/economy"; 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 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) { // Not logged in → signal immediately if (!user) setServerLoaded(true); return; } loadedRef.current = true; apiRequest("/save").then((data) => { if (data?.gameState) { onLoad(data.gameState); lastSaveRef.current = data.lastSave; console.info("[SaveSync] Loaded save from server — server is authority"); } 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 const saveToServer = useCallback(async () => { if (!user) 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 useEffect(() => { if (!user) return undefined; const interval = setInterval(() => { saveToServer(); }, SAVE_INTERVAL_MS); return () => clearInterval(interval); }, [saveToServer, user]); // Reload from server on tab focus (multi-tab/multi-browser sync) 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; console.info("[SaveSync] Reloaded from server on focus"); } } }); }; const handleBlur = () => { saveToServer(); console.info("[SaveSync] Saved on blur — other tabs will get latest"); }; window.addEventListener("focus", handleFocus); window.addEventListener("blur", handleBlur); return () => { window.removeEventListener("focus", handleFocus); window.removeEventListener("blur", handleBlur); }; }, [user, onLoad]); // Save on page unload useEffect(() => { const handleUnload = () => { if (!user) 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 }; }