Realign periodic polling to the deadline after a visibility catch-up

An overdue refresh on return to a visible tab advanced the due time but kept
the old setInterval phase, so the next phase tick was skipped and the gap
could approach two intervals (also after a failed resume refresh). The
catch-up now re-arms the interval at the new deadline. Interval ticks no
longer compare against the due time, since the phase and the deadline share
one origin. Overlap prevention and teardown are unchanged, and a stale
listener cannot re-arm a stopped loop.

Tests cover the next poll one interval after a between-ticks resume, with a
successful and a failed catch-up refresh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-23 12:22:42 +01:00
1 parent be44f523e0
commit 07d6417446
4 files changed
+94 -16

No files matched your search

+34 -14
View File
@@ -553,24 +553,44 @@ export class LayerLifecycle {
: 0;
if (refreshInterval > 0) {
const visibilityDocument = this._visibilityTarget;
let dueAt = Date.now() + refreshInterval;
const tick = () => {
if (
visibilityDocument?.hidden ||
visibilityDocument?.visibilityState === 'hidden' ||
!entry.enabled ||
entry.destroying ||
entry.refreshing ||
Date.now() < dueAt
)
return;
let intervalId = null;
let dueAt = 0;
const canPoll = () =>
!visibilityDocument?.hidden &&
visibilityDocument?.visibilityState !== 'hidden' &&
entry.enabled &&
!entry.destroying &&
!entry.refreshing &&
entry.intervalId === intervalId;
// dueAt and the interval phase share one origin, so every interval tick
// is a due poll. A visibility catch-up re-arms the interval at the new
// deadline; otherwise the old phase would skip its next tick.
const schedule = () => {
clearInterval(intervalId);
dueAt = Date.now() + refreshInterval;
intervalId = setInterval(onInterval, refreshInterval);
entry.intervalId = intervalId;
};
const onInterval = () => {
if (!canPoll()) return;
dueAt = Date.now() + refreshInterval;
void this._runPeriodicUpdate(layerId, entry);
};
entry.intervalId = setInterval(tick, refreshInterval);
visibilityDocument?.addEventListener?.('visibilitychange', tick);
const onVisibilityChange = () => {
if (!canPoll() || Date.now() < dueAt) return;
schedule();
void this._runPeriodicUpdate(layerId, entry);
};
schedule();
visibilityDocument?.addEventListener?.(
'visibilitychange',
onVisibilityChange,
);
entry.removeVisibilityListener = () =>
visibilityDocument?.removeEventListener?.('visibilitychange', tick);
visibilityDocument?.removeEventListener?.(
'visibilitychange',
onVisibilityChange,
);
} else if (updateInterval === 0) {
entry.intervalId = setInterval(() => {
if (!entry.enabled) return;