Fix runaway scroll-loading loop on the grand livre
Two compounding issues in the sliding-window edge loading: - loadOlder()/loadNewer() kept pushing windowStart/windowEnd outward even when a fetch came back empty (e.g. extending into future dates with no data yet). Since nothing about the table's height or scroll position changes when 0 rows come back, checkEdges() stayed satisfied and the very next scroll/layout tick fired the same load again, drifting the window further out forever. Now bounded by MIN_LOADABLE_DATE/MAX_LOADABLE_DATE. - loadOlder() and loadNewer() had independent in-flight guards and could run concurrently, racing on the same rows/windowStart/windowEnd state -- a scroll-position compensation write inside one (from trimming the far end) fires a real scroll event that can kick off the other direction mid-flight. Now serialized behind a shared loadingWindow lock.
This commit is contained in:
@@ -40,6 +40,18 @@
|
||||
const EXTEND_MONTHS = 6; // increment per scroll-triggered load
|
||||
const MAX_LOADED_MONTHS = 30; // trim the far end once the window exceeds this
|
||||
|
||||
// Absolute bounds on how far the window can extend. Without these,
|
||||
// loadOlder()/loadNewer() kept pushing windowStart/windowEnd outward
|
||||
// even when a fetch came back empty (e.g. extending into future dates
|
||||
// with no data yet) -- since nothing about the table's height or
|
||||
// scroll position changes when 0 rows come back, checkEdges() stayed
|
||||
// satisfied and the next scroll/layout tick fired the exact same load
|
||||
// again, drifting further out forever with no way to stop. Comfortably
|
||||
// before the earliest migrated year (2021) and well past any plausible
|
||||
// future-dated entry.
|
||||
const MIN_LOADABLE_DATE = '2020-06-01';
|
||||
const MAX_LOADABLE_DATE = addMonths(todayStr(), 24);
|
||||
|
||||
function todayStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
@@ -183,6 +195,16 @@
|
||||
windowEnd: null,
|
||||
loadingOlder: false,
|
||||
loadingNewer: false,
|
||||
// True while EITHER loadOlder() or loadNewer() is running -- they
|
||||
// both mutate `rows`/windowStart/windowEnd, and a scroll-position
|
||||
// compensation write inside one (from trimming the far end) fires
|
||||
// a real 'scroll' event that can kick off the other direction
|
||||
// while the first is still in flight. Without a shared lock, the
|
||||
// two interleave and race on the same state -- observed as `rows`
|
||||
// transiently emptying out (table blinks away) before settling.
|
||||
// loadingOlder/loadingNewer stay separate for the sentinel-row
|
||||
// text; this gates actual execution.
|
||||
loadingWindow: false,
|
||||
// Footer : totaux de l'année actuellement visible à l'écran (pas
|
||||
// de la fenêtre chargée), voir detectCurrentYear().
|
||||
currentYear: null,
|
||||
@@ -434,10 +456,15 @@
|
||||
async loadOlder() {
|
||||
// Bypassed while a specific "Année" filter is active -- that mode
|
||||
// loads exactly one year and nothing else (see enterYearMode).
|
||||
if (this.loadingOlder || !this.windowStart || this.filterYear) return;
|
||||
// Also bypassed once the window already reaches MIN_LOADABLE_DATE --
|
||||
// nothing older to fetch, so skip straight out instead of re-firing
|
||||
// an empty request on every subsequent scroll/layout tick.
|
||||
if (this.loadingWindow || !this.windowStart || this.filterYear || this.windowStart <= MIN_LOADABLE_DATE) return;
|
||||
this.loadingWindow = true;
|
||||
this.loadingOlder = true;
|
||||
try {
|
||||
const newStart = addMonths(this.windowStart, -EXTEND_MONTHS);
|
||||
let newStart = addMonths(this.windowStart, -EXTEND_MONTHS);
|
||||
if (newStart < MIN_LOADABLE_DATE) newStart = MIN_LOADABLE_DATE;
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(newStart, this.windowStart));
|
||||
const newRows = buildRows(data, includedMap);
|
||||
this.windowStart = newStart;
|
||||
@@ -464,13 +491,18 @@
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.loadingOlder = false;
|
||||
this.loadingWindow = false;
|
||||
}
|
||||
},
|
||||
async loadNewer() {
|
||||
if (this.loadingNewer || !this.windowEnd || this.filterYear) return;
|
||||
// Same MIN_LOADABLE_DATE reasoning as loadOlder(), mirrored at the
|
||||
// future end.
|
||||
if (this.loadingWindow || !this.windowEnd || this.filterYear || this.windowEnd >= MAX_LOADABLE_DATE) return;
|
||||
this.loadingWindow = true;
|
||||
this.loadingNewer = true;
|
||||
try {
|
||||
const newEnd = addMonths(this.windowEnd, EXTEND_MONTHS);
|
||||
let newEnd = addMonths(this.windowEnd, EXTEND_MONTHS);
|
||||
if (newEnd > MAX_LOADABLE_DATE) newEnd = MAX_LOADABLE_DATE;
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(this.windowEnd, newEnd));
|
||||
const newRows = buildRows(data, includedMap);
|
||||
this.windowEnd = newEnd;
|
||||
@@ -497,6 +529,7 @@
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.loadingNewer = false;
|
||||
this.loadingWindow = false;
|
||||
}
|
||||
},
|
||||
// Scroll-position based rather than IntersectionObserver: one
|
||||
|
||||
Reference in New Issue
Block a user