Skip to content

Commit 9e36aec

Browse files
tassoevanguijun13
andauthored
feat(memo): Cache timeout (#630)
* Add base tests for memoize and clear * feat: create timedMemoize function * feat: add reset timer functionality * refactor: join not timed and timed memoize funcs * refactor: dry timed functions to only one * style: remove comments Co-authored-by: guijun13 <guilhermejun13.grillo@gmail.com>
1 parent 46a9b9e commit 9e36aec

9 files changed

+174
-84
lines changed

packages/memo/.eslintrc.js

-6
This file was deleted.

packages/memo/.prettierrc.js

-1
This file was deleted.

packages/memo/CHANGELOG.md

-54
This file was deleted.

packages/memo/package.json

+28-6
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,50 @@
2929
"/dist"
3030
],
3131
"scripts": {
32-
"build": "run-s .:build:clean .:build:esm .:build:cjs",
33-
".:build:clean": "rimraf dist",
32+
"clean": "rimraf dist",
33+
"build": "run .:build:esm && run .:build:cjs",
3434
".:build:esm": "tsc -p tsconfig-esm.json",
3535
".:build:cjs": "tsc -p tsconfig-cjs.json",
3636
"lint": "lint",
3737
"lint-and-fix": "lint-and-fix",
3838
"lint-staged": "lint-staged",
39+
"test": "jest --runInBand",
3940
"docs": "typedoc"
4041
},
4142
"devDependencies": {
4243
"@rocket.chat/eslint-config-alt": "workspace:~",
4344
"@rocket.chat/prettier-config": "workspace:~",
45+
"@types/jest": "~27.4.0",
4446
"eslint": "~8.8.0",
47+
"jest": "~27.5.1",
4548
"lint-all": "workspace:~",
4649
"lint-staged": "~12.3.3",
47-
"npm-run-all": "^4.1.5",
4850
"prettier": "~2.5.1",
49-
"rimraf": "^3.0.2",
51+
"rimraf": "~3.0.2",
52+
"ts-jest": "~27.1.3",
5053
"typedoc": "~0.22.11",
5154
"typescript": "~4.3.5"
5255
},
53-
"dependencies": {
54-
"tslib": "^2.3.1"
56+
"eslintConfig": {
57+
"extends": "@rocket.chat/eslint-config-alt/typescript",
58+
"env": {
59+
"jest": true
60+
}
61+
},
62+
"prettier": "@rocket.chat/prettier-config/fuselage",
63+
"jest": {
64+
"preset": "ts-jest",
65+
"errorOnDeprecated": true,
66+
"testMatch": [
67+
"<rootDir>/src/**/*.spec.ts"
68+
],
69+
"globals": {
70+
"ts-jest": {
71+
"tsconfig": {
72+
"noUnusedLocals": false,
73+
"noUnusedParameters": false
74+
}
75+
}
76+
}
5577
}
5678
}

