Memoize Functions With a TTL Cache
Cache expensive function results by a custom key with per-entry TTL, bounded size, eviction, hit statistics, and manual invalidation.
Code
export function memoize(fn, options = {}) {
if (typeof fn !== "function") {
throw new TypeError("fn must be a function");
}
const {
ttlMs = 60_000,
maxSize = 500,
key = (...args) => JSON.stringify(args),
now = Date.now,
} = options;
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
throw new RangeError("ttlMs must be a positive finite number");
}
if (!Number.isInteger(maxSize) || maxSize < 1) {
throw new RangeError("maxSize must be a positive integer");
}
if (typeof key !== "function" || typeof now !== "function") {
throw new TypeError("key and now must be functions");
}
const cache = new Map();
const stats = { hits: 0, misses: 0, evictions: 0, expirations: 0 };
const memoized = function memoized(...args) {
const cacheKey = key(...args);
const timestamp = now();
const existing = cache.get(cacheKey);
if (existing !== undefined) {
if (existing.expiresAt > timestamp) {
stats.hits += 1;
cache.delete(cacheKey);
cache.set(cacheKey, existing);
return existing.value;
}
cache.delete(cacheKey);
stats.expirations += 1;
}
stats.misses += 1;
const value = fn.apply(this, args);
if (cache.size >= maxSize) {
const oldestKey = cache.keys().next().value;
if (oldestKey !== undefined) {
cache.delete(oldestKey);
stats.evictions += 1;
}
}
cache.set(cacheKey, { value, expiresAt: timestamp + ttlMs });
return value;
};
memoized.clear = () => cache.clear();
memoized.stats = () => ({ ...stats, size: cache.size });
return memoized;
}
let calls = 0;
const slowSquare = memoize(
(value) => {
calls += 1;
return value * value;
},
{ ttlMs: 40 },
);
console.log(slowSquare(4), slowSquare(4), calls);
console.log(slowSquare(5), calls);
console.log(slowSquare.stats());
await new Promise((resolve) => setTimeout(resolve, 50));
console.log(slowSquare(4), calls);
slowSquare.clear();
console.log(slowSquare.stats());Report a problem with this exampleOpens a pre-filled issue on GitHub.