fix(presence) : count visible browsers accurately
CI / Verify (push) Successful in 2m2s
CI / Build immutable images and deploy (push) Successful in 3m42s

This commit is contained in:
2026-10-12 00:57:56 +07:00 Unverified
parent fdbabecf56
commit 3fb34ffde2
+36 -21
View File
@@ -50,50 +50,65 @@ function useHeartbeat<T>({
}) { }) {
const [value, setValue] = useState<T | null>(null); const [value, setValue] = useState<T | null>(null);
const pending = useRef(false); const pending = useRef(false);
const queuedActive = useRef<boolean | null>(null);
const send = useEffectEvent(async (active = true) => { 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; pending.current = true;
let nextActive: boolean | null = active;
try { try {
const response = await fetch(endpoint, { while (nextActive !== null) {
method: "POST", const requestActive = nextActive;
headers: { "Content-Type": "application/json" }, queuedActive.current = null;
body: JSON.stringify({ const response = await fetch(endpoint, {
tabId: tabId(), method: "POST",
...(identifyBy === "browser" ? { browserId: visitorId() } : {}), headers: { "Content-Type": "application/json" },
guideId, body: JSON.stringify({
...(section !== undefined ? { section } : {}), tabId: tabId(),
active, ...(identifyBy === "browser" ? { browserId: visitorId() } : {}),
}), guideId,
cache: "no-store", ...(section !== undefined ? { section } : {}),
keepalive: !active, active: requestActive,
}); }),
if (!response.ok) throw new Error("presence unavailable"); cache: "no-store",
if (active) setValue(await response.json() as T); keepalive: !requestActive,
});
if (!response.ok) throw new Error("presence unavailable");
if (requestActive) setValue(await response.json() as T);
nextActive = queuedActive.current;
}
} catch { } catch {
if (active) setValue(null); if (active) setValue(null);
} finally { } finally {
pending.current = false; pending.current = false;
if (queuedActive.current !== null) {
const queued = queuedActive.current;
queuedActive.current = null;
void send(queued);
}
} }
}); });
useEffect(() => { useEffect(() => {
const immediate = window.setTimeout(() => void send(), 0); const immediate = window.setTimeout(() => void send(), 0);
const interval = window.setInterval(() => void send(), PRESENCE_HEARTBEAT_MS); const interval = window.setInterval(() => void send(), PRESENCE_HEARTBEAT_MS);
const refresh = () => { const refresh = () => void send();
if (document.visibilityState === "visible") void send(); const visibilityChange = () => void send(document.visibilityState === "visible");
};
const leave = () => { const leave = () => {
void send(false); void send(false);
}; };
window.addEventListener("focus", refresh); window.addEventListener("focus", refresh);
window.addEventListener("pagehide", leave); window.addEventListener("pagehide", leave);
document.addEventListener("visibilitychange", refresh); document.addEventListener("visibilitychange", visibilityChange);
return () => { return () => {
window.clearInterval(interval); window.clearInterval(interval);
window.clearTimeout(immediate); window.clearTimeout(immediate);
window.removeEventListener("focus", refresh); window.removeEventListener("focus", refresh);
window.removeEventListener("pagehide", leave); window.removeEventListener("pagehide", leave);
document.removeEventListener("visibilitychange", refresh); document.removeEventListener("visibilitychange", visibilityChange);
}; };
}, [endpoint, guideId, identifyBy, section]); }, [endpoint, guideId, identifyBy, section]);