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.
114 lines
3.5 KiB
JavaScript
114 lines
3.5 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { makeThrottledCache } = require('../lib/throttled-cache');
|
|
|
|
function fakeClock(start = 0) {
|
|
let t = start;
|
|
const fn = () => t;
|
|
fn.advance = (ms) => { t += ms; };
|
|
fn.set = (ms) => { t = ms; };
|
|
return fn;
|
|
}
|
|
|
|
test('returns undefined when empty', () => {
|
|
const c = makeThrottledCache(100);
|
|
assert.equal(c.get('any', {}), undefined);
|
|
});
|
|
|
|
test('returns the set value within the window', () => {
|
|
const clock = fakeClock();
|
|
const c = makeThrottledCache(100, clock);
|
|
const input = [1, 2, 3];
|
|
c.set('sig-a', input, 'value-1');
|
|
assert.equal(c.get('sig-a', input), 'value-1');
|
|
clock.advance(50);
|
|
assert.equal(c.get('sig-a', input), 'value-1', 'still valid at 50/100 ms');
|
|
clock.advance(49);
|
|
assert.equal(c.get('sig-a', input), 'value-1', 'still valid at 99/100 ms');
|
|
});
|
|
|
|
test('expires exactly at refreshMs boundary', () => {
|
|
const clock = fakeClock();
|
|
const c = makeThrottledCache(100, clock);
|
|
c.set('s', {}, 'v');
|
|
clock.advance(100);
|
|
assert.equal(c.get('s', {}), undefined, '>= refreshMs is a miss');
|
|
});
|
|
|
|
test('miss on different signature', () => {
|
|
const c = makeThrottledCache(1000, fakeClock());
|
|
const input = {};
|
|
c.set('sig-a', input, 'v');
|
|
assert.equal(c.get('sig-b', input), undefined);
|
|
});
|
|
|
|
test('miss on different input identity even with same signature', () => {
|
|
const c = makeThrottledCache(1000, fakeClock());
|
|
c.set('sig-a', { a: 1 }, 'v');
|
|
// Different object identity — the cache compares by ===, not by contents
|
|
assert.equal(c.get('sig-a', { a: 1 }), undefined);
|
|
});
|
|
|
|
test('overwrite by re-setting same signature', () => {
|
|
const clock = fakeClock();
|
|
const c = makeThrottledCache(100, clock);
|
|
const input = [];
|
|
c.set('s', input, 'old');
|
|
clock.advance(50);
|
|
c.set('s', input, 'new');
|
|
// The new entry has a fresh timestamp → still valid for another 100 ms
|
|
clock.advance(99);
|
|
assert.equal(c.get('s', input), 'new');
|
|
});
|
|
|
|
test('clear empties the cache', () => {
|
|
const c = makeThrottledCache(1000, fakeClock());
|
|
c.set('s', {}, 'v');
|
|
c.clear();
|
|
assert.equal(c.get('s', {}), undefined);
|
|
assert.equal(c.peek(), null);
|
|
});
|
|
|
|
test('peek reports age and signature', () => {
|
|
const clock = fakeClock();
|
|
const c = makeThrottledCache(1000, clock);
|
|
c.set('mysig', {}, 'v');
|
|
clock.advance(42);
|
|
const p = c.peek();
|
|
assert.equal(p.sig, 'mysig');
|
|
assert.equal(p.age, 42);
|
|
assert.equal(p.ts, 0);
|
|
});
|
|
|
|
test('throws on invalid refreshMs', () => {
|
|
assert.throws(() => makeThrottledCache(-1));
|
|
assert.throws(() => makeThrottledCache(NaN));
|
|
assert.throws(() => makeThrottledCache('100'));
|
|
});
|
|
|
|
test('refreshMs=0 means every call misses', () => {
|
|
const clock = fakeClock();
|
|
const c = makeThrottledCache(0, clock);
|
|
const input = {};
|
|
c.set('s', input, 'v');
|
|
// Same tick: 0 - 0 = 0 → not less than refreshMs (0) → miss
|
|
assert.equal(c.get('s', input), undefined);
|
|
});
|
|
|
|
test('default clock is Date.now when none provided', () => {
|
|
const c = makeThrottledCache(10000);
|
|
const input = {}; // single ref — get and set must use the SAME identity
|
|
c.set('x', input, 'v');
|
|
assert.equal(c.get('x', input), 'v');
|
|
});
|
|
|
|
test('large input arrays are tracked by identity, not value', () => {
|
|
const c = makeThrottledCache(1000, fakeClock());
|
|
const arr1 = new Array(10000).fill(0);
|
|
const arr2 = new Array(10000).fill(0);
|
|
c.set('s', arr1, 'cached');
|
|
assert.equal(c.get('s', arr1), 'cached');
|
|
assert.equal(c.get('s', arr2), undefined, 'different array → miss');
|
|
});
|