From 4cc3972cba42d0ec420c7508c358c90d6294b5df Mon Sep 17 00:00:00 2001 From: Ruben Deyhle Date: Tue, 21 Mar 2023 03:55:04 +0100 Subject: [PATCH] feat: add mget(), mset(), mdel() to multiCache (#367) --- README.md | 23 ++++++++++++++++++ src/multi-caching.ts | 25 ++++++++++++++++++- test/multi-caching.test.ts | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7802b1d6..345d91b9 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,29 @@ console.log(await multiCache.get('foo2')); // Delete from all caches await multiCache.del('foo2'); + +// Sets multiple keys in all caches. +// You can pass as many key, value tuples as you want +await multiCache.mset( + [ + ['foo', 'bar'], + ['foo2', 'bar2'], + ], + ttl +); + +// mget() fetches from highest priority cache. +// If the first cache does not return all the keys, +// the next cache is fetched with the keys that were not found. +// This is done recursively until either: +// - all have been found +// - all caches has been fetched +console.log(await multiCache.mget('key', 'key2'); +// >> ['bar', 'bar2'] + +// Delete keys with mdel() passing arguments... +await multiCache.mdel('foo', 'foo2'); + ``` See unit tests in [`test/multi-caching.test.ts`](./test/multi-caching.test.ts) for more information. diff --git a/src/multi-caching.ts b/src/multi-caching.ts index 57b86936..19a7ec33 100644 --- a/src/multi-caching.ts +++ b/src/multi-caching.ts @@ -1,6 +1,10 @@ import { Cache, Milliseconds } from './caching'; -export type MultiCache = Omit; +export type MultiCache = Omit & { + mset(args: [string, unknown][], ttl?: Milliseconds): Promise; + mget(...args: string[]): Promise; + mdel(...args: string[]): Promise; +}; /** * Module that lets you specify a hierarchy of caches. @@ -56,5 +60,24 @@ export function multiCaching( reset: async () => { await Promise.all(caches.map((x) => x.reset())); }, + mget: async (...keys: string[]) => { + const values = new Array(keys.length).fill(undefined); + for (const cache of caches) { + if (values.every((x) => x !== undefined)) break; + try { + const val = await cache.store.mget(...keys); + val.forEach((v, i) => { + if (values[i] === undefined && v !== undefined) values[i] = v; + }); + } catch (e) {} + } + return values; + }, + mset: async (args: [string, unknown][], ttl?: Milliseconds) => { + await Promise.all(caches.map((cache) => cache.store.mset(args, ttl))); + }, + mdel: async (...keys: string[]) => { + await Promise.all(caches.map((cache) => cache.store.mdel(...keys))); + }, }; } diff --git a/test/multi-caching.test.ts b/test/multi-caching.test.ts index ba39958a..8bdab292 100644 --- a/test/multi-caching.test.ts +++ b/test/multi-caching.test.ts @@ -91,6 +91,56 @@ describe('multiCaching', () => { }); }); + describe('mset()', () => { + it('lets us set multiple keys in all caches', async () => { + const keys = [faker.datatype.string(20), faker.datatype.string(20)]; + const values = [faker.datatype.string(), faker.datatype.string()]; + await multiCache.mset( + [ + [keys[0], values[0]], + [keys[1], values[1]], + ], + defaultTtl, + ); + await expect(memoryCache.get(keys[0])).resolves.toEqual(values[0]); + await expect(memoryCache2.get(keys[0])).resolves.toEqual(values[0]); + await expect(memoryCache3.get(keys[0])).resolves.toEqual(values[0]); + await expect(memoryCache.get(keys[1])).resolves.toEqual(values[1]); + await expect(memoryCache2.get(keys[1])).resolves.toEqual(values[1]); + await expect(memoryCache3.get(keys[1])).resolves.toEqual(values[1]); + }); + }); + + describe('mget()', () => { + it('lets us get multiple keys', async () => { + const keys = [faker.datatype.string(20), faker.datatype.string(20)]; + const values = [faker.datatype.string(), faker.datatype.string()]; + await multiCache.set(keys[0], values[0], defaultTtl); + await memoryCache3.set(keys[1], values[1], defaultTtl); + await expect(multiCache.mget(...keys)).resolves.toStrictEqual(values); + }); + }); + + describe('mdel()', () => { + it('lets us delete multiple keys', async () => { + const keys = [faker.datatype.string(20), faker.datatype.string(20)]; + const values = [faker.datatype.string(), faker.datatype.string()]; + await multiCache.mset( + [ + [keys[0], values[0]], + [keys[1], values[1]], + ], + defaultTtl, + ); + await expect(multiCache.mget(...keys)).resolves.toStrictEqual(values); + await multiCache.mdel(...keys); + await expect(multiCache.mget(...keys)).resolves.toStrictEqual([ + undefined, + undefined, + ]); + }); + }); + describe('when cache fails', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function const empty = (async () => {}) as never;