import { useEffect, useRef, useState } from "react"; /** * Measures client clock skew vs server time {now: }. * NTP-style estimate: * t0 = client ms before request * s = server ms from JSON * t1 = client ms after response * offset ≈ ((t0 + t1)/2) - s * Positive offset => client is AHEAD by that many ms. */ 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 }) { const { endpoint = "/api/time", intervalMs = 5 * 60_000, samples = 1, enabled = true, } = opts || {}; const [skewMs, setSkewMs] = useState(null); const [rttMs, setRttMs] = useState(null); const [error, setError] = useState(null); const timerRef = useRef(null); const measureOnce = async () => { const t0 = Date.now(); const res = await fetch(endpoint, { cache: "no-store" }); const t1 = Date.now(); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (typeof data?.now !== "number") { throw new Error("Bad time payload (expecting { now: number })"); } const serverMs = data.now; const rtt = t1 - t0; const offset = Math.round(((t0 + t1) / 2) - serverMs); return { offset, rtt }; }; 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 { offset, rtt } = await measureOnce(); offsets.push(offset); rtts.push(rtt); if (i < samples - 1) await new Promise(r => setTimeout(r, 120)); // tiny gap } offsets.sort((a, b) => a - b); const median = offsets[Math.floor(offsets.length / 2)]; const minRtt = Math.min(...rtts); setSkewMs(median); setRttMs(minRtt); } catch (e: any) { setError(e?.message || "Time sync failed"); } }; useEffect(() => { if (!enabled) return; measure(); // initial check 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 }, [endpoint, intervalMs, samples, enabled]); return { skewMs, rttMs, error, recheck: measure }; }