diff --git a/index.ts b/index.ts index b945855..f30e971 100644 --- a/index.ts +++ b/index.ts @@ -1,9 +1,9 @@ import mimicFunction from 'mimic-function'; -import mapAgeCleaner from 'map-age-cleaner'; -type AnyFunction = (...arguments_: readonly any[]) => any; +type AnyFunction = (...arguments_: readonly any[]) => unknown; const cacheStore = new WeakMap>(); +const cacheTimerStore = new WeakMap>(); type CacheStorageContent = { data: ValueType; @@ -104,8 +104,8 @@ export default function mem< maxAge, }: Options = {}, ): FunctionToMemoize { - if (typeof maxAge === 'number') { - mapAgeCleaner(cache as unknown as Map>); + if (typeof maxAge === 'number' && maxAge <= 0) { + return fn; } const memoized = function (this: any, ...arguments_: Parameters): ReturnType { @@ -123,6 +123,18 @@ export default function mem< maxAge: maxAge ? Date.now() + maxAge : Number.POSITIVE_INFINITY, }); + if (typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY) { + const timer = setTimeout(() => { + cache.delete(key); + }, maxAge); + + timer.unref?.(); + + const timers = cacheTimerStore.get(fn) ?? new Set(); + timers.add(timer as any); // eslint-disable-line @typescript-eslint/no-unsafe-argument + cacheTimerStore.set(fn, timers); + } + return result; } as FunctionToMemoize; @@ -198,7 +210,7 @@ export function memDecorator< /** Clear all cached data of a memoized function. -@param fn - Memoized function. +@param fn - The memoized function. */ export function memClear(fn: AnyFunction): void { const cache = cacheStore.get(fn); @@ -211,4 +223,8 @@ export function memClear(fn: AnyFunction): void { } cache.clear(); + + for (const timer of cacheTimerStore.get(fn) ?? []) { + clearTimeout(timer); + } } diff --git a/package.json b/package.json index 5c694f7..096ed43 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "promise" ], "dependencies": { - "map-age-cleaner": "^0.2.0", "mimic-function": "^5.0.0" }, "devDependencies": { diff --git a/readme.md b/readme.md index b4ffe68..3f7bf5a 100644 --- a/readme.md +++ b/readme.md @@ -244,7 +244,7 @@ Clear all cached data of a memoized function. Type: `Function` -Memoized function. +The memoized function. ## Tips diff --git a/test.ts b/test.ts index 41aa6c0..0de414b 100644 --- a/test.ts +++ b/test.ts @@ -240,3 +240,79 @@ test('memClear() throws when called on an unclearable cache', t => { instanceOf: TypeError, }); }); + +test('maxAge - cache item expires after specified duration', async t => { + let i = 0; + const fixture = () => i++; + const memoized = mem(fixture, {maxAge: 100}); + + t.is(memoized(), 0); // Initial call, cached + t.is(memoized(), 0); // Subsequent call, still cached + await delay(150); // Wait for longer than maxAge + t.is(memoized(), 1); // Cache expired, should compute again +}); + +test('maxAge - cache expiration timing is accurate', async t => { + let i = 0; + const fixture = () => i++; + const memoized = mem(fixture, {maxAge: 100}); + + t.is(memoized(), 0); + await delay(90); // Wait for slightly less than maxAge + t.is(memoized(), 0); // Should still be cached + await delay(20); // Total delay now exceeds maxAge + t.is(memoized(), 1); // Should recompute as cache has expired +}); + +test('maxAge - expired items are not present in cache', async t => { + let i = 0; + const fixture = () => i++; + const cache = new Map(); + const memoized = mem(fixture, {maxAge: 100, cache}); + + memoized(); // Call to cache the result + await delay(150); // Wait for cache to expire + memoized(); // Recompute and recache + t.is(cache.size, 1); // Only one item should be in the cache +}); + +test('maxAge - complex arguments and cache expiration', async t => { + let i = 0; + const fixture = object => i++; + const memoized = mem(fixture, {maxAge: 100, cacheKey: JSON.stringify}); + + const arg = {key: 'value'}; + t.is(memoized(arg), 0); + await delay(150); + t.is(memoized(arg), 1); // Argument is the same, but should recompute due to expiration +}); + +test('maxAge - concurrent calls return cached value', async t => { + let i = 0; + const fixture = () => i++; + const memoized = mem(fixture, {maxAge: 100}); + + t.is(memoized(), 0); + await delay(50); // Delay less than maxAge + t.is(memoized(), 0); // Should return cached value +}); + +test('maxAge - different arguments have separate expirations', async t => { + let i = 0; + const fixture = x => i++; + const memoized = mem(fixture, {maxAge: 100}); + + t.is(memoized('a'), 0); + await delay(150); // Expire the cache for 'a' + t.is(memoized('b'), 1); // 'b' should be a separate cache entry + t.is(memoized('a'), 2); // 'a' should be recomputed +}); + +test('maxAge - zero maxAge means no caching', t => { + let i = 0; + const fixture = () => i++; + const memoized = mem(fixture, {maxAge: 0}); + + t.is(memoized(), 0); + t.is(memoized(), 1); // No caching, should increment +});