UNPKG

4.19 kBPlain TextView Raw
1import type { EqualityFn } from './types'
2
3// Cache implementation based on Erik Rasmussen's `lru-memoize`:
4// https://github.com/erikras/lru-memoize
5
6const NOT_FOUND = 'NOT_FOUND'
7type NOT_FOUND_TYPE = typeof NOT_FOUND
8
9interface Entry {
10 key: unknown
11 value: unknown
12}
13
14interface Cache {
15 get(key: unknown): unknown | NOT_FOUND_TYPE
16 put(key: unknown, value: unknown): void
17 getEntries(): Entry[]
18 clear(): void
19}
20
21function createSingletonCache(equals: EqualityFn): Cache {
22 let entry: Entry | undefined
23 return {
24 get(key: unknown) {
25 if (entry && equals(entry.key, key)) {
26 return entry.value
27 }
28
29 return NOT_FOUND
30 },
31
32 put(key: unknown, value: unknown) {
33 entry = { key, value }
34 },
35
36 getEntries() {
37 return entry ? [entry] : []
38 },
39
40 clear() {
41 entry = undefined
42 }
43 }
44}
45
46function createLruCache(maxSize: number, equals: EqualityFn): Cache {
47 let entries: Entry[] = []
48
49 function get(key: unknown) {
50 const cacheIndex = entries.findIndex(entry => equals(key, entry.key))
51
52 // We found a cached entry
53 if (cacheIndex > -1) {
54 const entry = entries[cacheIndex]
55
56 // Cached entry not at top of cache, move it to the top
57 if (cacheIndex > 0) {
58 entries.splice(cacheIndex, 1)
59 entries.unshift(entry)
60 }
61
62 return entry.value
63 }
64
65 // No entry found in cache, return sentinel
66 return NOT_FOUND
67 }
68
69 function put(key: unknown, value: unknown) {
70 if (get(key) === NOT_FOUND) {
71 // TODO Is unshift slow?
72 entries.unshift({ key, value })
73 if (entries.length > maxSize) {
74 entries.pop()
75 }
76 }
77 }
78
79 function getEntries() {
80 return entries
81 }
82
83 function clear() {
84 entries = []
85 }
86
87 return { get, put, getEntries, clear }
88}
89
90export const defaultEqualityCheck: EqualityFn = (a, b): boolean => {
91 return a === b
92}
93
94export function createCacheKeyComparator(equalityCheck: EqualityFn) {
95 return function areArgumentsShallowlyEqual(
96 prev: unknown[] | IArguments | null,
97 next: unknown[] | IArguments | null
98 ): boolean {
99 if (prev === null || next === null || prev.length !== next.length) {
100 return false
101 }
102
103 // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
104 const length = prev.length
105 for (let i = 0; i < length; i++) {
106 if (!equalityCheck(prev[i], next[i])) {
107 return false
108 }
109 }
110
111 return true
112 }
113}
114
115export interface DefaultMemoizeOptions {
116 equalityCheck?: EqualityFn
117 resultEqualityCheck?: EqualityFn
118 maxSize?: number
119}
120
121// defaultMemoize now supports a configurable cache size with LRU behavior,
122// and optional comparison of the result value with existing values
123export function defaultMemoize<F extends (...args: any[]) => any>(
124 func: F,
125 equalityCheckOrOptions?: EqualityFn | DefaultMemoizeOptions
126) {
127 const providedOptions =
128 typeof equalityCheckOrOptions === 'object'
129 ? equalityCheckOrOptions
130 : { equalityCheck: equalityCheckOrOptions }
131
132 const {
133 equalityCheck = defaultEqualityCheck,
134 maxSize = 1,
135 resultEqualityCheck
136 } = providedOptions
137
138 const comparator = createCacheKeyComparator(equalityCheck)
139
140 const cache =
141 maxSize === 1
142 ? createSingletonCache(comparator)
143 : createLruCache(maxSize, comparator)
144
145 // we reference arguments instead of spreading them for performance reasons
146 function memoized() {
147 let value = cache.get(arguments)
148 if (value === NOT_FOUND) {
149 // @ts-ignore
150 value = func.apply(null, arguments)
151
152 if (resultEqualityCheck) {
153 const entries = cache.getEntries()
154 const matchingEntry = entries.find(entry =>
155 resultEqualityCheck(entry.value, value)
156 )
157
158 if (matchingEntry) {
159 value = matchingEntry.value
160 }
161 }
162
163 cache.put(arguments, value)
164 }
165 return value
166 }
167
168 memoized.clearCache = () => cache.clear()
169
170 return memoized as F & { clearCache: () => void }
171}