diff --git a/README.md b/README.md index d4b569f2..efde2c8d 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/src/caching.ts b/src/caching.ts index 5efa9863..7bdd5366 100644 --- a/src/caching.ts +++ b/src/caching.ts @@ -43,7 +43,7 @@ export type Cache = { get: (key: string) => Promise; del: (key: string) => Promise; reset: () => Promise; - wrap(key: string, fn: () => Promise, ttl?: WrapTTL): Promise; + wrap(key: string, fn: () => Promise, ttl?: WrapTTL, refreshThreshold?: Milliseconds): Promise; store: S; }; @@ -101,7 +101,8 @@ export function createCache( * const result = await cache.wrap('key', () => Promise.resolve(1)); * */ - wrap: async (key: string, fn: () => Promise, ttl?: WrapTTL) => { + wrap: async (key: string, fn: () => Promise, ttl?: WrapTTL, refreshThreshold?: Milliseconds) => { + const refreshThresholdConfig = refreshThreshold || args?.refreshThreshold || 0; return coalesceAsync(key, async () => { const value = await store.get(key); if (value === undefined) { @@ -109,10 +110,10 @@ export function createCache( const cacheTTL = typeof ttl === 'function' ? ttl(result) : ttl; await store.set(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(key, result, cacheTTL), ); diff --git a/src/multi-caching.ts b/src/multi-caching.ts index 0667e232..f0b5069b 100644 --- a/src/multi-caching.ts +++ b/src/multi-caching.ts @@ -35,6 +35,7 @@ export function multiCaching( key: string, fn: () => Promise, ttl?: WrapTTL, + refreshThreshold?: Milliseconds ): Promise { let value: T | undefined; let i = 0; @@ -54,7 +55,7 @@ export function multiCaching( 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; }, diff --git a/test/caching.test.ts b/test/caching.test.ts index 1e8cf851..5cfd7a75 100644 --- a/test/caching.test.ts +++ b/test/caching.test.ts @@ -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); @@ -404,14 +398,12 @@ 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)); @@ -419,11 +411,38 @@ describe('caching', () => { 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', () => { diff --git a/test/multi-caching.test.ts b/test/multi-caching.test.ts index eca4efb3..f7c87052 100644 --- a/test/multi-caching.test.ts +++ b/test/multi-caching.test.ts @@ -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);