Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try adding resultEqualityCheck to weakMapMemoize #647

Merged
merged 16 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@
},
"license": "MIT",
"devDependencies": {
"@reduxjs/toolkit": "^1.9.3",
"@reduxjs/toolkit": "^2.0.0-rc.1",
"@testing-library/react": "^14.1.2",
"@types/lodash": "^4.14.175",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@types/shelljs": "^0.8.11",
"@typescript-eslint/eslint-plugin": "5.1.0",
"@typescript-eslint/eslint-plugin-tslint": "5.1.0",
Expand All @@ -60,13 +63,14 @@
"eslint": "^8.0.1",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-typescript": "0.14.0",
"jsdom": "^23.0.0",
"lodash.memoize": "^4.1.2",
"memoize-one": "^6.0.0",
"micro-memoize": "^4.0.9",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.2",
"react-redux": "^9.0.0-rc.0",
"rimraf": "^3.0.2",
"shelljs": "^0.8.5",
"tsup": "^6.7.0",
Expand Down
8 changes: 2 additions & 6 deletions src/autotrackMemoize/autotrackMemoize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@ import type { Node } from './tracking'
import {
createCacheKeyComparator,
defaultEqualityCheck
} from '@internal/defaultMemoize'
import type {
AnyFunction,
DefaultMemoizeFields,
Simplify
} from '@internal/types'
} from '../defaultMemoize'
import type { AnyFunction, DefaultMemoizeFields, Simplify } from '../types'
import { createCache } from './autotracking'

/**
Expand Down
4 changes: 2 additions & 2 deletions src/autotrackMemoize/autotracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// Additional references:
// - https://www.pzuraq.com/blog/how-autotracking-works
// - https://v5.chriskrycho.com/journal/autotracking-elegant-dx-via-cutting-edge-cs/
import type { EqualityFn } from '@internal/types'
import { assertIsFunction } from '@internal/utils'
import type { EqualityFn } from '../types'
import { assertIsFunction } from '../utils'

// The global revision clock. Every time state changes, the clock increments.
export let $REVISION = 0
Expand Down
17 changes: 14 additions & 3 deletions src/defaultMemoize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type {
Simplify
} from './types'

import type { NOT_FOUND_TYPE } from './utils'
import { NOT_FOUND } from './utils'

// Cache implementation based on Erik Rasmussen's `lru-memoize`:
// https://github.com/erikras/lru-memoize

const NOT_FOUND = 'NOT_FOUND'
type NOT_FOUND_TYPE = typeof NOT_FOUND

interface Entry {
key: unknown
value: unknown
Expand Down Expand Up @@ -182,6 +182,8 @@ export function defaultMemoize<Func extends AnyFunction>(

const comparator = createCacheKeyComparator(equalityCheck)

let resultsCount = 0

const cache =
maxSize === 1
? createSingletonCache(comparator)
Expand All @@ -193,6 +195,7 @@ export function defaultMemoize<Func extends AnyFunction>(
if (value === NOT_FOUND) {
// @ts-ignore
value = func.apply(null, arguments)
resultsCount++

if (resultEqualityCheck) {
const entries = cache.getEntries()
Expand All @@ -202,6 +205,7 @@ export function defaultMemoize<Func extends AnyFunction>(

if (matchingEntry) {
value = matchingEntry.value
resultsCount--
markerikson marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -212,6 +216,13 @@ export function defaultMemoize<Func extends AnyFunction>(

memoized.clearCache = () => {
cache.clear()
memoized.resetResultsCount()
}

memoized.resultsCount = () => resultsCount

memoized.resetResultsCount = () => {
resultsCount = 0
}

return memoized as Func & Simplify<DefaultMemoizeFields>
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ export type DefaultMemoizeFields = {
* that future calls to the function recompute the results.
*/
clearCache: () => void
resultsCount: () => number
resetResultsCount: () => void
}

