Skip to content

Commit b257f0a

Browse files
committed
feat: added memo()
1 parent 402d7c2 commit b257f0a

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

src/memo.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { AnyFunction } from './types/common.js';
2+
3+
export type CacheKeyFn<Fn extends AnyFunction, ReturnValue = unknown> = (args: Parameters<Fn>) => ReturnValue;
4+
5+
export type DefaultCacheKeyFn<Fn extends AnyFunction> = CacheKeyFn<Fn, NonNullable<Parameters<Fn>[0]> | string>;
6+
7+
export type MemoizedFn<Fn extends AnyFunction, CkFn extends CacheKeyFn<Fn>> = Fn & {
8+
cache: Map<ReturnType<CkFn>, ReturnType<Fn>>;
9+
};
10+
11+
/**
12+
* Memoize a function
13+
*
14+
* @todo use `Map.getOrInsertComputed()` once it is widely available.
15+
*/
16+
export function memo<Fn extends AnyFunction, CkFn extends CacheKeyFn<Fn>>(
17+
fn: Fn,
18+
getCacheKey: CkFn,
19+
): MemoizedFn<Fn, CkFn>;
20+
21+
export function memo<Fn extends AnyFunction>(fn: Fn): MemoizedFn<Fn, DefaultCacheKeyFn<Fn>>;
22+
23+
export function memo<Fn extends AnyFunction, CkFn extends CacheKeyFn<Fn>>(
24+
fn: Fn,
25+
getCacheKey?: CkFn,
26+
): MemoizedFn<Fn, CkFn> {
27+
const defaultGetCacheKey: DefaultCacheKeyFn<Fn> = (args) => args[0] ?? JSON.stringify(args);
28+
29+
const resolvedGetCacheKey = getCacheKey ?? defaultGetCacheKey;
30+
31+
const memoized = (...args: Parameters<Fn>): ReturnType<Fn> => {
32+
const key = resolvedGetCacheKey(args);
33+
34+
if (memoized.cache.has(key)) {
35+
return memoized.cache.get(key);
36+
}
37+
38+
const value = fn(...args);
39+
40+
memoized.cache.set(key, value);
41+
42+
return value;
43+
};
44+
45+
memoized.cache = new Map();
46+
47+
return memoized as MemoizedFn<Fn, CkFn>;
48+
}

test/memo.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import { memo } from '../src/memo.js';
4+
5+
describe('memo()', () => {
6+
it('Memoizes functions', () => {
7+
const mock = vi.fn((str: string) => str);
8+
9+
const fn = memo(mock);
10+
11+
expect(mock).not.toHaveBeenCalled();
12+
13+
fn('test');
14+
15+
expect(mock).toHaveBeenCalledTimes(1);
16+
17+
fn('test');
18+
19+
expect(mock).toHaveBeenCalledTimes(1);
20+
21+
fn('test2');
22+
23+
expect(mock).toHaveBeenCalledTimes(2);
24+
});
25+
26+
it('Memoizes async functions', async () => {
27+
const mock = vi.fn(async (str: string): Promise<string> => str);
28+
29+
const fn = memo(mock);
30+
31+
expect(mock).not.toHaveBeenCalled();
32+
33+
await fn('test');
34+
35+
expect(mock).toHaveBeenCalledTimes(1);
36+
37+
await fn('test');
38+
39+
expect(mock).toHaveBeenCalledTimes(1);
40+
41+
await fn('test2');
42+
43+
expect(mock).toHaveBeenCalledTimes(2);
44+
45+
expect(fn('test3')).toBe(fn('test3'));
46+
});
47+
48+
it('Cache is available on the memoized function', () => {
49+
const mock = vi.fn((str: string) => str);
50+
51+
const fn = memo(mock);
52+
53+
expect(mock).not.toHaveBeenCalled();
54+
55+
fn('test');
56+
57+
expect(mock).toHaveBeenCalledTimes(1);
58+
59+
fn('test');
60+
61+
expect(mock).toHaveBeenCalledTimes(1);
62+
63+
fn.cache.clear();
64+
65+
fn('test');
66+
67+
expect(mock).toHaveBeenCalledTimes(2);
68+
});
69+
70+
it('Cache key is configurable', () => {
71+
const mock = vi.fn((_id: number, name: string) => name);
72+
73+
const fn = memo(mock, ([id, name]) => `${id}-${name}-key`);
74+
75+
fn(1, 'test');
76+
77+
expect(fn.cache.has('1-test-key')).toBe(true);
78+
});
79+
80+
it('Default getCacheKey()', () => {
81+
const mock = vi.fn((_id: number | null | undefined, name: string) => name);
82+
83+
const fn = memo(mock);
84+
85+
expect(mock).not.toHaveBeenCalled();
86+
87+
fn(1, 'test');
88+
89+
expect(mock).toHaveBeenCalledTimes(1);
90+
91+
expect(fn.cache.has(1)).toBe(true);
92+
93+
fn(null, 'test');
94+
95+
expect(mock).toHaveBeenCalledTimes(2);
96+
97+
fn(undefined, 'test');
98+
99+
// `undefined` serialized to `null` when used in arrays.
100+
expect(mock).toHaveBeenCalledTimes(2);
101+
102+
expect(fn.cache.has('[null,"test"]')).toBe(true);
103+
});
104+
});

0 commit comments

Comments
 (0)