packages/memo/src/memoize.spec.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { memoize, clear } from './memoize';
2+
3+
it('should memoize a function that takes no parameter', () => {
4+
const fn = jest.fn(() => 'foo');
5+
const memoized = jest.fn(memoize(fn));
6+
7+
memoized(undefined);
8+
memoized(undefined);
9+
10+
expect(memoized).toHaveBeenCalledTimes(2);
11+
expect(fn).toHaveBeenCalledTimes(1);
12+
13+
expect(fn).toHaveReturnedWith('foo');
14+
expect(memoized).toHaveReturnedWith('foo');
15+
});
16+
17+
it('should memoize a function that takes one parameter', () => {
18+
const fn = jest.fn((i: number) => i + 1);
19+
const memoized = jest.fn(memoize(fn));
20+
21+
memoized(5);
22+
memoized(5);
23+
memoized(2);
24+
25+
expect(memoized).toHaveBeenCalledTimes(3);
26+
expect(fn).toHaveBeenCalledTimes(2);
27+
28+
expect(fn).toHaveNthReturnedWith(1, 6);
29+
expect(fn).toHaveNthReturnedWith(2, 3);
30+
31+
expect(memoized).toHaveNthReturnedWith(1, 6);
32+
expect(memoized).toHaveNthReturnedWith(2, 6);
33+
expect(memoized).toHaveNthReturnedWith(3, 3);
34+
});
35+
36+
describe('clear', () => {
37+
it('should discard cached values of a memoized function', () => {
38+
const fn = jest.fn(() => 'foo');
39+
const memoized = memoize(fn);
40+
const spiedMemoized = jest.fn(memoized);
41+
42+
spiedMemoized(undefined);
43+
spiedMemoized(undefined);
44+
clear(memoized);
45+
spiedMemoized(undefined);
46+
47+
expect(spiedMemoized).toHaveBeenCalledTimes(3);
48+
expect(fn).toHaveBeenCalledTimes(2);
49+
50+
expect(fn).toHaveReturnedWith('foo');
51+
expect(spiedMemoized).toHaveReturnedWith('foo');
52+
});
53+
54+
it('should do nothing when a non-memoized function is passed', () => {
55+
const fn = jest.fn(() => 'foo');
56+
57+
expect(() => clear(fn)).not.toThrowError();
58+
});
59+
});
60+
61+
describe('timeout', () => {
62+
it('should memoize a function that takes one parameter and clear after x ms', () => {
63+
jest.useFakeTimers();
64+
65+
const fn = jest.fn((i: number) => i + 1);
66+
const memoized = jest.fn(memoize(fn, { maxAge: 3000 }));
67+
68+
memoized(5);
69+
jest.advanceTimersByTime(2000);
70+
memoized(5);
71+
jest.advanceTimersByTime(2000);
72+
memoized(5);
73+
jest.advanceTimersByTime(3000);
74+
memoized(5);
75+
76+
expect(fn).toHaveBeenCalledTimes(2);
77+
78+
expect(memoized).toHaveNthReturnedWith(1, 6);
79+
expect(memoized).toHaveNthReturnedWith(2, 6);
80+
expect(memoized).toHaveNthReturnedWith(3, 6);
81+
expect(memoized).toHaveNthReturnedWith(4, 6);
82+
});
83+
84+
it('should memoize a function caching for two parameters and clearing both after x ms each one', () => {
85+
jest.useFakeTimers();
86+
87+
const fn = jest.fn((i: number) => i + 1);
88+
const memoized = jest.fn(memoize(fn, { maxAge: 3000 }));
89+
90+
memoized(5);
91+
jest.advanceTimersByTime(2000);
92+
memoized(6);
93+
jest.advanceTimersByTime(2000);
94+
95+
memoized(6);
96+
memoized(5);
97+
98+
expect(fn).toHaveBeenCalledTimes(3);
99+
100+
expect(memoized).toHaveNthReturnedWith(1, 6);
101+
expect(memoized).toHaveNthReturnedWith(2, 7);
102+
expect(memoized).toHaveNthReturnedWith(3, 7);
103+
expect(memoized).toHaveNthReturnedWith(4, 6);
104+
});
105+
});

packages/memo/src/memoize.ts

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
type MemoizableFunction<T, A, R> = (this: T, arg: A) => R;
22
type MemoizedFunction<T, A, R> = (this: T, arg: A) => R;
33

4+
interface Options {
5+
maxAge: number;
6+
}
7+
48
const store = new WeakMap<
59
MemoizableFunction<unknown, unknown, unknown>,
610
Map<unknown, unknown>
@@ -13,21 +17,43 @@ const isCachedValue = <A, R>(
1317
): cachedValue is R => cache.has(arg) && cache.get(arg) === cachedValue;
1418

