diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 47838c964..19a88be2c 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -227,25 +227,29 @@ export function weakMapMemoize( // Allow errors to propagate result = func.apply(null, arguments as unknown as any[]) resultsCount++ - } - terminatedNode.s = TERMINATED + if (resultEqualityCheck) { + const lastResultValue = lastResult?.deref?.() ?? lastResult - if (resultEqualityCheck) { - const lastResultValue = lastResult?.deref?.() ?? lastResult - if ( - lastResultValue != null && - resultEqualityCheck(lastResultValue as ReturnType, result) - ) { - result = lastResultValue - resultsCount !== 0 && resultsCount-- - } + if ( + lastResultValue != null && + resultEqualityCheck(lastResultValue as ReturnType, result) + ) { + result = lastResultValue + + resultsCount !== 0 && resultsCount-- + } + + const needsWeakRef = + (typeof result === 'object' && result !== null) || + typeof result === 'function' - const needsWeakRef = - (typeof result === 'object' && result !== null) || - typeof result === 'function' - lastResult = needsWeakRef ? new Ref(result) : result + lastResult = needsWeakRef ? new Ref(result) : result + } } + + terminatedNode.s = TERMINATED + terminatedNode.v = result return result } diff --git a/test/benchmarks/resultEqualityCheck.bench.ts b/test/benchmarks/resultEqualityCheck.bench.ts new file mode 100644 index 000000000..76dffc06e --- /dev/null +++ b/test/benchmarks/resultEqualityCheck.bench.ts @@ -0,0 +1,190 @@ +import type { AnyFunction } from '@internal/types' +import type { OutputSelector, Selector } from 'reselect' +import { + createSelector, + lruMemoize, + referenceEqualityCheck, + weakMapMemoize +} from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import { + logSelectorRecomputations, + setFunctionNames, + setupStore, + toggleCompleted, + type RootState +} from '../testUtils' + +describe('memoize functions performance with resultEqualityCheck set to referenceEqualityCheck vs. without resultEqualityCheck', () => { + describe('comparing selectors created with createSelector', () => { + const store = setupStore() + + const arrayOfNumbers = Array.from({ length: 1_000 }, (num, index) => index) + + const commonOptions: Options = { + iterations: 10_000, + time: 0 + } + + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(store.getState()) + }) + } + + const createAppSelector = createSelector.withTypes() + + const selectTodoIdsWeakMap = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id) + ) + + const selectTodoIdsWeakMapWithResultEqualityCheck = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck: referenceEqualityCheck }, + argsMemoizeOptions: { resultEqualityCheck: referenceEqualityCheck } + } + ) + + const selectTodoIdsLru = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { memoize: lruMemoize, argsMemoize: lruMemoize } + ) + + const selectTodoIdsLruWithResultEqualityCheck = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoize: lruMemoize, + memoizeOptions: { resultEqualityCheck: referenceEqualityCheck }, + argsMemoize: lruMemoize, + argsMemoizeOptions: { resultEqualityCheck: referenceEqualityCheck } + } + ) + + const selectors = { + selectTodoIdsWeakMap, + selectTodoIdsWeakMapWithResultEqualityCheck, + selectTodoIdsLru, + selectTodoIdsLruWithResultEqualityCheck + } + + setFunctionNames(selectors) + + const createOptions = (selector: S) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + + Object.values(selectors).forEach(selector => { + bench( + selector, + () => { + runSelector(selector) + }, + createOptions(selector) + ) + }) + }) + + describe('comparing selectors created with memoize functions', () => { + const store = setupStore() + + const arrayOfNumbers = Array.from( + { length: 100_000 }, + (num, index) => index + ) + + const commonOptions: Options = { + iterations: 1000, + time: 0 + } + + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(store.getState()) + }) + } + + const selectTodoIdsWeakMap = weakMapMemoize((state: RootState) => + state.todos.map(({ id }) => id) + ) + + const selectTodoIdsWeakMapWithResultEqualityCheck = weakMapMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck: referenceEqualityCheck } + ) + + const selectTodoIdsLru = lruMemoize((state: RootState) => + state.todos.map(({ id }) => id) + ) + + const selectTodoIdsLruWithResultEqualityCheck = lruMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck: referenceEqualityCheck } + ) + + const memoizedFunctions = { + selectTodoIdsWeakMap, + selectTodoIdsWeakMapWithResultEqualityCheck, + selectTodoIdsLru, + selectTodoIdsLruWithResultEqualityCheck + } + + setFunctionNames(memoizedFunctions) + + const createOptions = < + Func extends AnyFunction & { resultsCount: () => number } + >( + memoizedFunction: Func + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + + afterAll: () => { + console.log( + memoizedFunction.name, + memoizedFunction.resultsCount() + ) + } + } + } + } + return { ...commonOptions, ...options } + } + + Object.values(memoizedFunctions).forEach(memoizedFunction => { + bench( + memoizedFunction, + () => { + runSelector(memoizedFunction) + }, + createOptions(memoizedFunction) + ) + }) + }) +}) diff --git a/test/computationComparisons.spec.tsx b/test/computationComparisons.spec.tsx index e23266382..472080f05 100644 --- a/test/computationComparisons.spec.tsx +++ b/test/computationComparisons.spec.tsx @@ -304,18 +304,18 @@ describe('resultEqualityCheck in weakMapMemoize', () => { expect(memoized.resultsCount()).toBe(5) expect(memoizedShallow(state)).toBe(memoizedShallow(state)) - expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow.resultsCount()).toBe(1) expect(memoizedShallow({ ...state })).toBe(memoizedShallow(state)) - expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow.resultsCount()).toBe(1) expect(memoizedShallow({ ...state })).toBe(memoizedShallow(state)) // We spread the state to force the function to re-run but the // result maintains the same reference because of `resultEqualityCheck`. const first = memoizedShallow({ ...state }) - expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow.resultsCount()).toBe(1) memoizedShallow({ ...state }) - expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow.resultsCount()).toBe(1) const second = memoizedShallow({ ...state }) - expect(memoizedShallow.resultsCount()).toBe(0) + expect(memoizedShallow.resultsCount()).toBe(1) expect(first).toBe(second) }) }) diff --git a/test/inputStabilityCheck.spec.ts b/test/inputStabilityCheck.spec.ts index 92242f557..2f7b8b954 100644 --- a/test/inputStabilityCheck.spec.ts +++ b/test/inputStabilityCheck.spec.ts @@ -1,5 +1,12 @@ import { shallowEqual } from 'react-redux' -import { createSelector, lruMemoize, setGlobalDevModeChecks } from 'reselect' +import { + createSelector, + lruMemoize, + referenceEqualityCheck, + setGlobalDevModeChecks +} from 'reselect' +import type { RootState } from './testUtils' +import { localTest } from './testUtils' describe('inputStabilityCheck', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) @@ -164,3 +171,105 @@ describe('inputStabilityCheck', () => { expect(consoleSpy).not.toHaveBeenCalled() }) }) + +describe('the effects of inputStabilityCheck with resultEqualityCheck', () => { + const createAppSelector = createSelector.withTypes() + + const resultEqualityCheck = vi + .fn(referenceEqualityCheck) + .mockName('resultEqualityCheck') + + afterEach(() => { + resultEqualityCheck.mockClear() + }) + + localTest( + 'resultEqualityCheck should not be called with empty objects when inputStabilityCheck is set to once and input selectors are stable', + ({ store }) => { + const selectTodoIds = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck }, + devModeChecks: { inputStabilityCheck: 'once' } + } + ) + + const firstResult = selectTodoIds(store.getState()) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).toBe(secondResult) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + const thirdResult = selectTodoIds(store.getState()) + + expect(secondResult).toBe(thirdResult) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + } + ) + + localTest( + 'resultEqualityCheck should not be called with empty objects when inputStabilityCheck is set to always and input selectors are stable', + ({ store }) => { + const selectTodoIds = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck }, + devModeChecks: { inputStabilityCheck: 'always' } + } + ) + + const firstResult = selectTodoIds(store.getState()) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).toBe(secondResult) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + const thirdResult = selectTodoIds(store.getState()) + + expect(secondResult).toBe(thirdResult) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + } + ) + + localTest( + 'resultEqualityCheck should not be called with empty objects when inputStabilityCheck is set to never and input selectors are unstable', + ({ store }) => { + const selectTodoIds = createAppSelector( + [state => [...state.todos]], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck }, + devModeChecks: { inputStabilityCheck: 'never' } + } + ) + + const firstResult = selectTodoIds(store.getState()) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).toBe(secondResult) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + const thirdResult = selectTodoIds(store.getState()) + + expect(secondResult).toBe(thirdResult) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + } + ) +}) diff --git a/test/lruMemoize.test.ts b/test/lruMemoize.test.ts index b7c5bd5dd..be3ed663d 100644 --- a/test/lruMemoize.test.ts +++ b/test/lruMemoize.test.ts @@ -1,10 +1,15 @@ // TODO: Add test for React Redux connect function -import { createSelectorCreator, lruMemoize } from 'reselect' +import { shallowEqual } from 'react-redux' +import { + createSelectorCreator, + lruMemoize, + referenceEqualityCheck +} from 'reselect' import type { RootState } from './testUtils' import { localTest, toggleCompleted } from './testUtils' -const createSelector = createSelectorCreator({ +const createSelectorLru = createSelectorCreator({ memoize: lruMemoize, argsMemoize: lruMemoize }) @@ -269,7 +274,7 @@ describe(lruMemoize, () => { const fooChangeSpy = vi.fn() - const fooChangeHandler = createSelector( + const fooChangeHandler = createSelectorLru( (state: any) => state.foo, fooChangeSpy ) @@ -285,7 +290,7 @@ describe(lruMemoize, () => { const state2 = { a: 1 } let count = 0 - const selector = createSelector([(state: any) => state.a], () => { + const selector = createSelectorLru([(state: any) => state.a], () => { count++ return undefined }) @@ -362,7 +367,7 @@ describe(lruMemoize, () => { funcCalls = 0 // Test out maxSize of 3 + exposure via createSelector - const selector = createSelector( + const selector = createSelectorLru( (state: string) => state, state => { funcCalls++ @@ -453,3 +458,265 @@ describe(lruMemoize, () => { } ) }) + +describe('lruMemoize integration with resultEqualityCheck', () => { + const createAppSelector = createSelectorLru.withTypes() + + const resultEqualityCheck = vi + .fn(shallowEqual) + .mockName('resultEqualityCheck') + + afterEach(() => { + resultEqualityCheck.mockClear() + }) + + localTest( + 'resultEqualityCheck works when set to shallowEqual', + ({ store }) => { + const selectTodoIds = lruMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck } + ) + + const firstResult = selectTodoIds(store.getState()) + + store.dispatch(toggleCompleted(0)) + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).toBe(secondResult) + + expect(selectTodoIds.resultsCount()).toBe(1) + } + ) + + localTest( + 'resultEqualityCheck should not be called on the first output selector call', + ({ store }) => { + const selectTodoIds = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck }, + devModeChecks: { inputStabilityCheck: 'once' } + } + ) + + expect(selectTodoIds(store.getState())).to.be.an('array').that.is.not + .empty + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + store.dispatch(toggleCompleted(0)) + + expect(selectTodoIds.lastResult()).toBe(selectTodoIds(store.getState())) + + expect(resultEqualityCheck).toHaveBeenCalledOnce() + + expect(selectTodoIds.memoizedResultFunc.resultsCount()).toBe(1) + + expect(selectTodoIds.recomputations()).toBe(2) + + expect(selectTodoIds.resultsCount()).toBe(2) + + expect(selectTodoIds.dependencyRecomputations()).toBe(2) + + store.dispatch(toggleCompleted(0)) + + expect(selectTodoIds.lastResult()).toBe(selectTodoIds(store.getState())) + + expect(resultEqualityCheck).toHaveBeenCalledTimes(2) + + expect(selectTodoIds.memoizedResultFunc.resultsCount()).toBe(1) + + expect(selectTodoIds.recomputations()).toBe(3) + + expect(selectTodoIds.resultsCount()).toBe(3) + + expect(selectTodoIds.dependencyRecomputations()).toBe(3) + } + ) + + localTest( + 'lruMemoize with resultEqualityCheck set to referenceEqualityCheck works the same as lruMemoize without resultEqualityCheck', + ({ store }) => { + const resultEqualityCheck = vi + .fn(referenceEqualityCheck) + .mockName('resultEqualityCheck') + + const selectTodoIdsWithResultEqualityCheck = lruMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck } + ) + + const firstResultWithResultEqualityCheck = + selectTodoIdsWithResultEqualityCheck(store.getState()) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + store.dispatch(toggleCompleted(0)) + + const secondResultWithResultEqualityCheck = + selectTodoIdsWithResultEqualityCheck(store.getState()) + + expect(firstResultWithResultEqualityCheck).not.toBe( + secondResultWithResultEqualityCheck + ) + + expect(firstResultWithResultEqualityCheck).toStrictEqual( + secondResultWithResultEqualityCheck + ) + + expect(selectTodoIdsWithResultEqualityCheck.resultsCount()).toBe(2) + + const selectTodoIds = lruMemoize((state: RootState) => + state.todos.map(({ id }) => id) + ) + + const firstResult = selectTodoIds(store.getState()) + + store.dispatch(toggleCompleted(0)) + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).not.toBe(secondResult) + + expect(firstResult).toStrictEqual(secondResult) + + expect(selectTodoIds.resultsCount()).toBe(2) + + resultEqualityCheck.mockClear() + } + ) +}) + +describe('lruMemoize integration with resultEqualityCheck', () => { + const createAppSelector = createSelectorLru.withTypes() + + const resultEqualityCheck = vi + .fn(shallowEqual) + .mockName('resultEqualityCheck') + + afterEach(() => { + resultEqualityCheck.mockClear() + }) + + localTest( + 'resultEqualityCheck works when set to shallowEqual', + ({ store }) => { + const selectTodoIds = lruMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck } + ) + + const firstResult = selectTodoIds(store.getState()) + + store.dispatch(toggleCompleted(0)) + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).toBe(secondResult) + + expect(selectTodoIds.resultsCount()).toBe(1) + } + ) + + localTest( + 'resultEqualityCheck should not be called on the first output selector call', + ({ store }) => { + const selectTodoIds = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck }, + devModeChecks: { inputStabilityCheck: 'once' } + } + ) + + expect(selectTodoIds(store.getState())).to.be.an('array').that.is.not + .empty + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + store.dispatch(toggleCompleted(0)) + + expect(selectTodoIds.lastResult()).toBe(selectTodoIds(store.getState())) + + expect(resultEqualityCheck).toHaveBeenCalledOnce() + + expect(selectTodoIds.memoizedResultFunc.resultsCount()).toBe(1) + + expect(selectTodoIds.recomputations()).toBe(2) + + expect(selectTodoIds.resultsCount()).toBe(2) + + expect(selectTodoIds.dependencyRecomputations()).toBe(2) + + store.dispatch(toggleCompleted(0)) + + expect(selectTodoIds.lastResult()).toBe(selectTodoIds(store.getState())) + + expect(resultEqualityCheck).toHaveBeenCalledTimes(2) + + expect(selectTodoIds.memoizedResultFunc.resultsCount()).toBe(1) + + expect(selectTodoIds.recomputations()).toBe(3) + + expect(selectTodoIds.resultsCount()).toBe(3) + + expect(selectTodoIds.dependencyRecomputations()).toBe(3) + } + ) + + localTest( + 'lruMemoize with resultEqualityCheck set to referenceEqualityCheck works the same as lruMemoize without resultEqualityCheck', + ({ store }) => { + const resultEqualityCheck = vi + .fn(referenceEqualityCheck) + .mockName('resultEqualityCheck') + + const selectTodoIdsWithResultEqualityCheck = lruMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck } + ) + + const firstResultWithResultEqualityCheck = + selectTodoIdsWithResultEqualityCheck(store.getState()) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + store.dispatch(toggleCompleted(0)) + + const secondResultWithResultEqualityCheck = + selectTodoIdsWithResultEqualityCheck(store.getState()) + + expect(firstResultWithResultEqualityCheck).not.toBe( + secondResultWithResultEqualityCheck + ) + + expect(firstResultWithResultEqualityCheck).toStrictEqual( + secondResultWithResultEqualityCheck + ) + + expect(selectTodoIdsWithResultEqualityCheck.resultsCount()).toBe(2) + + const selectTodoIds = lruMemoize((state: RootState) => + state.todos.map(({ id }) => id) + ) + + const firstResult = selectTodoIds(store.getState()) + + store.dispatch(toggleCompleted(0)) + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).not.toBe(secondResult) + + expect(firstResult).toStrictEqual(secondResult) + + expect(selectTodoIds.resultsCount()).toBe(2) + + resultEqualityCheck.mockClear() + } + ) +}) diff --git a/test/testUtils.ts b/test/testUtils.ts index b28470b3b..9c3c0bf0d 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,13 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' import { test } from 'vitest' -import type { lruMemoize } from '../src/lruMemoize' -import type { - AnyFunction, - OutputSelector, - SelectorArray, - Simplify -} from '../src/types' +import type { AnyFunction, OutputSelector, Simplify } from '../src/types' export interface Todo { id: number @@ -437,22 +431,17 @@ export const logRecomputations = (selector: S) => { ) } -export const logSelectorRecomputations = < - S extends OutputSelector ->( +export const logSelectorRecomputations = ( selector: S ) => { - console.log( - `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, - { - resultFunc: selector.recomputations(), - inputSelectors: selector.dependencyRecomputations(), - newResults: - typeof selector.memoizedResultFunc.resultsCount === 'function' - ? selector.memoizedResultFunc.resultsCount() - : undefined - } - ) + console.log(`\x1B[32m\x1B[1m${selector.name}\x1B[0m:`, { + resultFunc: selector.recomputations(), + inputSelectors: selector.dependencyRecomputations(), + newResults: + typeof selector.memoizedResultFunc.resultsCount === 'function' + ? selector.memoizedResultFunc.resultsCount() + : undefined + }) // console.log( // `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, // `\x1B[33m${selector.recomputations().toLocaleString('en-US')}\x1B[0m`, diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index e55239e8a..c504acab9 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -1,5 +1,12 @@ -import { createSelector, createSelectorCreator, weakMapMemoize } from 'reselect' -import { setEnvToProd } from './testUtils' +import { shallowEqual } from 'react-redux' +import { + createSelector, + createSelectorCreator, + referenceEqualityCheck, + weakMapMemoize +} from 'reselect' +import type { RootState } from './testUtils' +import { localTest, setEnvToProd, toggleCompleted } from './testUtils' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function const numOfStates = 1_000_000 @@ -224,3 +231,134 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { expect(totalTime).toBeLessThan(2000) }) }) + +describe('weakMapMemoize integration with resultEqualityCheck', () => { + const createAppSelector = createSelector.withTypes() + + const resultEqualityCheck = vi + .fn(shallowEqual) + .mockName('resultEqualityCheck') + + afterEach(() => { + resultEqualityCheck.mockClear() + }) + + localTest( + 'resultEqualityCheck works correctly when set to shallowEqual', + ({ store }) => { + const selectTodoIds = weakMapMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck } + ) + + const firstResult = selectTodoIds(store.getState()) + + store.dispatch(toggleCompleted(0)) + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).toBe(secondResult) + + expect(selectTodoIds.resultsCount()).toBe(1) + } + ) + + localTest( + 'resultEqualityCheck is not called on the first output selector call', + ({ store }) => { + const selectTodoIds = createAppSelector( + [state => state.todos], + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck }, + devModeChecks: { inputStabilityCheck: 'once' } + } + ) + + expect(selectTodoIds(store.getState())).to.be.an('array').that.is.not + .empty + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + store.dispatch(toggleCompleted(0)) + + expect(selectTodoIds.lastResult()).toBe(selectTodoIds(store.getState())) + + expect(resultEqualityCheck).toHaveBeenCalledOnce() + + expect(selectTodoIds.memoizedResultFunc.resultsCount()).toBe(1) + + expect(selectTodoIds.recomputations()).toBe(2) + + expect(selectTodoIds.resultsCount()).toBe(2) + + expect(selectTodoIds.dependencyRecomputations()).toBe(2) + + store.dispatch(toggleCompleted(0)) + + expect(selectTodoIds.lastResult()).toBe(selectTodoIds(store.getState())) + + expect(resultEqualityCheck).toHaveBeenCalledTimes(2) + + expect(selectTodoIds.memoizedResultFunc.resultsCount()).toBe(1) + + expect(selectTodoIds.recomputations()).toBe(3) + + expect(selectTodoIds.resultsCount()).toBe(3) + + expect(selectTodoIds.dependencyRecomputations()).toBe(3) + } + ) + + localTest( + 'weakMapMemoize with resultEqualityCheck set to referenceEqualityCheck works the same as weakMapMemoize without resultEqualityCheck', + ({ store }) => { + const resultEqualityCheck = vi + .fn(referenceEqualityCheck) + .mockName('resultEqualityCheck') + + const selectTodoIdsWithResultEqualityCheck = weakMapMemoize( + (state: RootState) => state.todos.map(({ id }) => id), + { resultEqualityCheck } + ) + + const firstResultWithResultEqualityCheck = + selectTodoIdsWithResultEqualityCheck(store.getState()) + + expect(resultEqualityCheck).not.toHaveBeenCalled() + + store.dispatch(toggleCompleted(0)) + + const secondResultWithResultEqualityCheck = + selectTodoIdsWithResultEqualityCheck(store.getState()) + + expect(firstResultWithResultEqualityCheck).not.toBe( + secondResultWithResultEqualityCheck + ) + + expect(firstResultWithResultEqualityCheck).toStrictEqual( + secondResultWithResultEqualityCheck + ) + + expect(selectTodoIdsWithResultEqualityCheck.resultsCount()).toBe(2) + + const selectTodoIds = weakMapMemoize((state: RootState) => + state.todos.map(({ id }) => id) + ) + + const firstResult = selectTodoIds(store.getState()) + + store.dispatch(toggleCompleted(0)) + + const secondResult = selectTodoIds(store.getState()) + + expect(firstResult).not.toBe(secondResult) + + expect(firstResult).toStrictEqual(secondResult) + + expect(selectTodoIds.resultsCount()).toBe(2) + + resultEqualityCheck.mockClear() + } + ) +})