diff --git a/packages/sdk/src/utils/CachingMap.ts b/packages/sdk/src/utils/CachingMap.ts deleted file mode 100644 index 8952e12f95..0000000000 --- a/packages/sdk/src/utils/CachingMap.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { MapKey } from '@streamr/utils' -import pMemoize from 'p-memoize' -import LRU from '../../vendor/quick-lru' - -interface Options { - maxSize: number - maxAge: number - cacheKey: (args: P) => K -} - -/** - * Caches into a LRU cache capped at options.maxSize. See documentation for mem/p-memoize. - * Won't call asyncFn again until options.maxAge or options.maxSize exceeded, or cachedAsyncFn.invalidate() is called. - * Won't cache rejections. - * - * ```js - * const cache = new CachingMap(asyncFn, opts) - * await cache.get(key) - * await cache.get(key) - * cache.invalidate(() => ...) - * ``` - */ -export class CachingMap { - - private readonly cachedFn: (...args: P) => Promise - private readonly cache: LRU - private readonly opts: Options - - constructor( - asyncFn: (...args: P) => Promise, - opts: Options - ) { - this.cache = new LRU({ - maxSize: opts.maxSize, - maxAge: opts.maxAge - }) - this.cachedFn = pMemoize(asyncFn, { - cachePromiseRejection: false, - cache: this.cache, - cacheKey: opts.cacheKey - }) - this.opts = opts - } - - get(...args: P): Promise { - return this.cachedFn(...args) - } - - set(args: P, value: V): void { - this.cache.set(this.opts.cacheKey(args), { data: value, maxAge: this.opts.maxAge }) - } - - invalidate(predicate: (key: K) => boolean): void { - for (const key of this.cache.keys()) { - if (predicate(key)) { - this.cache.delete(key) - } - } - } -} diff --git a/packages/sdk/test/unit/CachingMap.test.ts b/packages/sdk/test/unit/CachingMap.test.ts deleted file mode 100644 index 271952cb45..0000000000 --- a/packages/sdk/test/unit/CachingMap.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { CachingMap } from '../../src/utils/CachingMap' -import { wait } from '@streamr/utils' - -describe('CachingMap', () => { - - let plainFn: jest.Mock, [key1: string, key2: string]> - let cache: CachingMap - - beforeEach(() => { - plainFn = jest.fn() - plainFn.mockImplementation(async (key1: string, key2: string) => { - await wait(100) - return `${key1}${key2}`.toUpperCase() - }) - cache = new CachingMap(plainFn as any, { - maxSize: 10000, - maxAge: 30 * 60 * 1000, - cacheKey: ([key1, key2]) => `${key1};${key2}` - }) - }) - - it('happy path', async () => { - const result1 = await cache.get('foo', 'bar') - const result2 = await cache.get('foo', 'bar') - expect(result1).toBe('FOOBAR') - expect(result2).toBe('FOOBAR') - expect(plainFn).toHaveBeenCalledTimes(1) - }) - - it('miss', async () => { - const result1 = await cache.get('foo', 'x') - const result2 = await cache.get('foo', 'y') - expect(result1).toBe('FOOX') - expect(result2).toBe('FOOY') - expect(plainFn).toHaveBeenCalledTimes(2) - }) - - it('concurrency', async () => { - const [result1, result2] = await Promise.all([ - cache.get('foo', 'bar'), - cache.get('foo', 'bar') - ]) - expect(result1).toBe('FOOBAR') - expect(result2).toBe('FOOBAR') - expect(plainFn).toHaveBeenCalledTimes(1) - }) - - it('rejections are not cached', async () => { - plainFn.mockImplementation(async (key1: string, key2: string) => { - throw new Error(`error ${key1}-${key2}`) - }) - await expect(cache.get('foo', 'x')).rejects.toEqual(new Error('error foo-x')) - await expect(cache.get('foo', 'x')).rejects.toEqual(new Error('error foo-x')) - - expect(plainFn).toHaveBeenCalledTimes(2) // would be 1 if rejections were cached - }) - - it('throws are not cached', async () => { - plainFn.mockImplementation((key1: string, key2: string) => { - throw new Error(`error ${key1}-${key2}`) - }) - await expect(cache.get('foo', 'x')).rejects.toEqual(new Error('error foo-x')) - await expect(cache.get('foo', 'x')).rejects.toEqual(new Error('error foo-x')) - - expect(plainFn).toHaveBeenCalledTimes(2) // would be 1 if throws were cached - }) -}) diff --git a/packages/sdk/test/unit/CachingMap2.test.ts b/packages/sdk/test/unit/CachingMap2.test.ts deleted file mode 100644 index 1493c97368..0000000000 --- a/packages/sdk/test/unit/CachingMap2.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { wait } from '@streamr/utils' -import { CachingMap } from '../../src/utils/CachingMap' - -const DEFAULT_OPTS = { - maxSize: 10000, - maxAge: 30 * 60 * 1000, - cacheKey: (args: any[]) => args[0] -} - -describe('CachingMap', () => { - it('caches & be cleared', async () => { - const fn = jest.fn() - const cache = new CachingMap(fn, DEFAULT_OPTS) - await cache.get() - expect(fn).toHaveBeenCalledTimes(1) - await cache.get() - expect(fn).toHaveBeenCalledTimes(1) - await cache.get(1) - expect(fn).toHaveBeenCalledTimes(2) - await cache.get(1) - expect(fn).toHaveBeenCalledTimes(2) - await cache.get(2) - expect(fn).toHaveBeenCalledTimes(3) - await cache.get(1) - expect(fn).toHaveBeenCalledTimes(3) - await cache.get(2) - expect(fn).toHaveBeenCalledTimes(3) - cache.invalidate((v) => v === 1) - await cache.get(1) - expect(fn).toHaveBeenCalledTimes(4) - cache.invalidate((v) => v === 1) - await cache.get(1) - expect(fn).toHaveBeenCalledTimes(5) - }) - - it('adopts type of wrapped function', async () => { - // actually checking via ts-expect-error - // assertions don't matter, - async function fn(_s: string): Promise { - return 3 - } - - const cache = new CachingMap(fn, DEFAULT_OPTS) - const a: number = await cache.get('abc') // ok - expect(a).toEqual(3) - // @ts-expect-error not enough args - await cache.get() - // @ts-expect-error too many args - await cache.get('abc', 3) - // @ts-expect-error wrong argument type - await cache.get(3) - - // @ts-expect-error wrong return type - const c: string = await cache.get('abc') - expect(c).toEqual(3) - cache.invalidate((_d: string) => true) - const cache2 = new CachingMap(fn, { - ...DEFAULT_OPTS, - cacheKey: ([s]) => { - return s.length - } - }) - - cache2.invalidate((_d: number) => true) - }) - - it('does memoize consecutive calls', async () => { - let i = 0 - const fn = async () => { - i += 1 - return i - } - const memoized = new CachingMap(fn, DEFAULT_OPTS) - const firstCall = memoized.get() - const secondCall = memoized.get() - - expect(await Promise.all([firstCall, secondCall])).toEqual([1, 1]) - }) - - it('can not be executed in parallel', async () => { - const taskId1 = '0xbe406f5e1b7e951cd8e42ab28598671e5b73c3dd/test/75712/Encryption-0' - const taskId2 = 'd/e/f' - const calledWith: string[] = [] - const fn = jest.fn(async (key: string) => { - calledWith.push(key) - await wait(100) - return key - }) - - const cache = new CachingMap(fn, { - maxSize: 10000, - maxAge: 1800000, - cacheKey: ([v]) => { - return v - } - }) - const task = Promise.all([ - cache.get(taskId1), - cache.get(taskId2), - cache.get(taskId1), - cache.get(taskId2), - ]) - task.catch(() => {}) - setImmediate(() => { - cache.get(taskId1) - cache.get(taskId1) - cache.get(taskId2) - cache.get(taskId2) - }) - process.nextTick(() => { - cache.get(taskId1) - cache.get(taskId2) - cache.get(taskId1) - cache.get(taskId2) - }) - setTimeout(() => { - cache.get(taskId1) - cache.get(taskId1) - cache.get(taskId2) - cache.get(taskId2) - }) - await wait(10) - cache.get(taskId2) - cache.get(taskId2) - cache.get(taskId1) - cache.get(taskId1) - await Promise.all([ - cache.get(taskId1), - cache.get(taskId2), - cache.get(taskId1), - cache.get(taskId2), - ]) - await task - expect(fn).toHaveBeenCalledTimes(2) - expect(calledWith).toEqual([taskId1, taskId2]) - await wait(200) - expect(fn).toHaveBeenCalledTimes(2) - expect(calledWith).toEqual([taskId1, taskId2]) - }) -})