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 EXTEND_MONTHS = 6; // increment per scroll-triggered load
|
||||||
const MAX_LOADED_MONTHS = 30; // trim the far end once the window exceeds this
|
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() {
|
function todayStr() {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return new Date().toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
@@ -183,6 +195,16 @@
|
|||||||
windowEnd: null,
|
windowEnd: null,
|
||||||
loadingOlder: false,
|
loadingOlder: false,
|
||||||
loadingNewer: 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
|
// Footer : totaux de l'année actuellement visible à l'écran (pas
|
||||||
// de la fenêtre chargée), voir detectCurrentYear().
|
// de la fenêtre chargée), voir detectCurrentYear().
|
||||||
currentYear: null,
|
currentYear: null,
|
||||||
@@ -434,10 +456,15 @@
|
|||||||
async loadOlder() {
|
async loadOlder() {
|
||||||
// Bypassed while a specific "Année" filter is active -- that mode
|
// Bypassed while a specific "Année" filter is active -- that mode
|
||||||
// loads exactly one year and nothing else (see enterYearMode).
|
// 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;
|
this.loadingOlder = true;
|
||||||
try {
|
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 { data, includedMap } = await fetchLignes(rangeUrl(newStart, this.windowStart));
|
||||||
const newRows = buildRows(data, includedMap);
|
const newRows = buildRows(data, includedMap);
|
||||||
this.windowStart = newStart;
|
this.windowStart = newStart;
|
||||||
@@ -464,13 +491,18 @@
|
|||||||
this.error = err.message;
|
this.error = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingOlder = false;
|
this.loadingOlder = false;
|
||||||
|
this.loadingWindow = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadNewer() {
|
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;
|
this.loadingNewer = true;
|
||||||
try {
|
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 { data, includedMap } = await fetchLignes(rangeUrl(this.windowEnd, newEnd));
|
||||||
const newRows = buildRows(data, includedMap);
|
const newRows = buildRows(data, includedMap);
|
||||||
this.windowEnd = newEnd;
|
this.windowEnd = newEnd;
|
||||||
@@ -497,6 +529,7 @@
|
|||||||
this.error = err.message;
|
this.error = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingNewer = false;
|
this.loadingNewer = false;
|
||||||
|
this.loadingWindow = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Scroll-position based rather than IntersectionObserver: one
|
// Scroll-position based rather than IntersectionObserver: one
|
||||||
|
|||||||
Reference in New Issue
Block a user