fix: guard Dashboard Apps loads against stale overwrites and drag/error clobbering

loadApps only checked for an in-progress drag before its await, and its
catch wiped whatever was on screen. Three concurrency issues followed:

- A slow, older load resolving after a newer one could overwrite the
  newer app list (and clobber a reorder the user saved while the stale
  fetch was in flight). Tag each load with an increasing id and skip
  applying one only when a strictly newer load has already applied —
  gating on "already applied" (not "latest started") so the first load to
  resolve still populates the list for the pager's ResizeObserver.
- A drag that began while a load was awaiting could have the grid rebuilt
  out from under it; re-check the drag after the await.
- A transient re-fetch error replaced a working grid with "Failed to load
  apps"; only show that placeholder when nothing has loaded yet.

Verified live: a slow older load no longer clobbers a newer one, and the
grid still renders normally on activation.
This commit is contained in:
jelveh
2026-07-18 15:04:19 -07:00
parent 1bb5b09d75
commit f1e81357b6
+21 -1
View File
@@ -905,6 +905,15 @@ const TabApps = {
this._endDrag(false);
}
// Give each load a monotonically increasing id. Concurrent loads are
// routine (init + initial-route onActivate both fire on open); an
// older/slower response must not clobber a newer one that already
// applied — or a reorder the user saved while a stale fetch was in
// flight. We gate on "already applied", not "latest started", so the
// first load to resolve still populates _apps (the pager's
// ResizeObserver needs _apps set as soon as any load resolves).
const loadSeq = (this._loadSeq = (this._loadSeq || 0) + 1);
const $container = $el_window.find('.myapps-container');
try {
@@ -988,13 +997,24 @@ const TabApps = {
} catch ( _e ) {
orderedNames = null;
}
// Skip only if a strictly newer load already applied its result, or
// a drag began while we were awaiting (rendering would yank the grid
// out from under it).
if ( loadSeq < (this._appliedSeq || 0) ) return;
if ( this._drag?.started ) return;
this._appliedSeq = loadSeq;
this._hasCustomOrder = Array.isArray(orderedNames) && orderedNames.length > 0;
this._apps = reconcileAppOrder(merged, orderedNames);
this.renderApps($el_window);
} catch (e) {
console.error('Failed to load installed apps:', e);
$container.html('<div class="myapps-empty"><p>Failed to load apps</p></div>');
// Only show the failure placeholder when nothing has loaded yet; a
// transient re-fetch error must not wipe a grid already on screen.
if ( ! this._apps ) {
$container.html('<div class="myapps-empty"><p>Failed to load apps</p></div>');
}
}
},