Files
zoneminder/web/js/table-helpers.js
T
Isaac ConnorandClaude Opus 5 da99928235 fix: revalidate on every resume rather than trusting a time window
The staleness window was unsound. calculateAuthHash() keys the hash to the
clock hour it was minted in and getAuthUser() accepts the last
ZM_AUTH_HASH_TTL hourly buckets, so a hash dies at the top of an hour rather
than at some age. generateAuthHash() then serves the cached one until it is
half a TTL old, so what arrives can already be nearly spent: on the defaults a
hash minted at 10:59 is still handed out at 11:58 and is refused at 12:00. A
client stamping that arrival as fresh for an hour skips the probe until 12:58
and restarts its streams on a dead hash - the exact failure this was written to
prevent. No fixed window is safe, because the remaining life of a hash we hold
can be anything down to zero, and AUTH_STALE_MS also ignored the configured
ZM_AUTH_HASH_TTL.

So drop AUTH_STALE_MS, authIsStale() and authFreshAt, and have whenAuthFresh()
revalidate. The one case that can still skip the probe is having no hash at all
- authentication off, or a relay form that does not use one - where there is
nothing that can expire and nothing a probe would report. revalidateAuth()
already shares one request between concurrent callers, so a resume that wakes
several of these still costs a single probe, and that is what the montage code
did unconditionally before any of this.

refreshTablesPendingVisibility() now returns as soon as it finds nothing was
deferred. It is bound on every classic page including the unauthenticated ones,
and the version before this ran the whole auth path on an empty queue, so
merely becoming visible could fire a probe with no work behind it.

Tests: two authIsStale cases removed with the function, two whenAuthFresh cases
added - a probe is sent and the callback held until it answers, and no probe is
sent when there is no hash. Reintroducing a fast path fails the first. Full JS
suite green, ESLint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkQwahn9pi1y4wJe9BTxjM
2026-09-13 10:37:02 -04:00

73 lines
3.1 KiB
JavaScript

'use strict';
// Helpers shared by the bootstrap-table views (events, console, log, frames,
// reports, snapshots, watch). Loaded as a plain browser script before the view
// scripts (web/skins/classic/includes/functions.php), so the functions below are
// globals by the time any of them runs. Also CommonJS-exported for node unit
// tests (tests/js/table-helpers.test.js).
// Tables whose ajax request was skipped because the page was hidden, waiting to
// be refreshed once it is shown again.
const tablesPendingVisibility = [];
// Decide whether to skip a bootstrap-table ajax request because the page is
// hidden. Returns true when the caller should return without issuing it.
//
// Skipping alone is not enough. Bootstrap-table calls its ajax function on init
// as well as on refresh, and a skipped request is never re-issued, so the table
// renders "No matching records found" over a result it never asked for. Nothing
// brings it back and the user has to refresh by hand (issue #5026). Recording
// the table here means it is refreshed the moment the page becomes visible.
//
// A page can be hidden for the whole of its load - opened in a background tab,
// restored, or simply behind another window - so this is not only about a tab
// the user switched away from later.
function deferTableRequestWhileHidden(table) {
if (document.visibilityState !== 'hidden') return false;
table.bootstrapTable('hideLoading');
// A table can be deferred repeatedly (init, then auto-refresh ticks) and only
// needs refreshing once.
if (tablesPendingVisibility.indexOf(table) === -1) {
tablesPendingVisibility.push(table);
}
return true;
}
// Refresh every table whose request was skipped while hidden. The auth hash we
// were holding may have expired during the hide, so wait for a confirmed one
// rather than have every deferred table 403 (auth-helpers.js). whenAuthFresh is
// absent under node; refresh directly there.
function refreshTablesPendingVisibility() {
if (document.visibilityState === 'hidden') return;
// Drain before refreshing: refresh() calls the ajax function synchronously,
// which would otherwise re-add the table while we are still iterating.
const tables = tablesPendingVisibility.splice(0, tablesPendingVisibility.length);
// Nothing was deferred, so there is nothing to authenticate for. This handler
// is bound on every classic page including the unauthenticated ones, and
// whenAuthFresh() can send a probe, so becoming visible must stay a no-op
// when there is no work.
if (!tables.length) return;
const refresh = function() {
for (let i = 0; i < tables.length; i++) {
tables[i].bootstrapTable('refresh');
}
};
if (typeof whenAuthFresh === 'function') {
whenAuthFresh(refresh);
} else {
refresh();
}
}
if (typeof document !== 'undefined' && document.addEventListener) {
document.addEventListener('visibilitychange', refreshTablesPendingVisibility);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
deferTableRequestWhileHidden,
refreshTablesPendingVisibility,
tablesPendingVisibility,
};
}