Skip to content

Commit 4f7a4b4

Browse files
committed
Test custom memoize
1 parent 487a82a commit 4f7a4b4

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed

packages/x-data-grid/src/utils/createSelector.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { lruMemoize, createSelectorCreator, Selector, SelectorResultArray } from 'reselect';
22
import { argsEqual } from '../hooks/utils/useGridSelector';
3+
import { weakMapMemoize } from './customMemoize';
34

45
type CacheKey = { id: number };
56

@@ -9,6 +10,7 @@ const reselectCreateSelector = createSelectorCreator({
910
maxSize: 1,
1011
equalityCheck: Object.is,
1112
},
13+
argsMemoize: weakMapMemoize,
1214
});
1315

1416
type GridCreateSelectorFunction = ReturnType<typeof reselectCreateSelector> & {
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
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

Comments
 (0)