Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use custom cache expiration logic #94

Merged
merged 5 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 21 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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;
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

const cacheStore = new WeakMap<AnyFunction, CacheStorage<any, any>>();
const cacheTimerStore = new WeakMap<AnyFunction, Set<number>>();

type CacheStorageContent<ValueType> = {
data: ValueType;
Expand Down Expand Up @@ -104,8 +104,8 @@ export default function mem<
maxAge,
}: Options<FunctionToMemoize, CacheKeyType> = {},
): FunctionToMemoize {
if (typeof maxAge === 'number') {
mapAgeCleaner(cache as unknown as Map<CacheKeyType, ReturnType<FunctionToMemoize>>);
if (typeof maxAge === 'number' && maxAge <= 0) {
return fn;
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
}

const memoized = function (this: any, ...arguments_: Parameters<FunctionToMemoize>): ReturnType<FunctionToMemoize> {
Expand All @@ -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
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
cacheTimerStore.set(fn, timers);
}

return result;
} as FunctionToMemoize;

Expand Down Expand Up @@ -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);
Expand All @@ -211,4 +223,8 @@ export function memClear(fn: AnyFunction): void {
}

cache.clear();

for (const timer of cacheTimerStore.get(fn) ?? []) {
clearTimeout(timer);
}
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"promise"
],
"dependencies": {
"map-age-cleaner": "^0.2.0",
"mimic-function": "^5.0.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ Clear all cached data of a memoized function.

Type: `Function`

Memoized function.
The memoized function.

## Tips

Expand Down
76 changes: 76 additions & 0 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});