/*
Expand Down Expand Up @@ -464,7 +466,7 @@ export type FunctionType<T> = Extract<T, AnyFunction>
*/
export type ExtractReturnType<FunctionsArray extends readonly AnyFunction[]> = {
[Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number]
? FallbackIfUnknown<FallbackIfUnknown<ReturnType<FunctionsArray[Index]>, any>, any>
? FallbackIfUnknown<ReturnType<FunctionsArray[Index]>, any>
: never
}

Expand Down
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import type {
UnknownMemoizer
} from './types'

export const NOT_FOUND = 'NOT_FOUND'
export type NOT_FOUND_TYPE = typeof NOT_FOUND

/**
* Assert that the provided value is a function. If the assertion fails,
* a `TypeError` is thrown with an optional custom error message.
Expand Down
77 changes: 71 additions & 6 deletions src/weakMapMemoize.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
// Original source:
// - https://github.com/facebook/react/blob/0b974418c9a56f6c560298560265dcf4b65784bc/packages/react/src/ReactCache.js

import type { AnyFunction, DefaultMemoizeFields, Simplify } from './types'
import type {
AnyFunction,
DefaultMemoizeFields,
EqualityFn,
Simplify
} from './types'

class StrongRef<T> {
constructor(private value: T) {}
deref() {
return this.value
}
}

const Ref = WeakRef ?? StrongRef

const UNTERMINATED = 0
const TERMINATED = 1
Expand Down Expand Up @@ -55,6 +69,22 @@ function createCacheNode<T>(): CacheNode<T> {
}
}

/**
* @public
*/
export interface WeakMapMemoizeOptions {
/**
* If provided, used to compare a newly generated output value against previous values in the cache.
* If a match is found, the old value is returned. This addresses the common
* ```ts
* todos.map(todo => todo.id)
* ```
* use case, where an update to another field in the original data causes a recalculation
* due to changed references, but the output is still effectively the same.
*/
resultEqualityCheck?: EqualityFn
}

/**
* Creates a tree of `WeakMap`-based cache nodes based on the identity of the
* arguments it's been called with (in this case, the extracted values from your input selectors).
Expand Down Expand Up @@ -128,8 +158,16 @@ function createCacheNode<T>(): CacheNode<T> {
* @public
* @experimental
*/
export function weakMapMemoize<Func extends AnyFunction>(func: Func) {
export function weakMapMemoize<Func extends AnyFunction>(
func: Func,
options: WeakMapMemoizeOptions = {}
) {
let fnNode = createCacheNode()
const { resultEqualityCheck } = options

let lastResult: WeakRef<object> | undefined

let resultsCount = 0

function memoized() {
let cacheNode = fnNode
Expand Down Expand Up @@ -167,19 +205,46 @@ export function weakMapMemoize<Func extends AnyFunction>(func: Func) {
}
}
}

const terminatedNode = cacheNode as unknown as TerminatedCacheNode<any>

let result

if (cacheNode.s === TERMINATED) {
return cacheNode.v
result = cacheNode.v
} else {
// Allow errors to propagate
result = func.apply(null, arguments as unknown as any[])
resultsCount++
}
// Allow errors to propagate
const result = func.apply(null, arguments as unknown as any[])
const terminatedNode = cacheNode as unknown as TerminatedCacheNode<any>

terminatedNode.s = TERMINATED

if (resultEqualityCheck) {
const lastResultValue = lastResult?.deref() ?? lastResult
if (lastResultValue != null && resultEqualityCheck(lastResultValue, result)) {
result = lastResultValue
resultsCount !== 0 && resultsCount--
}

const needsWeakRef =
(typeof result === 'object' && result !== null) ||
typeof result === 'function'
lastResult = needsWeakRef ? new Ref(result) : result
}
terminatedNode.v = result
return result
}

memoized.clearCache = () => {
fnNode = createCacheNode()
memoized.resetResultsCount()
}

memoized.resultsCount = () => resultsCount

memoized.resetResultsCount = () => {
resultsCount = 0
}

return memoized as Func & Simplify<DefaultMemoizeFields>
Expand Down
Loading