Files
ClickerZ/Frontend/src/hooks/useSaveSync.ts
Tetardtek 79ac1b0659
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 19s
feat: multi-tab sync — save on blur, reload from server on focus
- 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)
2026-03-24 14:43:58 +01:00

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 };
}