1519
export const memoize = <T, A, R>(
16-
fn: MemoizableFunction<T, A, R>
20+
fn: MemoizableFunction<T, A, R>,
21+
_options?: Options
1722
): MemoizedFunction<T, A, R> => {
1823
const cache = new Map<A, R>();
24+
const cacheTimers = new Map<A, ReturnType<typeof setTimeout>>();
1925

2026
const memoized: MemoizedFunction<T, A, R> = function (this, arg) {
27+
const cleanUp = (): void => {
28+
cache.delete(arg);
29+
cacheTimers.delete(arg);
30+
};
31+
2132
const cachedValue = cache.get(arg);
2233

2334
if (isCachedValue(cachedValue, arg, cache)) {
35+
const oldTimer = cacheTimers.get(arg);
36+
if (oldTimer) {
37+
clearTimeout(oldTimer);
38+
}
39+
40+
if (_options) {
41+
const timer = setTimeout(cleanUp, _options.maxAge);
42+
cacheTimers.set(arg, timer);
43+
}
44+
2445
return cachedValue;
2546
}
2647

2748
const result = fn.call(this, arg);
2849

2950
cache.set(arg, result);
3051

52+
if (_options) {
53+
const timer = setTimeout(cleanUp, _options.maxAge);
54+
cacheTimers.set(arg, timer);
55+
}
56+
3157
return result;
3258
};
3359

@@ -37,11 +63,8 @@ export const memoize = <T, A, R>(
3763
};
3864

3965
export const clear = (
40-
fn: MemoizableFunction<unknown, unknown, unknown>
66+
fn: MemoizedFunction<unknown, unknown, unknown>
4167
): void => {
42-
const x = store.get(fn);
43-
44-
if (x) {
45-
x.clear();
46-
}
68+
const cache = store.get(fn);
69+
cache?.clear();
4770
};

packages/memo/tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,9 @@
1212
"forceConsistentCasingInFileNames": true,
1313
"moduleResolution": "node",
1414
"resolveJsonModule": true
15+
},
16+
"typedocOptions": {
17+
"entryPoints": ["src/index.ts"],
18+
"out": "../../static/memo"
1519
}
1620
}

packages/memo/typedoc.json

-4
This file was deleted.

yarn.lock

+7-6
Original file line numberDiff line numberDiff line change
@@ -5784,13 +5784,14 @@ __metadata:
57845784
dependencies:
57855785
"@rocket.chat/eslint-config-alt": "workspace:~"
57865786
"@rocket.chat/prettier-config": "workspace:~"
5787+
"@types/jest": ~27.4.0
57875788
eslint: ~8.8.0
5789+
jest: ~27.5.1
57885790
lint-all: "workspace:~"
57895791
lint-staged: ~12.3.3
5790-
npm-run-all: ^4.1.5
57915792
prettier: ~2.5.1
5792-
rimraf: ^3.0.2
5793-
tslib: ^2.3.1
5793+
rimraf: ~3.0.2
5794+
ts-jest: ~27.1.3
57945795
typedoc: ~0.22.11
57955796
typescript: ~4.3.5
57965797
languageName: unknown
@@ -17462,12 +17463,12 @@ fsevents@~2.1.2:
1746217463
linkType: hard
1746317464

1746417465
"istanbul-reports@npm:^3.1.3":
17465-
version: 3.1.4
17466-
resolution: "istanbul-reports@npm:3.1.4"
17466+
version: 3.1.3
17467+
resolution: "istanbul-reports@npm:3.1.3"
1746717468
dependencies:
1746817469
html-escaper: ^2.0.0
1746917470
istanbul-lib-report: ^3.0.0
17470-
checksum: 2132983355710c522f6b26808015cab9a0ee8b9f5ae0db0d3edeff40b886dd83cb670fb123cb7b32dbe59473d7c00cdde2ba6136bc0acdb20a865fccea64dfe1
17471+
checksum: ef6e0d9ed05ecab1974c6eb46cc2a12d8570911934192db4ed40cf1978449240ea80aae32c4dd5555b67407cdf860212d1a9e415443af69641aa57ed1da5ebbb
1747117472
languageName: node
1747217473
linkType: hard
1747317474

0 commit comments

Comments
 (0)