fix(queue): stale sort-cache froze UI when queueJobs was replaced

After importing a backup or restoring the queue at startup, queueJobs
is reassigned to a fresh array. The sort-cache keyed its hit on
"key|direction|length" — identical across a replacement with the same
job count. Result: renderQueueTable kept getting the cached sorted
array, which held references to the DISCARDED job objects (frozen at
status='preview'). Uploads ran perfectly in the background, the
status bar updated from stats events, but every row stayed "Bereit"
with "..." as size. The user had to poke `_queueSortCache={sig:'',…}`
in DevTools to unstick it.

Include the jobs array identity (jobsRef) in the cache check. A
replacement of queueJobs → different reference → cache miss → fresh
sort with the real current objects. O(1) identity check, no CPU cost
on the common case (same array, mutated jobs).
This commit is contained in:
Administrator 2026-04-22 18:56:24 +02:00
parent 5afb56b987
commit 58a21ed321

View File

@ -1141,7 +1141,14 @@ const _collatorSimple = new Intl.Collator('de');
// Dynamic keys (status/speed/progress) AND size (which goes 0 → actual when // Dynamic keys (status/speed/progress) AND size (which goes 0 → actual when
// previews resolve / upload starts) are recomputed each call — otherwise a // previews resolve / upload starts) are recomputed each call — otherwise a
// queue sorted by size during previews would be stuck in all-zeros order. // queue sorted by size during previews would be stuck in all-zeros order.
let _queueSortCache = { sig: '', result: [] }; //
// CRITICAL: the cache also tracks jobsRef (identity of the queueJobs array) so
// that a full replacement (e.g. backup import, queue restore) invalidates the
// cache. Length alone can match across a replace and would otherwise pin the
// renderer to stale job references — the UI freezes showing old statuses even
// though queueJobs itself has fresh objects. Observed as "upload runs in
// status bar but all rows stay 'Bereit'" after importing a backup.
let _queueSortCache = { sig: '', result: [], jobsRef: null };
const _STATIC_SORT_KEYS = new Set(['filename', 'host']); const _STATIC_SORT_KEYS = new Set(['filename', 'host']);
function sortQueueJobs(jobs) { function sortQueueJobs(jobs) {
@ -1149,7 +1156,9 @@ function sortQueueJobs(jobs) {
const factor = direction === 'asc' ? 1 : -1; const factor = direction === 'asc' ? 1 : -1;
const canCache = _STATIC_SORT_KEYS.has(key); const canCache = _STATIC_SORT_KEYS.has(key);
const sig = canCache ? `${key}|${direction}|${jobs.length}` : ''; const sig = canCache ? `${key}|${direction}|${jobs.length}` : '';
if (sig && _queueSortCache.sig === sig) return _queueSortCache.result; if (sig && _queueSortCache.sig === sig && _queueSortCache.jobsRef === jobs) {
return _queueSortCache.result;
}
const sorted = jobs.slice().sort((a, b) => { const sorted = jobs.slice().sort((a, b) => {
let cmp = 0; let cmp = 0;
@ -1161,7 +1170,7 @@ function sortQueueJobs(jobs) {
else if (key === 'progress') cmp = (a.progress || 0) - (b.progress || 0); else if (key === 'progress') cmp = (a.progress || 0) - (b.progress || 0);
return cmp * factor; return cmp * factor;
}); });
if (sig) _queueSortCache = { sig, result: sorted }; if (sig) _queueSortCache = { sig, result: sorted, jobsRef: jobs };
return sorted; return sorted;
} }