Skip to content

Commit 4c07b61

Browse files
committed
Add support for reusing a recent value if resultEqualityCheck given
1 parent 3e6c377 commit 4c07b61

File tree

2 files changed

+100
-7
lines changed

2 files changed

+100
-7
lines changed

src/defaultMemoize.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface Entry {
1111
interface Cache {
1212
get(key: any): Entry | undefined
1313
put(key: any, value: any): void
14+
getValues(): any[]
1415
}
1516

1617
function createSingletonCache(equals: EqualityFn): Cache {
@@ -24,6 +25,10 @@ function createSingletonCache(equals: EqualityFn): Cache {
2425

2526
put(key: any, value: any) {
2627
entry = { key, value }
28+
},
29+
30+
getValues() {
31+
return entry ? [entry.value] : []
2732
}
2833
}
2934
}
@@ -61,14 +66,18 @@ function createLruCache(maxSize: number, equals: EqualityFn): Cache {
6166
}
6267
}
6368

64-
return { get, put }
69+
function getValues() {
70+
return entries.map(entry => entry.value)
71+
}
72+
73+
return { get, put, getValues }
6574
}
6675

6776
export const defaultEqualityCheck: EqualityFn = (a, b): boolean => {
6877
return a === b
6978
}
7079

71-
function createCacheKeyComparator(equalityCheck: EqualityFn) {
80+
export function createCacheKeyComparator(equalityCheck: EqualityFn) {
7281
return function areArgumentsShallowlyEqual(
7382
prev: unknown[] | IArguments | null,
7483
next: unknown[] | IArguments | null
@@ -95,8 +104,8 @@ export interface DefaultMemoizeOptions {
95104
maxSize?: number
96105
}
97106

98-
// defaultMemoize now supports a configurable cache size and comparison of the result value.
99-
// Updated behavior based on the `betterMemoize` function from
107+
// defaultMemoize now supports a configurable cache size with LRU behavior,
108+
// and optional comparison of the result value with existing values
100109
export function defaultMemoize<F extends (...args: any[]) => any>(
101110
func: F,
102111
equalityCheckOrOptions?: EqualityFn | DefaultMemoizeOptions
@@ -113,9 +122,6 @@ export function defaultMemoize<F extends (...args: any[]) => any>(
113122
} = providedOptions
114123

115124
const comparator = createCacheKeyComparator(equalityCheck)
116-
let resultComparator = resultEqualityCheck
117-
? createCacheKeyComparator(resultEqualityCheck)
118-
: undefined
119125

120126
const cache =
121127
maxSize === 1
@@ -128,6 +134,17 @@ export function defaultMemoize<F extends (...args: any[]) => any>(
128134
if (value === undefined) {
129135
// @ts-ignore
130136
value = func.apply(null, arguments)
137+
138+
if (resultEqualityCheck) {
139+
const existingValues = cache.getValues()
140+
const matchingValue = existingValues.find(ev =>
141+
resultEqualityCheck(ev, value)
142+
)
143+
if (matchingValue) {
144+
return matchingValue
145+
}
146+
}
147+
131148
cache.put(arguments, value)
132149
}
133150
return value

test/test_selector.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,82 @@ describe('defaultMemoize', () => {
513513
expect(funcCalls).toBe(7)
514514
})
515515

516+
test('Allows reusing an existing result if they are equivalent', () => {
517+
interface Todo {
518+
id: number
519+
name: string
520+
}
521+
522+
const todos1: Todo[] = [
523+
{ id: 1, name: 'a' },
524+
{ id: 2, name: 'b' },
525+
{ id: 3, name: 'c' }
526+
]
527+
const todos2 = todos1.slice()
528+
todos2[2] = { id: 3, name: 'd' }
529+
530+
function is(x: unknown, y: unknown) {
531+
if (x === y) {
532+
return x !== 0 || y !== 0 || 1 / x === 1 / y
533+
} else {
534+
return x !== x && y !== y
535+
}
536+
}
537+
538+
function shallowEqual(objA: any, objB: any) {
539+
if (is(objA, objB)) return true
540+
541+
if (
542+
typeof objA !== 'object' ||
543+
objA === null ||
544+
typeof objB !== 'object' ||
545+
objB === null
546+
) {
547+
return false
548+
}
549+
550+
const keysA = Object.keys(objA)
551+
const keysB = Object.keys(objB)
552+
553+
if (keysA.length !== keysB.length) return false
554+
555+
for (let i = 0; i < keysA.length; i++) {
556+
if (
557+
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
558+
!is(objA[keysA[i]], objB[keysA[i]])
559+
) {
560+
return false
561+
}
562+
}
563+
564+
return true
565+
}
566+
567+
for (let maxSize of [1, 3]) {
568+
let funcCalls = 0
569+
570+
const memoizer = defaultMemoize(
571+
(state: Todo[]) => {
572+
funcCalls++
573+
return state.map(todo => todo.id)
574+
},
575+
{
576+
maxSize,
577+
resultEqualityCheck: shallowEqual
578+
}
579+
)
580+
581+
const ids1 = memoizer(todos1)
582+
expect(funcCalls).toBe(1)
583+
584+
const ids2 = memoizer(todos1)
585+
expect(funcCalls).toBe(1)
586+
expect(ids2).toBe(ids1)
587+
588+
const ids3 = memoizer(todos2)
589+
expect(funcCalls).toBe(2)
590+
expect(ids3).toBe(ids1)
591+
}
516592
})
517593

518594
test('Accepts an options object as an arg', () => {

0 commit comments

Comments
 (0)