diff --git a/frontend/src/hooks/useTimeSkew.ts b/frontend/src/hooks/useTimeSkew.ts index 03d816a..cb12355 100644 --- a/frontend/src/hooks/useTimeSkew.ts +++ b/frontend/src/hooks/useTimeSkew.ts @@ -11,16 +11,14 @@ import { API_ENDPOINT } from "../api/endpoint"; * Positive offset => client is AHEAD by that many ms. */ const TIME_URL_ENDPOINT = API_ENDPOINT.v1.TIME; - export function useTimeSkew(opts?: { - endpoint?: string; // e.g., "/api/time" or `${import.meta.env.BASE_URL}api/time` - intervalMs?: number; // how often to recheck; default 5 min - samples?: number; // take N measurements and median; default 1 - enabled?: boolean; // allow turning off; default true + intervalMs?: number; + samples?: number; + enabled?: boolean; }) { const { intervalMs = 5 * 60_000, - samples = 1, + samples = 3, // ✅ small median sampling enabled = true, } = opts || {}; @@ -31,15 +29,24 @@ export function useTimeSkew(opts?: { const measureOnce = async () => { const t0 = Date.now(); - const res = await fetch(TIME_URL_ENDPOINT, { cache: "no-store" }); + const res = await fetch(TIME_URL_ENDPOINT, { + cache: "no-store", + headers: { Accept: "application/json" }, + }); const t1 = Date.now(); if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.json(); - if (typeof data?.now !== "number") { + // Robust parsing: number or string, seconds or ms + const data = await res.json() as any; + let serverMs = typeof data?.now === "number" ? data.now : Number(data?.now); + if (!Number.isFinite(serverMs)) { throw new Error("Bad time payload (expecting { now: number })"); } - const serverMs = data.now; + // Heuristic: if it's too small to be ms epoch, treat as seconds + if (serverMs < 1e12) { // ~Sat Sep 09 2001 ms epoch + serverMs = serverMs * 1000; // seconds -> ms + } + const rtt = t1 - t0; const offset = Math.round(((t0 + t1) / 2) - serverMs); return { offset, rtt }; @@ -48,20 +55,14 @@ export function useTimeSkew(opts?: { const measure = async () => { try { setError(null); - if (samples <= 1) { - const { offset, rtt } = await measureOnce(); - setSkewMs(offset); - setRttMs(rtt); - return; - } - // multiple samples → median offset, min RTT (more stable) const offsets: number[] = []; const rtts: number[] = []; - for (let i = 0; i < samples; i++) { + const N = Math.max(1, samples); + for (let i = 0; i < N; i++) { const { offset, rtt } = await measureOnce(); offsets.push(offset); rtts.push(rtt); - if (i < samples - 1) await new Promise(r => setTimeout(r, 120)); // tiny gap + if (i + 1 < N) await new Promise(r => setTimeout(r, 120)); } offsets.sort((a, b) => a - b); const median = offsets[Math.floor(offsets.length / 2)]; @@ -75,14 +76,11 @@ export function useTimeSkew(opts?: { useEffect(() => { if (!enabled) return; - measure(); // initial check + measure(); // initial if (intervalMs > 0) { timerRef.current = window.setInterval(measure, intervalMs); } - return () => { - if (timerRef.current) window.clearInterval(timerRef.current); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps + return () => { if (timerRef.current) window.clearInterval(timerRef.current); }; }, [intervalMs, samples, enabled]); return { skewMs, rttMs, error, recheck: measure };