Skip to content

Commit

Permalink
feat(cache-manager): add refreshAllStores option (close #916) (#917)
Browse files Browse the repository at this point in the history
* feat(cache-manager): add refreshAllStores option

* update test cases

* updates

* update docs

* tweak param order
  • Loading branch information
meteorlxy authored Dec 3, 2024
1 parent 64f4341 commit 7a98aca
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 1 deletion.
3 changes: 3 additions & 0 deletions packages/cache-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ const cache = createCache({ stores: [keyv] });
- **refreshThreshold**?: number - Default refreshThreshold in milliseconds.

If the remaining TTL is less than **refreshThreshold**, the system will update the value asynchronously in background.
- **refreshAllStores**?: boolean - Default false

If set to true, the system will update the value of all stores when the refreshThreshold is met. Otherwise, it will only update from the top to the store that triggered the refresh.

- **nonBlocking**?: boolean - Default false

Expand Down
3 changes: 2 additions & 1 deletion packages/cache-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type CreateCacheOptions = {
stores?: Keyv[];
ttl?: number;
refreshThreshold?: number;
refreshAllStores?: boolean;
nonBlocking?: boolean;
};

Expand Down Expand Up @@ -207,7 +208,7 @@ export const createCache = (options?: CreateCacheOptions) => {
coalesceAsync(`+++${key}`, fnc)
.then(async result => {
try {
await set(stores.slice(0, i + 1), key, result, ms);
await set(options?.refreshAllStores ? stores : stores.slice(0, i + 1), key, result, ms);
eventEmitter.emit('refresh', {key, value: result});
} catch (error) {
eventEmitter.emit('refresh', {key, value, error});
Expand Down
103 changes: 103 additions & 0 deletions packages/cache-manager/test/wrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,106 @@ describe('wrap', () => {
expect(getValue).toBeCalledTimes(2);
});
});

describe('wrap with multi-layer stores', () => {
let keyv1: Keyv;
let keyv2: Keyv;
let cache: ReturnType<typeof createCache>;
const ttl1 = 800;
const ttl2 = 2000;
const refreshThreshold = 500;
const data = {key: '', value: ''};

beforeEach(async () => {
data.key = faker.string.alpha(20);
data.value = faker.string.sample();
keyv1 = new Keyv({ttl: ttl1});
keyv2 = new Keyv({ttl: ttl2});
cache = createCache({refreshThreshold, stores: [keyv1, keyv2]});
});

it('should refresh according to refreshThreshold', async () => {
// 1st call should be cached
expect(await cache.wrap(data.key, async () => 0)).toEqual(0);
expect(await keyv1.get(data.key)).toEqual(0);
expect(await keyv2.get(data.key)).toEqual(0);

// Sleep 501ms, trigger keyv1 refresh
await sleep(501);

// Background refresh, but stale value returned, while keyv1 is already updated
expect(await cache.wrap(data.key, async () => 1)).toEqual(0);
expect(await keyv1.get(data.key)).toEqual(1);
expect(await keyv2.get(data.key)).toEqual(0);

// New value returned
expect(await cache.wrap(data.key, async () => 2)).toEqual(1);
expect(await keyv1.get(data.key)).toEqual(1);
expect(await keyv2.get(data.key)).toEqual(0);

// Sleep 1001ms, keyv1 expired, trigger keyv2 refresh
await sleep(1001);

expect(await keyv1.get(data.key)).toBeUndefined();
expect(await keyv2.get(data.key)).toEqual(0);

// Background refresh, but stale value returned, while keyv1 is already updated
expect(await cache.wrap(data.key, async () => 3)).toEqual(0);
expect(await keyv1.get(data.key)).toEqual(3);
expect(await keyv2.get(data.key)).toEqual(3);

// New value returned
expect(await cache.wrap(data.key, async () => 4)).toEqual(3);
expect(await keyv1.get(data.key)).toEqual(3);
expect(await keyv2.get(data.key)).toEqual(3);
});

it('should respect refreshAllStores', async () => {
cache = createCache({refreshThreshold, refreshAllStores: true, stores: [keyv1, keyv2]});

// 1st call should be cached
expect(await cache.wrap(data.key, async () => 0)).toEqual(0);
expect(await keyv1.get(data.key)).toEqual(0);
expect(await keyv2.get(data.key)).toEqual(0);

// Sleep 501ms, trigger keyv1 refresh
await sleep(501);

// Background refresh, but stale value returned, while keyv1 and keyv2 are all updated
expect(await cache.wrap(data.key, async () => 1)).toEqual(0);
expect(await keyv1.get(data.key)).toEqual(1);
expect(await keyv2.get(data.key)).toEqual(1);

// New value returned
expect(await cache.wrap(data.key, async () => 2)).toEqual(1);
expect(await keyv1.get(data.key)).toEqual(1);
expect(await keyv2.get(data.key)).toEqual(1);

// Sleep 1001ms, keyv1 expired, but keyv2 was refreshed before, so keyv2 will not be refreshed, write back to keyv1 directly
await sleep(1001);

expect(await keyv1.get(data.key)).toBeUndefined();
expect(await keyv2.get(data.key)).toEqual(1);

// No background refresh, write keyv2 value back to keyv1
expect(await cache.wrap(data.key, async () => 3)).toEqual(1);
expect(await keyv1.get(data.key)).toEqual(1);
expect(await keyv2.get(data.key)).toEqual(1);

// Sleep 801ms, keyv1 expired, trigger keyv2 refresh
await sleep(801);

expect(await keyv1.get(data.key)).toBeUndefined();
expect(await keyv2.get(data.key)).toEqual(1);

// Background refresh, but stale value returned, while keyv1 and keyv2 are all updated
expect(await cache.wrap(data.key, async () => 4)).toEqual(1);
expect(await keyv1.get(data.key)).toEqual(4);
expect(await keyv2.get(data.key)).toEqual(4);

// New value returned
expect(await cache.wrap(data.key, async () => 5)).toEqual(4);
expect(await keyv1.get(data.key)).toEqual(4);
expect(await keyv2.get(data.key)).toEqual(4);
});
});

0 comments on commit 7a98aca

Please sign in to comment.