-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmemoize.ts
138 lines (114 loc) · 3.49 KB
/
memoize.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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { TAnyFunction } from '../typeHelpers'
export interface MemoizeOptions {
strategy?: MemoizeStrategy
serializer?: MemoizeSerializer
ttl?: number
}
type MemoizeStrategy = 'monadic' | 'variadic'
type MemoizeSerializer = (data: unknown) => string
/**
* ### memoize(func, options?)
*
* Create a function that memoizes the return value of `func`.
*
* ```js
* const func = (a, b) => a + b
* const memoizedFunc = flocky.memoize(func)
* const memoizedFuncWithTtl = flocky.memoize(func, { ttl: 30 * 1000 })
* memoizedFunc(1, 2)
* // -> 3
* ```
*
* <details>
* <summary>Implementation Details</summary>
*
* This method's implementation is based on [fast-memoize](https://github.com/caiogondim/fast-memoize.js),
* with some improvements for variadic performance and additional support for a TTL based cache.
* </details>
*/
export function memoize<TThis, TReturn, TFunc extends TAnyFunction<TReturn>>(
this: TThis,
func: TFunc,
options: MemoizeOptions = {}
): TFunc {
const strategy =
options.strategy === 'monadic' || (options.strategy !== 'variadic' && func.length <= 1)
? monadic
: variadic
const cache = options.ttl ? ttlCache(options.ttl) : defaultCache()
const serializer = options.serializer ? options.serializer : defaultSerializer
return strategy.bind(this, func, cache, serializer) as TFunc
}
function isPrimitive(value: unknown): value is string {
// We can not treat strings as primitive, because they overwrite numbers
return value == null || typeof value === 'number' || typeof value === 'boolean'
}
function monadic<TThis, TReturn, TFunc extends TAnyFunction<TReturn>>(
this: TThis,
func: TFunc,
cache: MemoizeCache<TReturn>,
serializer: MemoizeSerializer,
arg: unknown
): TReturn {
const cacheKey = isPrimitive(arg) ? arg : serializer(arg)
let value = cache.get(cacheKey)
if (typeof value === 'undefined') {
value = func.call(this, arg)
if (value instanceof Promise) {
value.catch(() => cache.remove(cacheKey))
}
cache.set(cacheKey, value)
}
return value
}
function variadic<TThis, TReturn, TFunc extends TAnyFunction<TReturn>>(
this: TThis,
func: TFunc,
cache: MemoizeCache<TReturn>,
serializer: MemoizeSerializer,
...args: Array<unknown>
): TReturn {
const cacheKey = serializer(args)
let value = cache.get(cacheKey)
if (typeof value === 'undefined') {
value = func.apply(this, args)
if (value instanceof Promise) {
value.catch(() => cache.remove(cacheKey))
}
cache.set(cacheKey, value)
}
return value
}
function defaultSerializer(data: unknown): string {
return JSON.stringify(data)
}
interface MemoizeCache<TReturn> {
get: (key: string) => TReturn | undefined
set: (key: string, value: TReturn) => void
remove: (key: string) => void
}
function defaultCache<TReturn>(): MemoizeCache<TReturn> {
const cache = Object.create(null) as Record<string, TReturn>
return {
get: (key) => cache[key],
set: (key, value): void => {
cache[key] = value
},
remove: (key) => delete cache[key],
}
}
function ttlCache<TReturn>(ttl: number): MemoizeCache<TReturn> {
const cache = Object.create(null) as Record<string, TReturn>
return {
get: (key) => cache[key],
set: (key, value): void => {
cache[key] = value
// Note: We do not need to clear the timeout because we never set a key
// if it still exists in the cache.
setTimeout(() => {
delete cache[key]
}, ttl)
},
remove: (key) => delete cache[key],
}
}