Files
hockey_new/hockey_data/static/session-activity.js

112 lines
4.2 KiB
JavaScript

(() => {
"use strict";
const SEND_INTERVAL = 60_000;
const RETRY_INTERVAL = 30_000;
let lastSentAt = -Infinity;
let pendingActivityAt = null;
let activityTimer = null;
let expiryTimer = null;
let activityInFlight = false;
let statusInFlight = false;
let stopped = false;
let idleLimit = 2 * 60 * 60 * 1000;
let lastStatusAt = 0;
async function sessionRequest(path, options = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15_000);
try {
const response = await fetch(path, { credentials: "same-origin", cache: "no-store", ...options, signal: controller.signal });
const payload = response.ok ? await response.json() : null;
return { status: response.status, ok: response.ok, payload };
} finally { clearTimeout(timer); }
}
function expired() {
if (stopped) return;
stopped = true;
clearTimeout(activityTimer);
clearTimeout(expiryTimer);
window.location.replace("/login?reason=inactive");
}
function acceptStatus(payload) {
if (stopped) return;
const seconds = Number(payload.expires_in_seconds);
if (!Number.isFinite(seconds)) throw new Error("Invalid session deadline");
idleLimit = Math.max(1, Number(payload.idle_timeout_seconds) || 7200) * 1000;
lastStatusAt = Date.now();
clearTimeout(expiryTimer);
// Check the shared server session before redirecting: another tab may have
// renewed it. Passive polling here must not extend its deadline.
expiryTimer = setTimeout(checkSession, Math.max(250, seconds * 1000));
}
async function checkSession() {
if (stopped || statusInFlight) return;
statusInFlight = true;
try {
const response = await sessionRequest("/api/hockey/auth/session");
if (response.status === 401) return expired();
if (!response.ok) throw new Error("Session status unavailable");
acceptStatus(response.payload);
} catch (_) {
// A temporary network/WFL failure must not log an operator out.
if (!stopped) {
clearTimeout(expiryTimer);
expiryTimer = setTimeout(checkSession, RETRY_INTERVAL);
}
} finally { statusInFlight = false; }
}
function scheduleActivity(delay = 0) {
if (stopped || activityInFlight || activityTimer !== null || pendingActivityAt === null) return;
activityTimer = setTimeout(sendActivity, Math.max(delay, SEND_INTERVAL - (Date.now() - lastSentAt), 0));
}
async function sendActivity() {
activityTimer = null;
if (stopped || activityInFlight || pendingActivityAt === null) return;
const activityAt = pendingActivityAt;
const idleMs = Math.max(0, Date.now() - activityAt);
pendingActivityAt = null;
if (idleMs >= idleLimit) { void checkSession(); return; }
activityInFlight = true;
lastSentAt = Date.now();
let retry = 0;
try {
const response = await sessionRequest("/api/hockey/auth/activity", {
method: "POST", credentials: "same-origin", cache: "no-store",
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ idle_ms: idleMs }),
});
if (response.status === 401) return expired();
if (!response.ok) throw new Error("Session renewal unavailable");
acceptStatus(response.payload);
} catch (_) {
pendingActivityAt = Math.max(pendingActivityAt ?? activityAt, activityAt);
retry = RETRY_INTERVAL;
} finally {
activityInFlight = false;
scheduleActivity(retry);
}
}
function recordActivity(event) {
if (stopped || !event.isTrusted || document.visibilityState === "hidden") return;
pendingActivityAt = Date.now();
scheduleActivity();
}
["pointerdown", "pointermove", "keydown", "wheel", "touchstart", "input"].forEach((type) => {
window.addEventListener(type, recordActivity, { capture: true, passive: true });
});
const checkOnReturn = () => {
if (document.visibilityState !== "hidden" && Date.now() - lastStatusAt >= SEND_INTERVAL) void checkSession();
};
window.addEventListener("focus", checkOnReturn);
window.addEventListener("pageshow", checkOnReturn);
window.addEventListener("online", () => { void checkSession(); scheduleActivity(); });
document.addEventListener("visibilitychange", checkOnReturn);
void checkSession();
})();