handleBatchDone's terminal-job auto-cap (introduced in 3.3.0) lived
inline as a manual two-pass loop over queueJobs. Pull the algorithm
into lib/queue-prune.js as pure pruneOldestTerminalJobs(jobs, limit)
that returns { kept, dropped } so the caller can clean up its index/
selection in one go. Same single implementation backs runtime and
tests via dual-environment loader (CommonJS module.exports for Node
tests, window.QueuePrune global for the renderer via index.html
script tag).
Coverage:
- Empty / null / non-array input → no-op
- All-non-terminal → no-op (regardless of limit)
- Terminal count ≤ limit → no-op
- Terminal count > limit → drops oldest by insertion order
- Mixed queue: non-terminals always kept, only terminals dropped
- limit=0 → drops every terminal
- Negative / NaN / Infinity limits → safe no-op
- Malformed entries (null, missing status) handled without throwing
- Large-queue stress (5000 done jobs) keeps newest 500
- TERMINAL_STATUSES set covers exactly done/skipped/error/aborted
Renderer uses window.QueuePrune?. so a failed script load just
disables the prune rather than crashing every batch-done. 107/107
tests green.
60 lines
2.3 KiB
JavaScript
60 lines
2.3 KiB
JavaScript
// Queue auto-prune logic. Extracted from renderer/app.js handleBatchDone so
|
|
// the algorithm can be unit-tested without needing a DOM or the renderer's
|
|
// module-level state (queueJobs, _jobIndexById).
|
|
//
|
|
// Loaded both as a CommonJS module (Node tests) and as a browser global
|
|
// (renderer/app.js via index.html script tag) so the same single
|
|
// implementation backs both runtime and tests — no drift between them.
|
|
//
|
|
// Behaviour: when the number of terminal-status jobs (done / skipped /
|
|
// error / aborted) in the queue exceeds `limit`, drop the oldest terminal
|
|
// jobs (insertion order) until we're back at the limit. Non-terminal jobs
|
|
// (queued / preview / uploading / retrying / getting-server) are always
|
|
// kept — those are work the user can still act on. Without this cap a
|
|
// long session accumulates thousands of done rows and every render becomes
|
|
// O(N) on a perpetually-growing N.
|
|
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
const TERMINAL_STATUSES = new Set(['done', 'skipped', 'error', 'aborted']);
|
|
|
|
/**
|
|
* Compute which jobs to keep vs drop, given a queue and a terminal-jobs cap.
|
|
* @param {Array<{id: string, status: string}>} jobs the current queue
|
|
* @param {number} limit max terminal jobs to keep
|
|
* @returns {null | { kept: Array, dropped: Array }} null when nothing changed
|
|
*/
|
|
function pruneOldestTerminalJobs(jobs, limit) {
|
|
if (!Array.isArray(jobs) || jobs.length === 0) return null;
|
|
if (!Number.isFinite(limit) || limit < 0) return null;
|
|
|
|
// Walk once, record indices of terminal jobs in insertion order.
|
|
const terminalIdxs = [];
|
|
for (let i = 0; i < jobs.length; i++) {
|
|
const j = jobs[i];
|
|
if (j && TERMINAL_STATUSES.has(j.status)) terminalIdxs.push(i);
|
|
}
|
|
if (terminalIdxs.length <= limit) return null;
|
|
|
|
const dropCount = terminalIdxs.length - limit;
|
|
const dropSet = new Set(terminalIdxs.slice(0, dropCount));
|
|
|
|
const kept = [];
|
|
const dropped = [];
|
|
for (let i = 0; i < jobs.length; i++) {
|
|
if (dropSet.has(i)) dropped.push(jobs[i]);
|
|
else kept.push(jobs[i]);
|
|
}
|
|
return { kept, dropped };
|
|
}
|
|
|
|
const api = { pruneOldestTerminalJobs, TERMINAL_STATUSES };
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = api;
|
|
} else if (root) {
|
|
root.QueuePrune = api;
|
|
}
|
|
})(typeof window !== 'undefined' ? window : this);
|