Skip to content

Commit

Permalink
Add refreshThreshold as parameter of wrap function. Fix #606 (#630)
Browse files Browse the repository at this point in the history
* Add refreshThreshold as parameter of wrap function. Fix #606

* Add test unit for refreshThreshold override

* Less is more. Reuse sleep fn.
  • Loading branch information
lchenay authored Jan 19, 2024
1 parent 8475076 commit 72c0d4a
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ See unit tests in [`test/multi-caching.test.ts`](./test/multi-caching.test.ts) f
### Refresh cache keys in background

Both the `caching` and `multicaching` modules support a mechanism to refresh expiring cache keys in background when using the `wrap` function.
This is done by adding a `refreshThreshold` attribute while creating the caching store.
This is done by adding a `refreshThreshold` attribute while creating the caching store or passing it to the `wrap` function.

If `refreshThreshold` is set and after retrieving a value from cache the TTL will be checked.
If the remaining TTL is less than `refreshThreshold`, the system will update the value asynchronously,
Expand Down
9 changes: 5 additions & 4 deletions src/caching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type Cache<S extends Store = Store> = {
get: <T>(key: string) => Promise<T | undefined>;
del: (key: string) => Promise<void>;
reset: () => Promise<void>;
wrap<T>(key: string, fn: () => Promise<T>, ttl?: WrapTTL<T>): Promise<T>;
wrap<T>(key: string, fn: () => Promise<T>, ttl?: WrapTTL<T>, refreshThreshold?: Milliseconds): Promise<T>;
store: S;
};

Expand Down Expand Up @@ -101,18 +101,19 @@ export function createCache<S extends Store, C extends Config>(
* const result = await cache.wrap('key', () => Promise.resolve(1));
*
*/
wrap: async <T>(key: string, fn: () => Promise<T>, ttl?: WrapTTL<T>) => {
wrap: async <T>(key: string, fn: () => Promise<T>, ttl?: WrapTTL<T>, refreshThreshold?: Milliseconds) => {
const refreshThresholdConfig = refreshThreshold || args?.refreshThreshold || 0;
return coalesceAsync(key, async () => {
const value = await store.get<T>(key);
if (value === undefined) {
const result = await fn();
const cacheTTL = typeof ttl === 'function' ? ttl(result) : ttl;
await store.set<T>(key, result, cacheTTL);
return result;
} else if (args?.refreshThreshold) {
} else if (refreshThresholdConfig) {
const cacheTTL = typeof ttl === 'function' ? ttl(value) : ttl;
const remainingTtl = await store.ttl(key);
if (remainingTtl !== -1 && remainingTtl < args.refreshThreshold) {
if (remainingTtl !== -1 && remainingTtl < refreshThresholdConfig) {
coalesceAsync(`+++${key}`, fn).then((result) =>
store.set<T>(key, result, cacheTTL),
);
Expand Down
3 changes: 2 additions & 1 deletion src/multi-caching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function multiCaching<Caches extends Cache[]>(
key: string,
fn: () => Promise<T>,
ttl?: WrapTTL<T>,
refreshThreshold?: Milliseconds
): Promise<T> {
let value: T | undefined;
let i = 0;
Expand All @@ -54,7 +55,7 @@ export function multiCaching<Caches extends Cache[]>(
Promise.all(
caches.slice(0, i).map((cache) => cache.set(key, value, cacheTTL)),
).then();
caches[i].wrap(key, fn, ttl).then(); // call wrap for store for internal refreshThreshold logic, see: src/caching.ts caching.wrap
caches[i].wrap(key, fn, ttl, refreshThreshold).then(); // call wrap for store for internal refreshThreshold logic, see: src/caching.ts caching.wrap
}
return value;
},
Expand Down
45 changes: 32 additions & 13 deletions test/caching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,17 +372,11 @@ describe('caching', () => {
});

await cache.wrap('refreshThreshold', async () => 0);
await new Promise((resolve) => {
setTimeout(resolve, 2 * 1000);
});
await sleep(2 * 1000);
await cache.wrap('refreshThreshold', async () => 1);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await sleep(500);
await cache.wrap('refreshThreshold', async () => 2);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await sleep(500);
return cache.wrap('refreshThreshold', async () => 3);
})(),
).resolves.toEqual(1);
Expand All @@ -404,26 +398,51 @@ describe('caching', () => {
resolve(value);
}, timeout),
);
const delay = (timeout: number) =>
new Promise((resolve) => setTimeout(resolve, timeout));

let value = await cache.wrap(key, resolveAfter(100, 1));
expect(value).toEqual(1);
expect(callCount).toEqual(1);

await delay(1100);
await sleep(1100);
for (let i = 0; i < 6; i++) {
// Only the first fn should be called - returning 2
value = await cache.wrap(key, resolveAfter(2000, 2 + i));
expect(value).toEqual(1);
expect(callCount).toEqual(1);
}

await delay(2100);
await sleep(2100);
value = await cache.wrap(key, resolveAfter(2000, 8));
expect(value).toEqual(2);
expect(callCount).toEqual(2);
});

it('should allow dynamic refreshThreshold on wrap function', async () => {
cache = await caching('memory', {
ttl: 2 * 1000,
refreshThreshold: 1 * 1000,
});

// Without override params

// 1st call should be cached
expect(await cache.wrap('refreshThreshold', async () => 0)).toEqual(0);
await sleep(1001);
// background refresh, but stale value returned
expect(await cache.wrap('refreshThreshold', async () => 1)).toEqual(0);
// New value in cache
expect(await cache.wrap('refreshThreshold', async () => 2)).toEqual(1);

// With override params

await sleep(1001);
// No background refresh with the new override params
expect(await cache.wrap('refreshThreshold', async () => 3, undefined, 500)).toEqual(1);
await sleep(500);
// Background refresh, but stale value returned
expect(await cache.wrap('refreshThreshold', async () => 4, undefined, 500)).toEqual(1);
expect(await cache.wrap('refreshThreshold', async () => 5, undefined, 500)).toEqual(4);
});
});

describe('createCache', () => {
Expand Down
12 changes: 3 additions & 9 deletions test/multi-caching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,17 +269,11 @@ describe('multiCaching', () => {
const multi = multiCaching([cache0, cache1]);

await multi.wrap('refreshThreshold', async () => 0);
await new Promise((resolve) => {
setTimeout(resolve, 2 * 1000);
});
await sleep(2 * 1000);
await multi.wrap('refreshThreshold', async () => 1);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await sleep(500);
await multi.wrap('refreshThreshold', async () => 2);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await sleep(500);
return multi.wrap('refreshThreshold', async () => 3);
})(),
).resolves.toEqual(1);
Expand Down

0 comments on commit 72c0d4a

Please sign in to comment.