Multi-Hoster-Upload/lib/throttled-cache.js
Administrator cf34353036 test(sort): extract throttled-cache utility + 12 unit tests
The dynamic-key sort throttle (3.3.0) used an inline ad-hoc cache
object with a Date.now() comparison. Pull it out into a clean
generic-purpose makeThrottledCache helper that takes the TTL and an
optional clock function so tests can drive time without sleeping.
Same dual-environment loader (CommonJS for tests, window global for
the renderer via index.html script tag) as queue-prune.

API: get(sig, input) / set(sig, input, value) / clear() / peek().
sig + input identity must both match for a hit. Inputs are compared
by reference (===), exactly what sortQueueJobs needs to invalidate
on a fresh queueJobs array (e.g. backup import).

Coverage:
- empty cache → undefined
- within TTL → cached value
- past TTL → miss (boundary at refreshMs)
- different signature → miss
- different input identity → miss (even with same content)
- overwrite refreshes timestamp
- clear empties everything
- peek reports age + signature for diagnostics
- invalid TTL throws (negative, NaN, non-number)
- TTL=0 means every call misses (immediate expiry)
- default clock works (Date.now)
- large arrays tracked by identity, not value

Renderer rewires _dynamicSortCache to the new helper with a fallback
no-op shim if window.ThrottledCache failed to load. 119/119 green.
2026-04-28 07:12:52 +02:00

52 lines
1.9 KiB
JavaScript

// 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);