|
| 1 | +// Original source: |
| 2 | +// - https://github.com/facebook/react/blob/0b974418c9a56f6c560298560265dcf4b65784bc/packages/react/src/ReactCache.js |
| 3 | + |
| 4 | +/** |
| 5 | + * An alias for type `{}`. Represents any value that is not `null` or `undefined`. |
| 6 | + * It is mostly used for semantic purposes to help distinguish between an |
| 7 | + * empty object type and `{}` as they are not the same. |
| 8 | + * |
| 9 | + * @internal |
| 10 | + */ |
| 11 | +export type AnyNonNullishValue = NonNullable<unknown>; |
| 12 | + |
| 13 | +/** |
| 14 | + * Useful to flatten the type output to improve type hints shown in editors. |
| 15 | + * And also to transform an interface into a type to aide with assignability. |
| 16 | + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts Source} |
| 17 | + * |
| 18 | + * @internal |
| 19 | + */ |
| 20 | +export type Simplify<T> = T extends AnyFunction |
| 21 | + ? T |
| 22 | + : { |
| 23 | + [KeyType in keyof T]: T[KeyType]; |
| 24 | + } & AnyNonNullishValue; |
| 25 | + |
| 26 | +/** |
| 27 | + * A standard function returning true if two values are considered equal. |
| 28 | + * |
| 29 | + * @public |
| 30 | + */ |
| 31 | +export type EqualityFn<T = any> = (a: T, b: T) => boolean; |
| 32 | + |
| 33 | +/** |
| 34 | + * Represents the additional properties attached to a function memoized by `reselect`. |
| 35 | + * |
| 36 | + * `lruMemoize`, `weakMapMemoize` and `autotrackMemoize` all return these properties. |
| 37 | + * |
| 38 | + * @see {@linkcode ExtractMemoizerFields ExtractMemoizerFields} |
| 39 | + * |
| 40 | + * @public |
| 41 | + */ |
| 42 | +export type DefaultMemoizeFields = { |
| 43 | + /** |
| 44 | + * Clears the memoization cache associated with a memoized function. |
| 45 | + * This method is typically used to reset the state of the cache, allowing |
| 46 | + * for the garbage collection of previously memoized results and ensuring |
| 47 | + * that future calls to the function recompute the results. |
| 48 | + */ |
| 49 | + clearCache: () => void; |
| 50 | + resultsCount: () => number; |
| 51 | + resetResultsCount: () => void; |
| 52 | +}; |
| 53 | + |
| 54 | +/** |
| 55 | + * Any function with any arguments. |
| 56 | + * |
| 57 | + * @internal |
| 58 | + */ |
| 59 | +export type AnyFunction = (...args: any[]) => any; |
| 60 | + |
| 61 | +class StrongRef<T> { |
| 62 | + constructor(private value: T) {} |
| 63 | + |
| 64 | + deref() { |
| 65 | + return this.value; |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +/** |
| 70 | + * @returns The {@linkcode StrongRef} if {@linkcode WeakRef} is not available. |
| 71 | + * |
| 72 | + * @since 5.1.2 |
| 73 | + * @internal |
| 74 | + */ |
| 75 | +const getWeakRef = () => |
| 76 | + typeof WeakRef === 'undefined' ? (StrongRef as unknown as typeof WeakRef) : WeakRef; |
| 77 | + |
| 78 | +const Ref = /** @__PURE__ */ getWeakRef(); |
| 79 | + |
| 80 | +const UNTERMINATED = 0; |
| 81 | +const TERMINATED = 1; |
| 82 | + |
| 83 | +interface UnterminatedCacheNode<T> { |
| 84 | + /** |
| 85 | + * Status, represents whether the cached computation returned a value or threw an error. |
| 86 | + */ |
| 87 | + s: 0; |
| 88 | + /** |
| 89 | + * Value, either the cached result or an error, depending on status. |
| 90 | + */ |
| 91 | + v: void; |
| 92 | + /** |
| 93 | + * Object cache, a `WeakMap` where non-primitive arguments are stored. |
| 94 | + */ |
| 95 | + o: null | WeakMap<Function | Object, CacheNode<T>>; |
| 96 | + /** |
| 97 | + * Primitive cache, a regular Map where primitive arguments are stored. |
| 98 | + */ |
| 99 | + p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>; |
| 100 | +} |
| 101 | + |
| 102 | +interface TerminatedCacheNode<T> { |
| 103 | + /** |
| 104 | + * Status, represents whether the cached computation returned a value or threw an error. |
| 105 | + */ |
| 106 | + s: 1; |
| 107 | + /** |
| 108 | + * Value, either the cached result or an error, depending on status. |
| 109 | + */ |
| 110 | + v: T; |
| 111 | + /** |
| 112 | + * Object cache, a `WeakMap` where non-primitive arguments are stored. |
| 113 | + */ |
| 114 | + o: null | WeakMap<Function | Object, CacheNode<T>>; |
| 115 | + /** |
| 116 | + * Primitive cache, a regular `Map` where primitive arguments are stored. |
| 117 | + */ |
| 118 | + p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>; |
| 119 | +} |
| 120 | + |
| 121 | +type CacheNode<T> = TerminatedCacheNode<T> | UnterminatedCacheNode<T>; |
| 122 | + |
| 123 | +function createCacheNode<T>(): CacheNode<T> { |
| 124 | + return { |
| 125 | + s: UNTERMINATED, |
| 126 | + v: undefined, |
| 127 | + o: null, |
| 128 | + p: null, |
| 129 | + }; |
| 130 | +} |
| 131 | + |
| 132 | +/** |
| 133 | + * Configuration options for a memoization function utilizing `WeakMap` for |
| 134 | + * its caching mechanism. |
| 135 | + * |
| 136 | + * @template Result - The type of the return value of the memoized function. |
| 137 | + * |
| 138 | + * @since 5.0.0 |
| 139 | + * @public |
| 140 | + */ |
| 141 | +export interface WeakMapMemoizeOptions<Result = any> { |
| 142 | + /** |
| 143 | + * If provided, used to compare a newly generated output value against previous values in the cache. |
| 144 | + * If a match is found, the old value is returned. This addresses the common |
| 145 | + * ```ts |
| 146 | + * todos.map(todo => todo.id) |
| 147 | + * ``` |
| 148 | + * use case, where an update to another field in the original data causes a recalculation |
| 149 | + * due to changed references, but the output is still effectively the same. |
| 150 | + * |
| 151 | + * @since 5.0.0 |
| 152 | + */ |
| 153 | + resultEqualityCheck?: EqualityFn<Result>; |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Derefences the argument if it is a Ref. Else if it is a value already, return it. |
| 158 | + * |
| 159 | + * @param r - the object to maybe deref |
| 160 | + * @returns The derefenced value if the argument is a Ref, else the argument value itself. |
| 161 | + */ |
| 162 | +function maybeDeref(r: any) { |
| 163 | + if (r instanceof Ref) { |
| 164 | + return r.deref(); |
| 165 | + } |
| 166 | + |
| 167 | + return r; |
| 168 | +} |
| 169 | + |
| 170 | +/** |
| 171 | + * Creates a tree of `WeakMap`-based cache nodes based on the identity of the |
| 172 | + * arguments it's been called with (in this case, the extracted values from your input selectors). |
| 173 | + * This allows `weakMapMemoize` to have an effectively infinite cache size. |
| 174 | + * Cache results will be kept in memory as long as references to the arguments still exist, |
| 175 | + * and then cleared out as the arguments are garbage-collected. |
| 176 | + * |
| 177 | + * __Design Tradeoffs for `weakMapMemoize`:__ |
| 178 | + * - Pros: |
| 179 | + * - It has an effectively infinite cache size, but you have no control over |
| 180 | + * how long values are kept in cache as it's based on garbage collection and `WeakMap`s. |
| 181 | + * - Cons: |
| 182 | + * - There's currently no way to alter the argument comparisons. |
| 183 | + * They're based on strict reference equality. |
| 184 | + * - It's roughly the same speed as `lruMemoize`, although likely a fraction slower. |
| 185 | + * |
| 186 | + * __Use Cases for `weakMapMemoize`:__ |
| 187 | + * - This memoizer is likely best used for cases where you need to call the |
| 188 | + * same selector instance with many different arguments, such as a single |
| 189 | + * selector instance that is used in a list item component and called with |
| 190 | + * item IDs like: |
| 191 | + * ```ts |
| 192 | + * useSelector(state => selectSomeData(state, props.category)) |
| 193 | + * ``` |
| 194 | + * @param func - The function to be memoized. |
| 195 | + * @returns A memoized function with a `.clearCache()` method attached. |
| 196 | + * |
| 197 | + * @example |
| 198 | + * <caption>Using `createSelector`</caption> |
| 199 | + * ```ts |
| 200 | + * import { createSelector, weakMapMemoize } from 'reselect' |
| 201 | + * |
| 202 | + * interface RootState { |
| 203 | + * items: { id: number; category: string; name: string }[] |
| 204 | + * } |
| 205 | + * |
| 206 | + * const selectItemsByCategory = createSelector( |
| 207 | + * [ |
| 208 | + * (state: RootState) => state.items, |
| 209 | + * (state: RootState, category: string) => category |
| 210 | + * ], |
| 211 | + * (items, category) => items.filter(item => item.category === category), |
| 212 | + * { |
| 213 | + * memoize: weakMapMemoize, |
| 214 | + * argsMemoize: weakMapMemoize |
| 215 | + * } |
| 216 | + * ) |
| 217 | + * ``` |
| 218 | + * |
| 219 | + * @example |
| 220 | + * <caption>Using `createSelectorCreator`</caption> |
| 221 | + * ```ts |
| 222 | + * import { createSelectorCreator, weakMapMemoize } from 'reselect' |
| 223 | + * |
| 224 | + * const createSelectorWeakMap = createSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) |
| 225 | + * |
| 226 | + * const selectItemsByCategory = createSelectorWeakMap( |
| 227 | + * [ |
| 228 | + * (state: RootState) => state.items, |
| 229 | + * (state: RootState, category: string) => category |
| 230 | + * ], |
| 231 | + * (items, category) => items.filter(item => item.category === category) |
| 232 | + * ) |
| 233 | + * ``` |
| 234 | + * |
| 235 | + * @template Func - The type of the function that is memoized. |
| 236 | + * |
| 237 | + * @see {@link https://reselect.js.org/api/weakMapMemoize `weakMapMemoize`} |
| 238 | + * |
| 239 | + * @since 5.0.0 |
| 240 | + * @public |
| 241 | + * @experimental |
| 242 | + */ |
| 243 | +export function weakMapMemoize<Func extends AnyFunction>( |
| 244 | + func: Func, |
| 245 | + options: WeakMapMemoizeOptions<ReturnType<Func>> = {}, |
| 246 | +) { |
| 247 | + let fnNode = createCacheNode(); |
| 248 | + const { resultEqualityCheck } = options; |
| 249 | + |
| 250 | + let lastResult: WeakRef<object> | undefined; |
| 251 | + |
| 252 | + let resultsCount = 0; |
| 253 | + |
| 254 | + function memoized() { |
| 255 | + let cacheNode = fnNode; |
| 256 | + const { length } = arguments; |
| 257 | + for (let i = 0, l = length; i < l; i++) { |
| 258 | + let arg = arguments[i]; |
| 259 | + if (typeof arg === 'function' || (typeof arg === 'object' && arg !== null)) { |
| 260 | + if ('current' in arg && 'instanceId' in arg.current) { |
| 261 | + arg = arg.current.state; |
| 262 | + } |
| 263 | + // Objects go into a WeakMap |
| 264 | + let objectCache = cacheNode.o; |
| 265 | + if (objectCache === null) { |
| 266 | + cacheNode.o = objectCache = new WeakMap(); |
| 267 | + } |
| 268 | + const objectNode = objectCache.get(arg); |
| 269 | + if (objectNode === undefined) { |
| 270 | + cacheNode = createCacheNode(); |
| 271 | + objectCache.set(arg, cacheNode); |
| 272 | + } else { |
| 273 | + cacheNode = objectNode; |
| 274 | + } |
| 275 | + } else { |
| 276 | + // Primitives go into a regular Map |
| 277 | + let primitiveCache = cacheNode.p; |
| 278 | + if (primitiveCache === null) { |
| 279 | + cacheNode.p = primitiveCache = new Map(); |
| 280 | + } |
| 281 | + const primitiveNode = primitiveCache.get(arg); |
| 282 | + if (primitiveNode === undefined) { |
| 283 | + cacheNode = createCacheNode(); |
| 284 | + primitiveCache.set(arg, cacheNode); |
| 285 | + } else { |
| 286 | + cacheNode = primitiveNode; |
| 287 | + } |
| 288 | + } |
| 289 | + } |
| 290 | + |
| 291 | + const terminatedNode = cacheNode as unknown as TerminatedCacheNode<any>; |
| 292 | + |
| 293 | + let result; |
| 294 | + |
| 295 | + if (cacheNode.s === TERMINATED) { |
| 296 | + result = cacheNode.v; |
| 297 | + } else { |
| 298 | + // Allow errors to propagate |
| 299 | + result = func.apply(null, arguments as unknown as any[]); |
| 300 | + resultsCount++; |
| 301 | + |
| 302 | + if (resultEqualityCheck) { |
| 303 | + // Deref lastResult if it is a Ref |
| 304 | + const lastResultValue = maybeDeref(lastResult); |
| 305 | + |
| 306 | + if ( |
| 307 | + lastResultValue != null && |
| 308 | + resultEqualityCheck(lastResultValue as ReturnType<Func>, result) |
| 309 | + ) { |
| 310 | + result = lastResultValue; |
| 311 | + |
| 312 | + resultsCount !== 0 && resultsCount--; |
| 313 | + } |
| 314 | + |
| 315 | + const needsWeakRef = |
| 316 | + (typeof result === 'object' && result !== null) || typeof result === 'function'; |
| 317 | + |
| 318 | + lastResult = needsWeakRef ? /** @__PURE__ */ new Ref(result) : result; |
| 319 | + } |
| 320 | + } |
| 321 | + |
| 322 | + terminatedNode.s = TERMINATED; |
| 323 | + |
| 324 | + terminatedNode.v = result; |
| 325 | + return result; |
| 326 | + } |
| 327 | + |
| 328 | + memoized.clearCache = () => { |
| 329 | + fnNode = createCacheNode(); |
| 330 | + memoized.resetResultsCount(); |
| 331 | + }; |
| 332 | + |
| 333 | + memoized.resultsCount = () => resultsCount; |
| 334 | + |
| 335 | + memoized.resetResultsCount = () => { |
| 336 | + resultsCount = 0; |
| 337 | + }; |
| 338 | + |
| 339 | + return memoized as Func & Simplify<DefaultMemoizeFields>; |
| 340 | +} |
0 commit comments