diff --git a/packages/sdk/src/utils/Mapping.ts b/packages/sdk/src/utils/Mapping.ts index 1018c65b76..3c81246ece 100644 --- a/packages/sdk/src/utils/Mapping.ts +++ b/packages/sdk/src/utils/Mapping.ts @@ -1,10 +1,12 @@ import { formLookupKey } from './utils' import LRU from '../../vendor/quick-lru' +import { MarkRequired } from 'ts-essentials' type KeyType = (string | number)[] interface BaseOptions { valueFactory: (...args: K) => Promise + isCacheableValue?: (value: V) => boolean } interface CacheMapOptions extends BaseOptions { @@ -23,12 +25,20 @@ interface ValueWrapper { * A map that lazily creates values. The factory function is called only when a key * is accessed for the first time. Subsequent calls to `get()` return the cached value * unless it has been evicted due to `maxSize` or `maxAge` limits. + * + * It is possible to implement e.g. positive cache by using `isCacheableValue()` + * config option. If that method returns `false`, the value is not stored to cache. + * Note that using this option doesn't change the concurrent promise handling: + * also in this case all concurrent `get()` calls are grouped so that only one + * call to `valueFactory` is made. (If we wouldn't group these calls, all concurrent + * `get()` calls were cache misses, i.e. affecting significantly cases where the + * `isCacheableValue()` returns `true`.) */ export class Mapping { private readonly delegate: Map> private readonly pendingPromises: Map> = new Map() - private readonly opts: CacheMapOptions | LazyMapOptions + private readonly opts: MarkRequired | LazyMapOptions, 'isCacheableValue'> /** * Prefer constructing the class via createCacheMap() and createLazyMap() @@ -44,7 +54,10 @@ export class Mapping { } else { this.delegate = new Map>() } - this.opts = opts + this.opts = { + isCacheableValue: () => true, + ...opts + } } async get(...args: K): Promise { @@ -64,7 +77,9 @@ export class Mapping { this.pendingPromises.delete(key) } valueWrapper = { value } - this.delegate.set(key, valueWrapper) + if (this.opts.isCacheableValue(value)) { + this.delegate.set(key, valueWrapper) + } } return valueWrapper.value } diff --git a/packages/sdk/test/unit/Mapping.test.ts b/packages/sdk/test/unit/Mapping.test.ts index 60ab14aaae..b2d40730a5 100644 --- a/packages/sdk/test/unit/Mapping.test.ts +++ b/packages/sdk/test/unit/Mapping.test.ts @@ -56,6 +56,23 @@ describe('Mapping', () => { expect(valueFactory).toHaveBeenCalledTimes(2) }) + it('isCacheableValue', async () => { + const valueFactory = jest.fn().mockImplementation(async (p1: string, p2: number) => { + return `${p1}${p2}` + }) + const mapping = createLazyMap({ valueFactory, isCacheableValue: (value: string) => value === 'foo1' }) + const result1 = await mapping.get('foo', 1) + const result2 = await mapping.get('foo', 1) + expect(result1).toBe('foo1') + expect(result2).toBe('foo1') + expect(valueFactory).toHaveBeenCalledTimes(1) + const result3 = await mapping.get('foo', 2) + const result4 = await mapping.get('foo', 2) + expect(result3).toBe('foo2') + expect(result4).toBe('foo2') + expect(valueFactory).toHaveBeenCalledTimes(1 + 2) // two additional calls as neither of the new calls was cached + }) + it('concurrency', async () => { const valueFactory = jest.fn().mockImplementation(async (p1: string, p2: number) => { await wait(50)