Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 19s
- On blur: immediate save to server (no 30s wait) - On focus: fetch server save, load if newer than local - Handles multi-browser scenario (laptop → desktop)
145 lines
4.3 KiB
TypeScript
145 lines
4.3 KiB
TypeScript
// 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<string | null>(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 };
|
|
}
|