Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ REPORT_NOTIFY_URL=http://mock.com/

# Url for connecting to Redis
REDIS_URL=redis://localhost:6379

# Disable memoization in tests
MEMOIZATION_TTL=-1
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ module.exports = {
setupFiles: [ './jest.setup.js' ],

setupFilesAfterEnv: ['./jest.setup.redis-mock.js', './jest.setup.mongo-repl-set.js'],

globalTeardown: './jest.global-teardown.js',
};
2 changes: 1 addition & 1 deletion jest.global-teardown.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ module.exports = () => {
process.exit(0);
}, 1000);
}
}
};
112 changes: 106 additions & 6 deletions lib/memoize/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* Ignore eslint unused vars rule for decorator
*/

import { memoize } from './index';

Check warning on line 12 in lib/memoize/index.test.ts

View workflow job for this annotation

GitHub Actions / ESlint

'memoize' is defined but never used
import Crypto from '../utils/crypto';

describe('memoize decorator — per-test inline classes', () => {
Expand All @@ -26,6 +26,7 @@
@memoize({ strategy: 'concat', ttl: 60_000, max: 50 })
public async run(a: number, b: string) {
this.calls += 1;

return `${a}-${b}`;
}
}
Expand All @@ -37,7 +38,7 @@
*/
expect(await sample.run(1, 'x')).toBe('1-x');
/**
* In this case
* In this case
*/
expect(await sample.run(1, 'x')).toBe('1-x');
expect(await sample.run(1, 'x')).toBe('1-x');
Expand All @@ -52,6 +53,7 @@
@memoize({ strategy: 'concat' })
public async run(a: unknown, b: unknown) {
this.calls += 1;

return `${String(a)}|${String(b)}`;
}
}
Expand Down Expand Up @@ -84,9 +86,11 @@
it('should memoize return value for stringified objects across several calls', async () => {
class Sample {
public calls = 0;

@memoize({ strategy: 'concat' })
public async run(x: unknown, y: unknown) {
this.calls += 1;

return 'ok';
}
}
Expand All @@ -103,9 +107,11 @@
it('should memoize return value for method with non-default arguments (NaN, Infinity, -0, Symbol, Date, RegExp) still cache same-args', async () => {
class Sample {
public calls = 0;

@memoize({ strategy: 'concat' })
public async run(...args: unknown[]) {
this.calls += 1;

return args.map(String).join(',');
}
}
Expand All @@ -127,27 +133,31 @@

class Sample {
public calls = 0;

@memoize({ strategy: 'hash' })
public async run(...args: unknown[]) {
this.calls += 1;

return 'ok';
}
}
const sample = new Sample();

await sample.run({a: 1}, undefined, 0);
await sample.run({a: 1}, undefined, 0);
await sample.run({ a: 1 }, undefined, 0);
await sample.run({ a: 1 }, undefined, 0);

expect(hashSpy).toHaveBeenCalledWith([{a: 1}, undefined, 0], 'blake2b512', 'base64url');
expect(hashSpy).toHaveBeenCalledWith([ { a: 1 }, undefined, 0], 'blake2b512', 'base64url');
expect(sample.calls).toBe(1);
});

it('should not memoize return value with hash strategy and different arguments', async () => {
class Sample {
public calls = 0;

@memoize({ strategy: 'hash' })
public async run(...args: unknown[]) {
this.calls += 1;

return 'ok';
}
}
Expand All @@ -163,9 +173,11 @@
it('should memoize return value with hash strategy across several calls with same args', async () => {
class Sample {
public calls = 0;

@memoize({ strategy: 'hash' })
public async run(arg: unknown) {
this.calls += 1;

return 'ok';
}
}
Expand All @@ -186,9 +198,11 @@

class Sample {
public calls = 0;

@memoizeWithMockedTimers({ strategy: 'concat', ttl: 1_000 })
public async run(x: string) {
this.calls += 1;

return x;
}
}
Expand All @@ -204,16 +218,19 @@

await sample.run('k1');
expect(sample.calls).toBe(2);

});

it('error calls should never be momized', async () => {
class Sample {
public calls = 0;

@memoize()
public async run(x: number) {
this.calls += 1;
if (x === 1) throw new Error('boom');
if (x === 1) {
throw new Error('boom');
}

return x * 2;
}
}
Expand All @@ -226,4 +243,87 @@
await expect(sample.run(1)).rejects.toThrow('boom');
expect(sample.calls).toBe(2);
});

it('should NOT cache results listed in skipCache (primitives)', async () => {
class Sample {
public calls = 0;

@memoize({ strategy: 'concat', skipCache: [null, undefined, 0, false, ''] })
public async run(kind: 'null' | 'undef' | 'zero' | 'false' | 'empty') {
this.calls += 1;
switch (kind) {
case 'null': return null;
case 'undef': return undefined;
case 'zero': return 0;
case 'false': return false;
case 'empty': return '';
}
}
}

const sample = new Sample();

// Each repeated call should invoke the original again because result is in skipCache.
await sample.run('null');
await sample.run('null');

await sample.run('undef');
await sample.run('undef');

await sample.run('zero');
await sample.run('zero');

await sample.run('false');
await sample.run('false');

await sample.run('empty');
await sample.run('empty');

// 5 kinds × 2 calls each = 10 calls, none cached
expect(sample.calls).toBe(10);
});

it('should cache results NOT listed in skipCache', async () => {
class Sample {
public calls = 0;

@memoize({ strategy: 'concat', skipCache: [null, undefined] })
public async run(x: number) {
this.calls += 1;
// returns a non-skipped primitive
return x * 2;
}
}

const sample = new Sample();

expect(await sample.run(21)).toBe(42);
expect(await sample.run(21)).toBe(42);

expect(sample.calls).toBe(1);
});

it('should use equality for skipCache with objects: deep equal objects are cached', async () => {
const deepEqualObject = { a: 1 };

class Sample {
public calls = 0;

@memoize({ strategy: 'concat', skipCache: [deepEqualObject] })
public async run() {
this.calls += 1;

return { a: 1 };
}
}

const sample = new Sample();

const first = await sample.run();
const second = await sample.run();

expect(first).toEqual({ a: 1 });
expect(second).toBe(first);
expect(sample.calls).toBe(1);
});
});
10 changes: 9 additions & 1 deletion lib/memoize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface MemoizeOptions {
* Strategy for key generation
*/
strategy?: MemoizeKeyStrategy;

/**
* It allows to skip caching for list of return values specified
*/
skipCache?: any[]
}

/**
Expand All @@ -40,6 +45,7 @@ export function memoize(options: MemoizeOptions = {}): MethodDecorator {
max = 50,
ttl = 1000 * 60 * 30,
strategy = 'concat',
skipCache = []
} = options;
/* eslint-enable */

Expand Down Expand Up @@ -84,7 +90,9 @@ export function memoize(options: MemoizeOptions = {}): MethodDecorator {
try {
const result = await originalMethod.apply(this, args);

cache.set(key, result);
if (!skipCache.includes(result)) {
cache.set(key, result);
}

return result;
} catch (err) {
Expand Down
Loading
Loading