diff --git a/lib/throttled-cache.js b/lib/throttled-cache.js
new file mode 100644
index 0000000..c6376cf
--- /dev/null
+++ b/lib/throttled-cache.js
@@ -0,0 +1,51 @@
+// Time-windowed memoization. Reuses a previously-computed value if the
+// signature + input identity match AND the cached entry is younger than
+// `refreshMs`. Used by the renderer's dynamic-key sort throttle (every
+// progress tick re-sorts a 5000-row queue → reuse for 200 ms, the user
+// can't perceive sub-200 ms reorder lag).
+//
+// Loaded both as a CommonJS module (Node tests) and as a browser global
+// (renderer/app.js via index.html script tag) — same single implementation
+// across runtime and tests.
+
+(function (root) {
+ 'use strict';
+
+ /**
+ * Build a throttled cache. The clock is injected so tests don't have to
+ * sleep — pass `() => fakeClock.value` from tests.
+ *
+ * @param {number} refreshMs cache TTL in milliseconds
+ * @param {() => number} [now] clock source, defaults to Date.now
+ */
+ function makeThrottledCache(refreshMs, now) {
+ if (!Number.isFinite(refreshMs) || refreshMs < 0) {
+ throw new TypeError('refreshMs must be a non-negative finite number');
+ }
+ const clock = typeof now === 'function' ? now : () => Date.now();
+ let entry = null;
+ return {
+ get(sig, input) {
+ if (!entry) return undefined;
+ if (entry.sig !== sig) return undefined;
+ if (entry.input !== input) return undefined;
+ if (clock() - entry.ts >= refreshMs) return undefined;
+ return entry.value;
+ },
+ set(sig, input, value) {
+ entry = { sig, input, value, ts: clock() };
+ return value;
+ },
+ clear() { entry = null; },
+ // Introspection (mainly for tests/debug). Returns null when empty.
+ peek() {
+ if (!entry) return null;
+ return { sig: entry.sig, ts: entry.ts, age: clock() - entry.ts };
+ }
+ };
+ }
+
+ const api = { makeThrottledCache };
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
+ else if (root) root.ThrottledCache = api;
+})(typeof window !== 'undefined' ? window : this);
diff --git a/renderer/app.js b/renderer/app.js
index 7127eaa..d969f56 100644
--- a/renderer/app.js
+++ b/renderer/app.js
@@ -1167,8 +1167,11 @@ const _STATIC_SORT_KEYS = new Set(['filename', 'host']);
// one UI_UPDATE_INTERVAL window (200ms), reuse the previous sort even if it's
// slightly out of order — the user can't perceive sub-200ms reorder lag, and
// at 5000 queued jobs this is the difference between smooth and stuttering.
-let _dynamicSortCache = { key: '', direction: '', jobsRef: null, result: null, ts: 0 };
+// Uses lib/throttled-cache.js (see tests/throttled-cache.test.js).
const DYNAMIC_SORT_REFRESH_MS = 200;
+const _dynamicSortCache = window.ThrottledCache
+ ? window.ThrottledCache.makeThrottledCache(DYNAMIC_SORT_REFRESH_MS)
+ : { get: () => undefined, set: (s, i, v) => v, clear: () => {} };
function sortQueueJobs(jobs) {
const { key, direction } = queueSortState;
@@ -1179,11 +1182,13 @@ function sortQueueJobs(jobs) {
return _queueSortCache.result;
}
// Dynamic-key throttle: same key+direction+array, sorted within the last
- // 200ms → reuse. Cleared on user-initiated sort changes (different key).
- if (!canCache && _dynamicSortCache.jobsRef === jobs &&
- _dynamicSortCache.key === key && _dynamicSortCache.direction === direction &&
- _dynamicSortCache.result && (Date.now() - _dynamicSortCache.ts) < DYNAMIC_SORT_REFRESH_MS) {
- return _dynamicSortCache.result;
+ // 200ms → reuse. The cache is keyed by `key|direction` and uses the jobs
+ // array identity as the input ref, so a fresh queueJobs (e.g. after
+ // backup import) misses correctly.
+ if (!canCache) {
+ const dynSig = `${key}|${direction}`;
+ const cached = _dynamicSortCache.get(dynSig, jobs);
+ if (cached) return cached;
}
const sorted = jobs.slice().sort((a, b) => {
@@ -1197,7 +1202,7 @@ function sortQueueJobs(jobs) {
return cmp * factor;
});
if (sig) _queueSortCache = { sig, result: sorted, jobsRef: jobs };
- else _dynamicSortCache = { key, direction, jobsRef: jobs, result: sorted, ts: Date.now() };
+ else _dynamicSortCache.set(`${key}|${direction}`, jobs, sorted);
return sorted;
}
diff --git a/renderer/index.html b/renderer/index.html
index a9a2ae9..be63aaf 100644
--- a/renderer/index.html
+++ b/renderer/index.html
@@ -331,6 +331,7 @@
+