diff --git a/hooks/use-presence.ts b/hooks/use-presence.ts index 50d83c2..3ad80b3 100644 --- a/hooks/use-presence.ts +++ b/hooks/use-presence.ts @@ -50,50 +50,65 @@ function useHeartbeat({ }) { const [value, setValue] = useState(null); const pending = useRef(false); + const queuedActive = useRef(null); const send = useEffectEvent(async (active = true) => { - if (active && (document.visibilityState !== "visible" || pending.current)) return; + if (active && document.visibilityState !== "visible") return; + if (pending.current) { + queuedActive.current = active; + return; + } pending.current = true; + let nextActive: boolean | null = active; try { - const response = await fetch(endpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - tabId: tabId(), - ...(identifyBy === "browser" ? { browserId: visitorId() } : {}), - guideId, - ...(section !== undefined ? { section } : {}), - active, - }), - cache: "no-store", - keepalive: !active, - }); - if (!response.ok) throw new Error("presence unavailable"); - if (active) setValue(await response.json() as T); + while (nextActive !== null) { + const requestActive = nextActive; + queuedActive.current = null; + const response = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + tabId: tabId(), + ...(identifyBy === "browser" ? { browserId: visitorId() } : {}), + guideId, + ...(section !== undefined ? { section } : {}), + active: requestActive, + }), + cache: "no-store", + keepalive: !requestActive, + }); + if (!response.ok) throw new Error("presence unavailable"); + if (requestActive) setValue(await response.json() as T); + nextActive = queuedActive.current; + } } catch { if (active) setValue(null); } finally { pending.current = false; + if (queuedActive.current !== null) { + const queued = queuedActive.current; + queuedActive.current = null; + void send(queued); + } } }); useEffect(() => { const immediate = window.setTimeout(() => void send(), 0); const interval = window.setInterval(() => void send(), PRESENCE_HEARTBEAT_MS); - const refresh = () => { - if (document.visibilityState === "visible") void send(); - }; + const refresh = () => void send(); + const visibilityChange = () => void send(document.visibilityState === "visible"); const leave = () => { void send(false); }; window.addEventListener("focus", refresh); window.addEventListener("pagehide", leave); - document.addEventListener("visibilitychange", refresh); + document.addEventListener("visibilitychange", visibilityChange); return () => { window.clearInterval(interval); window.clearTimeout(immediate); window.removeEventListener("focus", refresh); window.removeEventListener("pagehide", leave); - document.removeEventListener("visibilitychange", refresh); + document.removeEventListener("visibilitychange", visibilityChange); }; }, [endpoint, guideId, identifyBy, section]);