Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | 4x 4x 4x 4x 4x 14x 14x 14x 14x 4x 7x 7x 4x 6x 6x 6x 6x 1x 1x 1x 1x 6x 14x 14x 14x 14x 6x 6x 8x 6x 2x 6x 36x 6x 6x 6x 6x 36x 6x 6x 6x 6x | import React, { useEffect } from "react";
/**
* Idle detector for the inactivity auto-logout.
*
* The previous implementation armed a single `setTimeout(idleTime)` and relied
* on it firing on time. Browsers heavily throttle — and after a few minutes can
* freeze — timers in backgrounded/occluded tabs, so a user who switched away and
* came back was often NOT logged out on time (the timer was delayed or never
* fired). That is the "idle logout doesn't work" symptom.
*
* This version is **wall-clock based** instead:
* - every (throttled) user-activity event stores `Date.now()` in localStorage;
* - a periodic poll AND `visibilitychange`/`focus` recompute the *real* elapsed
* time from that timestamp and flip to idle once it exceeds `idleTime`.
*
* Because each check derives elapsed time from a stored timestamp rather than
* trusting the timer to fire exactly, it survives background-tab throttling: even
* if the poll is throttled to once a minute (or frozen), the moment the tab is
* refocused `visibilitychange` runs the check immediately. localStorage also
* makes activity/idle consistent across tabs of the same origin.
*/
export const ACTIVITY_KEY = "eruditiontx_last_activity";
const ACTIVITY_EVENTS = ["mousedown", "mousemove", "keydown", "scroll", "touchstart", "click"];
const POLL_INTERVAL_MS = 5000;
const WRITE_THROTTLE_MS = 1000;
const readLastActivity = () => {
try {
const raw = window.localStorage.getItem(ACTIVITY_KEY);
const ts = raw ? parseInt(raw, 10) : NaN;
return Number.isFinite(ts) ? ts : null;
} catch {
return null;
}
};
const writeLastActivity = (ts) => {
try {
window.localStorage.setItem(ACTIVITY_KEY, String(ts));
} catch {
/* private mode / quota — ignore; the check falls back to seeding "now" */
}
};
const IdleTimer = ({ idleTime, setIsIdle }) => {
const idleMs = idleTime * 1000;
useEffect(() => {
let lastWrite = 0;
const markActivity = () => {
const now = Date.now();
Iif (now - lastWrite < WRITE_THROTTLE_MS) return; // throttle localStorage writes
lastWrite = now;
writeLastActivity(now);
};
const checkIdle = () => {
const last = readLastActivity();
// No stored timestamp yet → treat now as activity (don't false-trip).
Iif (last === null) {
writeLastActivity(Date.now());
return;
}
const remainingMs = idleMs - (Date.now() - last);
if (remainingMs <= 0) {
setIsIdle(true);
return;
}
console.log(`[IdleTimer] idle in ${Math.ceil(remainingMs / 1000)}s`);
};
const onVisibility = () => {
Eif (document.visibilityState === "visible") checkIdle();
};
// Seed activity on mount so a fresh session starts the clock now.
writeLastActivity(Date.now());
ACTIVITY_EVENTS.forEach((event) => window.addEventListener(event, markActivity, { passive: true }));
document.addEventListener("visibilitychange", onVisibility);
window.addEventListener("focus", checkIdle);
const interval = setInterval(checkIdle, POLL_INTERVAL_MS);
return () => {
ACTIVITY_EVENTS.forEach((event) => window.removeEventListener(event, markActivity));
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("focus", checkIdle);
clearInterval(interval);
};
}, [idleMs, setIsIdle]);
return null;
};
export default IdleTimer;
|