-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathindex.ts
102 lines (78 loc) · 3.83 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/* eslint-disable @typescript-eslint/no-unsafe-return */
import type NonPartial from '@src/types/utils/NonPartial';
import type {TakeFirst} from '@src/types/utils/TupleOperations';
import ArrayCache from './cache/ArrayCache';
import {MemoizeStats} from './stats';
import type {Callable, ClientOptions, Constructable, IsomorphicFn, IsomorphicParameters, IsomorphicReturnType, MemoizedFn, Stats} from './types';
import {getEqualityComparator, mergeOptions, truncateArgs} from './utils';
/**
* Global memoization class. Use it to orchestrate memoization (e.g. start/stop global monitoring).
*/
class Memoize {
static isMonitoringEnabled = false;
private static memoizedList: Array<{id: string; memoized: Stats}> = [];
static registerMemoized(id: string, memoized: Stats) {
this.memoizedList.push({id, memoized});
}
static startMonitoring() {
if (this.isMonitoringEnabled) {
return;
}
this.isMonitoringEnabled = true;
Memoize.memoizedList.forEach(({memoized}) => {
memoized.startMonitoring();
});
}
static stopMonitoring() {
if (!this.isMonitoringEnabled) {
return;
}
this.isMonitoringEnabled = false;
return Memoize.memoizedList.map(({id, memoized}) => ({id, stats: memoized.stopMonitoring()}));
}
}
/**
* Wraps a function with a memoization layer. Useful for caching expensive calculations.
* @param fn - Function to memoize
* @param opts - Options for the memoization layer, for more details see `ClientOptions` type.
* @returns Memoized function with a cache API attached to it.
*/
function memoize<Fn extends IsomorphicFn, MaxArgs extends number = NonPartial<IsomorphicParameters<Fn>>['length'], Key = TakeFirst<IsomorphicParameters<Fn>, MaxArgs>>(
fn: Fn,
opts?: ClientOptions<Fn, MaxArgs, Key>,
) {
const options = mergeOptions<Fn, MaxArgs, Key>(opts);
const cache = ArrayCache<Key, IsomorphicReturnType<Fn>>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)});
const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled);
const memoized = function memoized(...args: IsomorphicParameters<Fn>): IsomorphicReturnType<Fn> {
// Detect if memoized function was called with `new` keyword. If so we need to call the original function as constructor.
const constructable = !!new.target;
const truncatedArgs = truncateArgs(args, options.maxArgs);
const key = options.transformKey ? options.transformKey(truncatedArgs) : (truncatedArgs as Key);
const statsEntry = stats.createEntry();
statsEntry.track('didHit', true);
const retrievalTimeStart = performance.now();
const cached = cache.getSet(key, () => {
const fnTimeStart = performance.now();
const result = (constructable ? new (fn as Constructable)(...args) : (fn as Callable)(...args)) as IsomorphicReturnType<Fn>;
statsEntry.trackTime('fnTime', fnTimeStart);
statsEntry.track('didHit', false);
return result;
});
// Subtract the time it took to run the function from the total retrieval time
statsEntry.trackTime('cacheRetrievalTime', retrievalTimeStart + (statsEntry.get('fnTime') ?? 0));
statsEntry.track('cacheSize', cache.size);
statsEntry.save();
return cached.value;
} as MemoizedFn<Fn, Key>;
/**
* Cache API attached to the memoized function. Currently there is an issue with typing cache keys, but the functionality works as expected.
*/
memoized.cache = cache;
memoized.startMonitoring = () => stats.startMonitoring();
memoized.stopMonitoring = () => stats.stopMonitoring();
Memoize.registerMemoized(options.monitoringName ?? fn.name, memoized);
return memoized;
}
export default memoize;
export {Memoize};