diff --git a/index.ts b/index.ts index c5c71d0..67d861d 100644 --- a/index.ts +++ b/index.ts @@ -58,12 +58,14 @@ export type Options< readonly cacheKey?: (arguments_: Parameters) => CacheKeyType; /** - Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache. To disable caching so that only concurrent executions resolve with the same value, pass `false`. + Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache. + + To disable caching so that only concurrent executions resolve with the same value, pass `false` or `'while-pending'`. @default new Map() @example new WeakMap() */ - readonly cache?: CacheStorage> | false; + readonly cache?: CacheStorage> | false | 'while-pending'; /** Controls whether a fulfilled value should be written to the cache. @@ -137,6 +139,9 @@ export default function pMemoize< cache = new Map>(), } = options ?? {}; + // Normalize 'while-pending' to false for internal use + const normalizedCache = cache === 'while-pending' ? false : cache; + // Promise objects can't be serialized so we keep track of them internally and only provide their resolved values to `cache` // `Promise>` is used instead of `ReturnType` because promise properties are not kept const promiseCache = new Map>>(); @@ -150,8 +155,8 @@ export default function pMemoize< const promise = (async () => { try { - if (cache && await cache.has(key)) { - return (await cache.get(key)) as AsyncReturnType; + if (normalizedCache && await normalizedCache.has(key)) { + return (await normalizedCache.get(key)) as AsyncReturnType; } const promise = fn.apply(this, arguments_) as Promise>; @@ -161,13 +166,13 @@ export default function pMemoize< try { return result; } finally { - if (cache) { + if (normalizedCache) { const allow = options?.shouldCache ? await options.shouldCache(result, {key, argumentsList: arguments_}) : true; if (allow) { - await cache.set(key, result); + await normalizedCache.set(key, result); } } } @@ -185,7 +190,7 @@ export default function pMemoize< ignoreNonConfigurable: true, }); - cacheStore.set(memoized, cache); + cacheStore.set(memoized, normalizedCache); return memoized; } diff --git a/readme.md b/readme.md index 531ad21..d72f53e 100644 --- a/readme.md +++ b/readme.md @@ -68,10 +68,12 @@ See the [caching strategy](#caching-strategy) section for more information. ##### cache -Type: `object | false`\ +Type: `object | false | 'while-pending'`\ Default: `new Map()` -Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache. To disable caching so that only concurrent executions resolve with the same value, pass `false`. +Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache. + +To disable caching so that only concurrent executions resolve with the same value, pass `false` or `'while-pending'`. See the [caching strategy](https://github.com/sindresorhus/mem#caching-strategy) section in the `mem` package for more information. diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index d419cb0..1ec34e3 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -12,6 +12,7 @@ expectType(pMemoize(fn, { })); expectType(pMemoize(fn, {cache: new Map()})); expectType(pMemoize(fn, {cache: false})); +expectType(pMemoize(fn, {cache: 'while-pending'})); /* Overloaded function tests */ async function overloadedFn(parameter: false): Promise; diff --git a/test.ts b/test.ts index 0f191f8..2ffe504 100644 --- a/test.ts +++ b/test.ts @@ -207,6 +207,17 @@ test('disables caching', async t => { t.deepEqual(await Promise.all([memoized(), memoized()]), [3, 3]); }); +test('cache: "while-pending" disables caching but deduplicates in-flight requests', async t => { + let index = 0; + + const memoized = pMemoize(async () => index++, {cache: 'while-pending'}); + + t.is(await memoized(), 0); + t.is(await memoized(), 1); + t.is(await memoized(), 2); + t.deepEqual(await Promise.all([memoized(), memoized()]), [3, 3]); +}); + test('.pMemoizeClear()', async t => { let index = 0; const fixture = async () => index++; @@ -297,6 +308,20 @@ test('.pMemoizeClear() throws when called on a disabled cache', t => { }); }); +test('.pMemoizeClear() throws when called on a while-pending cache', t => { + const fixture = async () => 1; + const memoized = pMemoize(fixture, { + cache: 'while-pending', + }); + + t.throws(() => { + pMemoizeClear(memoized); + }, { + message: 'Can\'t clear a function that doesn\'t use a cache!', + instanceOf: TypeError, + }); +}); + test.serial('shouldCache: skips cache.set when predicate false', async t => { let calls = 0; const cache = new Map();