diff --git a/README.md b/README.md index 586926be0..25c1bf560 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or s | `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | | `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | | `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
since 5.0.0 | -| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
since 5.0.0 | +| `options.devModeChecks?` | Overrides the settings for the global development mode checks for the selector.
since 5.0.0 | | `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). since 5.0.0 | | `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
since 5.0.0 | @@ -1055,7 +1055,7 @@ const selectTodoIds = createSelectorAutotrack( -### Development-Only Stability Checks +### Development-Only Checks Reselect includes extra checks in development mode to help catch and warn about mistakes in selector behavior. @@ -1079,7 +1079,7 @@ that will cause the selector to never memoize properly. Since this is a common mistake, we've added a development mode check to catch this. By default, [`createSelector`] will now run the [input selectors] twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. ```ts -type StabilityCheckFrequency = 'always' | 'once' | 'never' +type DevModeCheckFrequency = 'always' | 'once' | 'never' ``` | Possible Values | Description | @@ -1093,43 +1093,112 @@ type StabilityCheckFrequency = 'always' | 'once' | 'never' You can configure this behavior in two ways: - + -##### 1. Globally through `setInputStabilityCheckEnabled`: +##### 1. Globally through `setGlobalDevModeChecks`: -A `setInputStabilityCheckEnabled` function is exported from Reselect, which should be called with the desired setting. +A `setGlobalDevModeChecks` function is exported from Reselect, which should be called with the desired setting. ```ts -import { setInputStabilityCheckEnabled } from 'reselect' +import { setGlobalDevModeChecks } from 'reselect' // Run only the first time the selector is called. (default) -setInputStabilityCheckEnabled('once') +setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) // Run every time the selector is called. -setInputStabilityCheckEnabled('always') +setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) // Never run the input stability check. -setInputStabilityCheckEnabled('never') +setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) ``` ##### 2. Per selector by passing an `inputStabilityCheck` option directly to [`createSelector`]: ```ts -// Create a selector that double-checks the results of [`input selectors`][Input Selectors] every time it runs. +// Create a selector that double-checks the results of input selectors every time it runs. const selectCompletedTodosLength = createSelector( [ - // This `input selector` will not be memoized properly since it always returns a new reference. + // ❌ Incorrect Use Case: This input selector will not be memoized properly since it always returns a new reference. (state: RootState) => state.todos.filter(({ completed }) => completed === true) ], completedTodos => completedTodos.length, // Will override the global setting. - { inputStabilityCheck: 'always' } + { devModeChecks: { inputStabilityCheck: 'always' } } ) ``` > [!WARNING] -> This will override the global input stability check set by calling `setInputStabilityCheckEnabled`. +> This will override the global input stability check set by calling `setGlobalDevModeChecks`. + + + +#### `identityFunctionCheck` + +When working with Reselect, it's crucial to adhere to a fundamental philosophy regarding the separation of concerns between extraction and transformation logic. + +- **Extraction Logic**: This refers to operations like `state => state.todos`, which should be placed in [input selectors]. Extraction logic is responsible for retrieving or 'selecting' data from a broader state or dataset. + +- **Transformation Logic**: In contrast, transformation logic, such as `todos => todos.map(({ id }) => id)`, belongs in the [result function]. This is where you manipulate, format, or transform the data extracted by the input selectors. + +Most importantly, effective memoization in Reselect hinges on following these guidelines. Memoization, only functions correctly when extraction and transformation logic are properly segregated. By keeping extraction logic in input selectors and transformation logic in the result function, Reselect can efficiently determine when to reuse cached results and when to recompute them. This not only enhances performance but also ensures the consistency and predictability of your selectors. + +For memoization to work as intended, it's imperative to follow both guidelines. If either is disregarded, memoization will not function properly. Consider the following example for clarity: + +```ts +// ❌ Incorrect Use Case: This will not memoize correctly, and does nothing useful! +const brokenSelector = createSelector( + // ✔️ GOOD: Contains extraction logic. + [(state: RootState) => state.todos], + // ❌ BAD: Does not contain transformation logic. + todos => todos +) +``` + +```ts +type DevModeCheckFrequency = 'always' | 'once' | 'never' +``` + +| Possible Values | Description | +| :-------------- | :---------------------------------------------- | +| `once` | Run only the first time the selector is called. | +| `always` | Run every time the selector is called. | +| `never` | Never run the identity function check. | + +> [!IMPORTANT] +> The identity function check is automatically disabled in production environments. + +You can configure this behavior in two ways: + + + +##### 1. Globally through `setGlobalDevModeChecks`: + +```ts +import { setGlobalDevModeChecks } from 'reselect' + +// Run only the first time the selector is called. (default) +setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) + +// Run every time the selector is called. +setGlobalDevModeChecks({ identityFunctionCheck: 'always' }) + +// Never run the identity function check. +setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) +``` + +##### 2. Per selector by passing an `identityFunctionCheck` option directly to [`createSelector`]: + +```ts +// Create a selector that checks to see if the result function is an identity function. +const selectTodos = createSelector( + [(state: RootState) => state.todos], + // This result function does not contain any transformation logic. + todos => todos, + // Will override the global setting. + { devModeChecks: { identityFunctionCheck: 'always' } } +) +``` @@ -1184,6 +1253,7 @@ Version 5.0.0 introduces several new features and improvements: - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the [output selector fields]. These additions provide greater control and insight over [input selectors], complementing the new `argsMemoize` API. - Introduced `inputStabilityCheck`, a development tool that runs the [input selectors] twice using the same arguments and triggers a warning If they return differing results for the same call. + - Introduced `identityFunctionCheck`, a development tool that checks to see if the [result function] returns its own input. These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. diff --git a/package.json b/package.json index 56196ebc3..107563e02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reselect", - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "description": "Selectors for Redux.", "main": "./dist/cjs/reselect.cjs", "module": "./dist/reselect.legacy-esm.js", diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 51ee1030d..92553655a 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -13,7 +13,6 @@ import type { SelectorArray, SetRequired, Simplify, - StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -22,8 +21,7 @@ import { collectInputSelectorResults, ensureIsArray, getDependencies, - runStabilityCheck, - shouldRunInputStabilityCheck + getDevModeChecksExecutionInfo } from './utils' /** @@ -143,50 +141,6 @@ export interface CreateSelectorFunction< InterruptRecursion } -let globalStabilityCheck: StabilityCheckFrequency = 'once' - -/** - * In development mode, an extra check is conducted on your input selectors. - * It runs your input selectors an extra time with the same arguments, and - * warns in the console if they return a different result _(based on your `memoize` method)_. - * - * This function allows you to override this setting for all of your selectors. - * - * **Note**: This setting can still be overridden per selector inside `createSelector`'s `options` object. - * See {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} - * and {@linkcode CreateSelectorOptions.inputStabilityCheck inputStabilityCheck} for more details. - * - * _The input stability check does not run in production builds._ - * - * @param inputStabilityCheckFrequency - How often the `inputStabilityCheck` should run for all selectors. - * - * @example - * ```ts - * import { setInputStabilityCheckEnabled } from 'reselect' -import { assert } from './autotrackMemoize/utils'; -import { OutputSelectorFields, Mapped } from './types'; - * - * // Run only the first time the selector is called. (default) - * setInputStabilityCheckEnabled('once') - * - * // Run every time the selector is called. - * setInputStabilityCheckEnabled('always') - * - * // Never run the input stability check. - * setInputStabilityCheckEnabled('never') - * ``` - * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} - * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setinputstabilitycheckenabled global-configuration} - * - * @since 5.0.0 - * @public - */ -export function setInputStabilityCheckEnabled( - inputStabilityCheckFrequency: StabilityCheckFrequency -) { - globalStabilityCheck = inputStabilityCheckFrequency -} - /** * Creates a selector creator function with the specified memoization function and options for customizing memoization behavior. * @@ -374,7 +328,7 @@ export function createSelectorCreator< memoizeOptions = [], argsMemoize = weakMapMemoize, argsMemoizeOptions = [], - inputStabilityCheck = globalStabilityCheck + devModeChecks = {} } = combinedOptions // Simplifying assumption: it's unlikely that the first options arg of the provided memoizer @@ -408,21 +362,28 @@ export function createSelectorCreator< arguments ) - if ( - process.env.NODE_ENV !== 'production' && - shouldRunInputStabilityCheck(inputStabilityCheck, firstRun) - ) { - // make a second copy of the params, to check if we got the same results - const inputSelectorResultsCopy = collectInputSelectorResults( - dependencies, - arguments - ) + if (process.env.NODE_ENV !== 'production') { + const { identityFunctionCheck, inputStabilityCheck } = + getDevModeChecksExecutionInfo(firstRun, devModeChecks) + if (identityFunctionCheck.shouldRun) { + identityFunctionCheck.run( + resultFunc as Combiner + ) + } + + if (inputStabilityCheck.shouldRun) { + // make a second copy of the params, to check if we got the same results + const inputSelectorResultsCopy = collectInputSelectorResults( + dependencies, + arguments + ) - runStabilityCheck( - { inputSelectorResults, inputSelectorResultsCopy }, - { memoize, memoizeOptions: finalMemoizeOptions }, - arguments - ) + inputStabilityCheck.run( + { inputSelectorResults, inputSelectorResultsCopy }, + { memoize, memoizeOptions: finalMemoizeOptions }, + arguments + ) + } if (firstRun) firstRun = false } diff --git a/src/devModeChecks/identityFunctionCheck.ts b/src/devModeChecks/identityFunctionCheck.ts new file mode 100644 index 000000000..dc4e3d450 --- /dev/null +++ b/src/devModeChecks/identityFunctionCheck.ts @@ -0,0 +1,29 @@ +import type { AnyFunction } from '../types' + +/** + * Runs a check to determine if the given result function behaves as an + * identity function. An identity function is one that returns its + * input unchanged, for example, `x => x`. This check helps ensure + * efficient memoization and prevent unnecessary re-renders by encouraging + * proper use of transformation logic in result functions and + * extraction logic in input selectors. + * + * @param resultFunc - The result function to be checked. + */ +export const runIdentityFunctionCheck = (resultFunc: AnyFunction) => { + let isInputSameAsOutput = false + try { + const emptyObject = {} + if (resultFunc(emptyObject) === emptyObject) isInputSameAsOutput = true + } catch { + // Do nothing + } + if (isInputSameAsOutput) { + console.warn( + 'The result function returned its own inputs without modification. e.g' + + '\n`createSelector([state => state.todos], todos => todos)`' + + '\nThis could lead to inefficient memoization and unnecessary re-renders.' + + '\nEnsure transformation logic is in the result function, and extraction logic is in the input selectors.' + ) + } +} diff --git a/src/devModeChecks/inputStabilityCheck.ts b/src/devModeChecks/inputStabilityCheck.ts new file mode 100644 index 000000000..4f62d418e --- /dev/null +++ b/src/devModeChecks/inputStabilityCheck.ts @@ -0,0 +1,47 @@ +import type { CreateSelectorOptions, UnknownMemoizer } from '../types' + +/** + * Runs a stability check to ensure the input selector results remain stable + * when provided with the same arguments. This function is designed to detect + * changes in the output of input selectors, which can impact the performance of memoized selectors. + * + * @param inputSelectorResultsObject - An object containing two arrays: `inputSelectorResults` and `inputSelectorResultsCopy`, representing the results of input selectors. + * @param options - Options object consisting of a `memoize` function and a `memoizeOptions` object. + * @param inputSelectorArgs - List of arguments being passed to the input selectors. + */ +export const runInputStabilityCheck = ( + inputSelectorResultsObject: { + inputSelectorResults: unknown[] + inputSelectorResultsCopy: unknown[] + }, + options: Required< + Pick< + CreateSelectorOptions, + 'memoize' | 'memoizeOptions' + > + >, + inputSelectorArgs: unknown[] | IArguments +) => { + const { memoize, memoizeOptions } = options + const { inputSelectorResults, inputSelectorResultsCopy } = + inputSelectorResultsObject + const createAnEmptyObject = memoize(() => ({}), ...memoizeOptions) + // if the memoize method thinks the parameters are equal, these *should* be the same reference + const areInputSelectorResultsEqual = + createAnEmptyObject.apply(null, inputSelectorResults) === + createAnEmptyObject.apply(null, inputSelectorResultsCopy) + if (!areInputSelectorResultsEqual) { + // do we want to log more information about the selector? + console.warn( + 'An input selector returned a different result when passed same arguments.' + + '\nThis means your output selector will likely run more frequently than intended.' + + '\nAvoid returning a new reference inside your input selector, e.g.' + + '\n`createSelector([state => state.todos.map(todo => todo.id)], todoIds => todoIds.length)`', + { + arguments: inputSelectorArgs, + firstInputs: inputSelectorResults, + secondInputs: inputSelectorResultsCopy + } + ) + } +} diff --git a/src/devModeChecks/setGlobalDevModeChecks.ts b/src/devModeChecks/setGlobalDevModeChecks.ts new file mode 100644 index 000000000..04d5f7164 --- /dev/null +++ b/src/devModeChecks/setGlobalDevModeChecks.ts @@ -0,0 +1,63 @@ +import type { DevModeChecks } from '../types' + +/** + * Global configuration for development mode checks. This specifies the default + * frequency at which each development mode check should be performed. + * + * @since 5.0.0 + * @internal + */ +export const globalDevModeChecks: DevModeChecks = { + inputStabilityCheck: 'once', + identityFunctionCheck: 'once' +} + +/** + * Overrides the development mode checks settings for all selectors. + * + * Reselect performs additional checks in development mode to help identify and + * warn about potential issues in selector behavior. This function allows you to + * customize the behavior of these checks across all selectors in your application. + * + * **Note**: This setting can still be overridden per selector inside `createSelector`'s `options` object. + * See {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} + * and {@linkcode CreateSelectorOptions.identityFunctionCheck identityFunctionCheck} for more details. + * + * _The development mode checks do not run in production builds._ + * + * @param devModeChecks - An object specifying the desired settings for development mode checks. You can provide partial overrides. Unspecified settings will retain their current values. + * + * @example + * ```ts + * import { setGlobalDevModeChecks } from 'reselect' +import { DevModeChecks } from '../types'; + * + * // Run only the first time the selector is called. (default) + * setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) + * + * // Run every time the selector is called. + * setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) + * + * // Never run the input stability check. + * setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) + * + * // Run only the first time the selector is called. (default) + * setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) + * + * // Run every time the selector is called. + * setGlobalDevModeChecks({ identityFunctionCheck: 'always' }) + * + * // Never run the identity function check. + * setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) + * ``` + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setglobaldevmodechecks global-configuration} + * + * @since 5.0.0 + * @public + */ +export const setGlobalDevModeChecks = ( + devModeChecks: Partial +) => { + Object.assign(globalDevModeChecks, devModeChecks) +} diff --git a/src/index.ts b/src/index.ts index 6a85cb051..fe1bd6573 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,5 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' -export { - createSelector, - createSelectorCreator, - setInputStabilityCheckEnabled -} from './createSelectorCreator' +export { createSelector, createSelectorCreator } from './createSelectorCreator' export type { CreateSelectorFunction } from './createSelectorCreator' export { createStructuredSelector } from './createStructuredSelector' export type { @@ -12,10 +8,14 @@ export type { } from './createStructuredSelector' export { defaultEqualityCheck, defaultMemoize } from './defaultMemoize' export type { DefaultMemoizeOptions } from './defaultMemoize' +export { setGlobalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' export type { Combiner, CreateSelectorOptions, DefaultMemoizeFields, + DevModeCheckFrequency, + DevModeChecks, + DevModeChecksExecutionInfo, EqualityFn, ExtractMemoizerFields, GetParamsFromSelectors, @@ -27,7 +27,6 @@ export type { Selector, SelectorArray, SelectorResultArray, - StabilityCheckFrequency, UnknownMemoizer } from './types' export { weakMapMemoize } from './weakMapMemoize' diff --git a/src/types.ts b/src/types.ts index a28640814..6ead83c92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,20 +68,15 @@ export interface CreateSelectorOptions< OverrideArgsMemoizeFunction extends UnknownMemoizer = never > { /** - * Overrides the global input stability check for the selector. - * - `once` - Run only the first time the selector is called. - * - `always` - Run every time the selector is called. - * - `never` - Never run the input stability check. - * - * @default 'once' + * Reselect performs additional checks in development mode to help identify + * and warn about potential issues in selector behavior. This option + * allows you to customize the behavior of these checks per selector. * * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} - * @see {@link https://github.com/reduxjs/reselect#inputstabilitycheck inputStabilityCheck} - * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} * * @since 5.0.0 */ - inputStabilityCheck?: StabilityCheckFrequency + devModeChecks?: Partial /** * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} @@ -298,12 +293,72 @@ export type Combiner = Distribute< export type EqualityFn = (a: T, b: T) => boolean /** - * The frequency of input stability checks. + * The frequency of development mode checks. + * + * @since 5.0.0 + * @public + */ +export type DevModeCheckFrequency = 'always' | 'once' | 'never' + +/** + * Represents the configuration for development mode checks. * * @since 5.0.0 * @public */ -export type StabilityCheckFrequency = 'always' | 'once' | 'never' +export interface DevModeChecks { + /** + * Overrides the global input stability check for the selector. + * - `once` - Run only the first time the selector is called. + * - `always` - Run every time the selector is called. + * - `never` - Never run the input stability check. + * + * @default 'once' + * + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#inputstabilitycheck inputStabilityCheck} + * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} + * + * @since 5.0.0 + */ + inputStabilityCheck: DevModeCheckFrequency + + /** + * Overrides the global identity function check for the selector. + * - `once` - Run only the first time the selector is called. + * - `always` - Run every time the selector is called. + * - `never` - Never run the identity function check. + * + * @default 'once' + * + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#identityfunctioncheck identityFunctionCheck} + * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} + * + * @since 5.0.0 + */ + identityFunctionCheck: DevModeCheckFrequency +} + +/** + * Represents execution information for development mode checks. + * + * @public + * @since 5.0.0 + */ +export type DevModeChecksExecutionInfo = { + [K in keyof DevModeChecks]: { + /** + * A boolean indicating whether the check should be executed. + */ + shouldRun: boolean + + /** + * The function to execute for the check. + */ + run: AnyFunction + } +} /** * Determines the combined single "State" type (first arg) from all input selectors. diff --git a/src/utils.ts b/src/utils.ts index b49b92282..756740515 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,12 @@ +import { runIdentityFunctionCheck } from './devModeChecks/identityFunctionCheck' +import { runInputStabilityCheck } from './devModeChecks/inputStabilityCheck' +import { globalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { - CreateSelectorOptions, + DevModeChecks, Selector, SelectorArray, - StabilityCheckFrequency, - UnknownMemoizer + DevModeChecksExecutionInfo } from './types' export const NOT_FOUND = 'NOT_FOUND' @@ -122,64 +125,32 @@ export function collectInputSelectorResults( } /** - * Run a stability check to ensure the input selector results remain stable - * when provided with the same arguments. This function is designed to detect - * changes in the output of input selectors, which can impact the performance of memoized selectors. + * Retrieves execution information for development mode checks. * - * @param inputSelectorResultsObject - An object containing two arrays: `inputSelectorResults` and `inputSelectorResultsCopy`, representing the results of input selectors. - * @param options - Options object consisting of a `memoize` function and a `memoizeOptions` object. - * @param inputSelectorArgs - List of arguments being passed to the input selectors. - */ -export function runStabilityCheck( - inputSelectorResultsObject: { - inputSelectorResults: unknown[] - inputSelectorResultsCopy: unknown[] - }, - options: Required< - Pick< - CreateSelectorOptions, - 'memoize' | 'memoizeOptions' - > - >, - inputSelectorArgs: unknown[] | IArguments -) { - const { memoize, memoizeOptions } = options - const { inputSelectorResults, inputSelectorResultsCopy } = - inputSelectorResultsObject - const createAnEmptyObject = memoize(() => ({}), ...memoizeOptions) - // if the memoize method thinks the parameters are equal, these *should* be the same reference - const areInputSelectorResultsEqual = - createAnEmptyObject.apply(null, inputSelectorResults) === - createAnEmptyObject.apply(null, inputSelectorResultsCopy) - if (!areInputSelectorResultsEqual) { - // do we want to log more information about the selector? - console.warn( - 'An input selector returned a different result when passed same arguments.' + - '\nThis means your output selector will likely run more frequently than intended.' + - '\nAvoid returning a new reference inside your input selector, e.g.' + - '\n`createSelector([(arg1, arg2) => ({ arg1, arg2 })],(arg1, arg2) => {})`', - { - arguments: inputSelectorArgs, - firstInputs: inputSelectorResults, - secondInputs: inputSelectorResultsCopy - } - ) - } -} - -/** - * Determines if the input stability check should run. - * - * @param inputStabilityCheck - The frequency of the input stability check. + * @param devModeChecks - Custom Settings for development mode checks. These settings will override the global defaults. * @param firstRun - Indicates whether it is the first time the selector has run. - * @returns true if the input stability check should run, otherwise false. + * @returns An object containing the execution information for each development mode check. */ -export const shouldRunInputStabilityCheck = ( - inputStabilityCheck: StabilityCheckFrequency, - firstRun: boolean +export const getDevModeChecksExecutionInfo = ( + firstRun: boolean, + devModeChecks: Partial ) => { - return ( - inputStabilityCheck === 'always' || - (inputStabilityCheck === 'once' && firstRun) - ) + const { identityFunctionCheck, inputStabilityCheck } = { + ...globalDevModeChecks, + ...devModeChecks + } + return { + identityFunctionCheck: { + shouldRun: + identityFunctionCheck === 'always' || + (identityFunctionCheck === 'once' && firstRun), + run: runIdentityFunctionCheck + }, + inputStabilityCheck: { + shouldRun: + inputStabilityCheck === 'always' || + (inputStabilityCheck === 'once' && firstRun), + run: runInputStabilityCheck + } + } satisfies DevModeChecksExecutionInfo } diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 10092b40a..fdac93660 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -1,7 +1,7 @@ // This entire implementation courtesy of Anders Hjelsberg: // https://github.com/microsoft/TypeScript/pull/50831#issuecomment-1253830522 -import type { AnyFunction } from '@internal/types' +import type { AnyFunction } from '../types' /** * Represents the longest array within an array of arrays. diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 726f18d16..4e21732c5 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -1,7 +1,8 @@ import { - createSelectorCreator, - unstable_autotrackMemoize as autotrackMemoize + unstable_autotrackMemoize as autotrackMemoize, + createSelectorCreator } from 'reselect' +import { setEnvToProd } 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 @@ -33,7 +34,8 @@ describe('Basic selector behavior with autotrack', () => { // console.log('Selector test') const selector = createSelector( (state: StateA) => state.a, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -51,7 +53,8 @@ describe('Basic selector behavior with autotrack', () => { test("don't pass extra parameters to inputSelector when only called with the state", () => { const selector = createSelector( (...params: any[]) => params.length, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({})).toBe(1) }) @@ -95,30 +98,23 @@ describe('Basic selector behavior with autotrack', () => { ) }) - describe('performance checks', () => { - const originalEnv = process.env.NODE_ENV + const isCoverage = process.env.COVERAGE - beforeAll(() => { - process.env.NODE_ENV = 'production' - }) - afterAll(() => { - process.env.NODE_NV = originalEnv - }) + // don't run performance tests for coverage + describe.skipIf(isCoverage)('performance checks', () => { + beforeAll(setEnvToProd) test('basic selector cache hit performance', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, - (a, b) => a + b + (a, b) => a + b, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1, b: 2 } const start = performance.now() - for (let i = 0; i < 1000000; i++) { + for (let i = 0; i < 1_000_000; i++) { selector(state1) } const totalTime = performance.now() - start @@ -130,18 +126,15 @@ describe('Basic selector behavior with autotrack', () => { }) test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, - (a, b) => a + b + (a, b) => a + b, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const start = performance.now() - for (let i = 0; i < 1000000; i++) { + for (let i = 0; i < 1_000_000; i++) { selector(states[i]) } const totalTime = performance.now() - start @@ -203,7 +196,8 @@ describe('Basic selector behavior with autotrack', () => { () => { called++ throw Error('test error') - } + }, + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') @@ -218,7 +212,8 @@ describe('Basic selector behavior with autotrack', () => { called++ if (a > 1) throw Error('test error') return a - } + }, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1 } const state2 = { a: 2 } diff --git a/test/computationComparisons.spec.tsx b/test/computationComparisons.spec.tsx index 484306a42..a939c8588 100644 --- a/test/computationComparisons.spec.tsx +++ b/test/computationComparisons.spec.tsx @@ -9,10 +9,11 @@ import { Provider, shallowEqual, useSelector } from 'react-redux' import { createSelector, unstable_autotrackMemoize, - weakMapMemoize + weakMapMemoize, + defaultMemoize } from 'reselect' -import type { OutputSelector, defaultMemoize } from 'reselect' +import type { OutputSelector } from 'reselect' import type { RootState, Todo } from './testUtils' import { addTodo, @@ -97,7 +98,10 @@ describe('Computations and re-rendering with React components', () => { const selectTodoByIdResultEquality = createSelector( [selectTodos, selectTodoId], mapTodoById, - { memoizeOptions: { resultEqualityCheck: shallowEqual, maxSize: 500 } } + { + memoize: defaultMemoize, + memoizeOptions: { resultEqualityCheck: shallowEqual, maxSize: 500 } + } ) const selectTodoByIdWeakMap = createSelector( @@ -176,77 +180,70 @@ describe('Computations and re-rendering with React components', () => { ] ] - test.each(testCases)( - `%s`, - async ( - name, - selectTodoIds, - selectTodoById - ) => { - selectTodoIds.resetRecomputations() - selectTodoIds.resetDependencyRecomputations() - selectTodoById.resetRecomputations() - selectTodoById.resetDependencyRecomputations() - selectTodoIds.memoizedResultFunc.resetResultsCount() - selectTodoById.memoizedResultFunc.resetResultsCount() - - const numTodos = store.getState().todos.length - rtl.render( - - - - ) - - console.log(`Recomputations after render (${name}): `) - console.log('selectTodoIds: ') - logSelectorRecomputations(selectTodoIds as any) - console.log('selectTodoById: ') - logSelectorRecomputations(selectTodoById as any) - - console.log('Render count: ', { - listRenders, - listItemRenders, - listItemMounts - }) - - expect(listItemRenders).toBe(numTodos) - - rtl.act(() => { - store.dispatch(toggleCompleted(3)) - }) - - console.log(`\nRecomputations after toggle completed (${name}): `) - console.log('selectTodoIds: ') - logSelectorRecomputations(selectTodoIds as any) - console.log('selectTodoById: ') - logSelectorRecomputations(selectTodoById as any) - - console.log('Render count: ', { - listRenders, - listItemRenders, - listItemMounts - }) - - rtl.act(() => { - store.dispatch(addTodo({ title: 'a', description: 'b' })) - }) - - console.log(`\nRecomputations after added (${name}): `) - console.log('selectTodoIds: ') - logSelectorRecomputations(selectTodoIds as any) - console.log('selectTodoById: ') - logSelectorRecomputations(selectTodoById as any) - - console.log('Render count: ', { - listRenders, - listItemRenders, - listItemMounts - }) - } - ) + test.each(testCases)(`%s`, async (name, selectTodoIds, selectTodoById) => { + selectTodoIds.resetRecomputations() + selectTodoIds.resetDependencyRecomputations() + selectTodoById.resetRecomputations() + selectTodoById.resetDependencyRecomputations() + selectTodoIds.memoizedResultFunc.resetResultsCount() + selectTodoById.memoizedResultFunc.resetResultsCount() + + const numTodos = store.getState().todos.length + rtl.render( + + + + ) + + // console.log(`Recomputations after render (${name}): `) + // console.log('selectTodoIds: ') + // logSelectorRecomputations(selectTodoIds as any) + // console.log('selectTodoById: ') + // logSelectorRecomputations(selectTodoById as any) + + // console.log('Render count: ', { + // listRenders, + // listItemRenders, + // listItemMounts + // }) + + expect(listItemRenders).toBe(numTodos) + + rtl.act(() => { + store.dispatch(toggleCompleted(3)) + }) + + // console.log(`\nRecomputations after toggle completed (${name}): `) + // console.log('selectTodoIds: ') + // logSelectorRecomputations(selectTodoIds as any) + // console.log('selectTodoById: ') + // logSelectorRecomputations(selectTodoById as any) + + // console.log('Render count: ', { + // listRenders, + // listItemRenders, + // listItemMounts + // }) + + rtl.act(() => { + store.dispatch(addTodo({ title: 'a', description: 'b' })) + }) + + // console.log(`\nRecomputations after added (${name}): `) + // console.log('selectTodoIds: ') + // // logSelectorRecomputations(selectTodoIds as any) + // console.log('selectTodoById: ') + // // logSelectorRecomputations(selectTodoById as any) + + // console.log('Render count: ', { + // listRenders, + // listItemRenders, + // listItemMounts + // }) + }) }) describe('resultEqualityCheck in weakMapMemoize', () => { diff --git a/test/defaultMemoize.spec.ts b/test/defaultMemoize.spec.ts index 7d3237f62..502c29d44 100644 --- a/test/defaultMemoize.spec.ts +++ b/test/defaultMemoize.spec.ts @@ -274,12 +274,12 @@ describe('defaultMemoize', () => { ) fooChangeHandler(state) - expect(fooChangeSpy.mock.calls.length).toEqual(1) + expect(fooChangeSpy.mock.calls.length).toEqual(2) // no change fooChangeHandler(state) // this would fail - expect(fooChangeSpy.mock.calls.length).toEqual(1) + expect(fooChangeSpy.mock.calls.length).toEqual(2) const state2 = { a: 1 } let count = 0 @@ -290,9 +290,9 @@ describe('defaultMemoize', () => { }) selector(state) - expect(count).toBe(1) + expect(count).toBe(2) selector(state) - expect(count).toBe(1) + expect(count).toBe(2) }) test('Accepts an options object as an arg', () => { @@ -368,7 +368,8 @@ describe('defaultMemoize', () => { return state }, { - memoizeOptions: { maxSize: 3 } + memoizeOptions: { maxSize: 3 }, + devModeChecks: { identityFunctionCheck: 'never' } } ) @@ -410,13 +411,7 @@ describe('defaultMemoize', () => { // 'a' here would _not_ recalculate selector('b') // ['b'] expect(funcCalls).toBe(5) - - try { - //@ts-expect-error issue 591 - selector.resultFunc.clearCache() - fail('should have thrown for issue 591') - } catch (err) { - //expected catch - } + // @ts-expect-error + expect(selector.resultFunc.clearCache).toBeUndefined() }) }) diff --git a/test/examples.test.ts b/test/examples.test.ts index f487097e7..9a54026e6 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -1,220 +1,220 @@ -import type { - OutputSelector, - Selector, - SelectorArray, - UnknownMemoizer -} from 'reselect' -import { - createSelector, - createSelectorCreator, - defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import { test } from 'vitest' -import type { RootState } from './testUtils' -import { addTodo, setupStore } from './testUtils' - -const store = setupStore() - -const EMPTY_ARRAY: [] = [] - -export const fallbackToEmptyArray = (array: T[]) => { - return array.length === 0 ? EMPTY_ARRAY : array -} - -const selectCompletedTodos = createSelector( - [(state: RootState) => state.todos], - todos => { - return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) - } -) - -const completedTodos = selectCompletedTodos(store.getState()) - -store.dispatch(addTodo({ title: '', description: '' })) - -test('empty array', () => { - expect(completedTodos).toBe(selectCompletedTodos(store.getState())) -}) - -test('identity', () => { - const identity = any>(func: Func) => func - const createNonMemoizedSelector = createSelectorCreator({ - memoize: identity, - argsMemoize: identity - }) - const nonMemoizedSelector = createNonMemoizedSelector( - [(state: RootState) => state.todos], - todos => todos.filter(todo => todo.completed === true), - { inputStabilityCheck: 'never' } - ) - - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - - expect(nonMemoizedSelector.recomputations()).toBe(3) -}) - -test.todo('Top Level Selectors', () => { - type TopLevelSelectors = { - [K in keyof State as K extends string - ? `select${Capitalize}` - : never]: Selector - } - - const topLevelSelectors: TopLevelSelectors = { - selectAlerts: state => state.alerts, - selectTodos: state => state.todos, - selectUsers: state => state.users - } -}) - -test.todo('Find Fastest Selector', () => { - const store = setupStore() - const selectTodoIds = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const findFastestSelector = ( - selector: S, - ...selectorArgs: Parameters - ) => { - const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] - const results = memoizeFuncs - .map(memoize => { - const alternateSelector = createSelector( - selector.dependencies as [...SelectorArray], - selector.resultFunc, - { memoize } - ) - const start = performance.now() - alternateSelector.apply(null, selectorArgs) - const time = performance.now() - start - return { name: memoize.name, time, selector: alternateSelector } - }) - .sort((a, b) => a.time - b.time) - const fastest = results.reduce((minResult, currentResult) => - currentResult.time < minResult.time ? currentResult : minResult - ) - const ratios = results - .filter(({ time }) => time !== fastest.time) - .map( - ({ time, name }) => - `\x1B[33m \x1B[1m${ - time / fastest.time - }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` - ) - if (fastest.selector.memoize.name !== selector.memoize.name) { - console.warn( - `The memoization method for \x1B[1;41m${ - selector.name - }\x1B[0m is \x1B[31m${ - selector.memoize.name - }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ - fastest.selector.memoize.name - }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ - fastest.name - }\x1B[0m because it is${ratios.join('\nand\n')}` - ) - } - return { results, fastest } as const - } -}) - -test('TypedCreateSelector', () => { - type TypedCreateSelector< - State, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - > = < - InputSelectors extends readonly Selector[], - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - ...createSelectorArgs: Parameters< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ) => ReturnType< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - const createAppSelector: TypedCreateSelector = createSelector - const selector = createAppSelector( - [state => state.todos, (state, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id)?.completed - ) -}) - -test('createCurriedSelector copy paste pattern', () => { - const state = store.getState() - const currySelector = < - State, - Result, - Params extends readonly any[], - AdditionalFields - >( - selector: ((state: State, ...args: Params) => Result) & AdditionalFields - ) => { - const curriedSelector = (...args: Params) => { - return (state: State) => { - return selector(state, ...args) - } - } - return Object.assign(curriedSelector, selector) - } - - const createCurriedSelector = < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - >( - ...args: Parameters< - typeof createSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ) => { - return currySelector(createSelector(...args)) - } - const selectTodoById = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - const selectTodoByIdCurried = createCurriedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - expect(selectTodoById(state, 0)).toStrictEqual( - selectTodoByIdCurried(0)(state) - ) - expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) - expect(selectTodoById.lastResult()).toBeDefined() - expect(selectTodoByIdCurried.lastResult()).toBeDefined() - expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) - expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) - expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( - selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) - ) - expect(selectTodoById.recomputations()).toBe( - selectTodoByIdCurried.recomputations() - ) - expect(selectTodoById.resultFunc(state.todos, 0)).toBe( - selectTodoByIdCurried.resultFunc(state.todos, 0) - ) -}) +import type { + OutputSelector, + Selector, + SelectorArray, + UnknownMemoizer +} from 'reselect' +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector, + createSelectorCreator, + defaultMemoize, + weakMapMemoize +} from 'reselect' +import { test } from 'vitest' +import type { RootState } from './testUtils' +import { addTodo, setupStore } from './testUtils' + +const store = setupStore() + +const EMPTY_ARRAY: [] = [] + +export const fallbackToEmptyArray = (array: T[]) => { + return array.length === 0 ? EMPTY_ARRAY : array +} + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) + } +) + +const completedTodos = selectCompletedTodos(store.getState()) + +store.dispatch(addTodo({ title: '', description: '' })) + +test('empty array', () => { + expect(completedTodos).toBe(selectCompletedTodos(store.getState())) +}) + +test('identity', () => { + const identity = any>(func: Func) => func + const createNonMemoizedSelector = createSelectorCreator({ + memoize: identity, + argsMemoize: identity + }) + const nonMemoizedSelector = createNonMemoizedSelector( + [(state: RootState) => state.todos], + todos => todos.filter(todo => todo.completed === true), + { devModeChecks: { inputStabilityCheck: 'never' } } + ) + + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + + expect(nonMemoizedSelector.recomputations()).toBe(3) +}) + +test.todo('Top Level Selectors', () => { + type TopLevelSelectors = { + [K in keyof State as K extends string + ? `select${Capitalize}` + : never]: Selector + } + + const topLevelSelectors: TopLevelSelectors = { + selectAlerts: state => state.alerts, + selectTodos: state => state.todos, + selectUsers: state => state.users + } +}) + +test.todo('Find Fastest Selector', () => { + const store = setupStore() + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const findFastestSelector = ( + selector: S, + ...selectorArgs: Parameters + ) => { + const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] + const results = memoizeFuncs + .map(memoize => { + const alternateSelector = createSelector( + selector.dependencies as [...SelectorArray], + selector.resultFunc, + { memoize } + ) + const start = performance.now() + alternateSelector.apply(null, selectorArgs) + const time = performance.now() - start + return { name: memoize.name, time, selector: alternateSelector } + }) + .sort((a, b) => a.time - b.time) + const fastest = results.reduce((minResult, currentResult) => + currentResult.time < minResult.time ? currentResult : minResult + ) + const ratios = results + .filter(({ time }) => time !== fastest.time) + .map( + ({ time, name }) => + `\x1B[33m \x1B[1m${ + time / fastest.time + }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` + ) + if (fastest.selector.memoize.name !== selector.memoize.name) { + console.warn( + `The memoization method for \x1B[1;41m${ + selector.name + }\x1B[0m is \x1B[31m${ + selector.memoize.name + }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ + fastest.selector.memoize.name + }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ + fastest.name + }\x1B[0m because it is${ratios.join('\nand\n')}` + ) + } + return { results, fastest } as const + } +}) + +test('TypedCreateSelector', () => { + type TypedCreateSelector< + State, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + > = < + InputSelectors extends readonly Selector[], + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => ReturnType< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + const createAppSelector: TypedCreateSelector = createSelector + const selector = createAppSelector( + [state => state.todos, (state, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id)?.completed + ) +}) + +test('createCurriedSelector copy paste pattern', () => { + const state = store.getState() + const currySelector = < + State, + Result, + Params extends readonly any[], + AdditionalFields + >( + selector: ((state: State, ...args: Params) => Result) & AdditionalFields + ) => { + const curriedSelector = (...args: Params) => { + return (state: State) => { + return selector(state, ...args) + } + } + return Object.assign(curriedSelector, selector) + } + + const createCurriedSelector = < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + >( + ...args: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => { + return currySelector(createSelector(...args)) + } + const selectTodoById = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectTodoByIdCurried = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + expect(selectTodoById(state, 0)).toStrictEqual( + selectTodoByIdCurried(0)(state) + ) + expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) + expect(selectTodoById.lastResult()).toBeDefined() + expect(selectTodoByIdCurried.lastResult()).toBeDefined() + expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) + expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) + expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) + ) + expect(selectTodoById.recomputations()).toBe( + selectTodoByIdCurried.recomputations() + ) + expect(selectTodoById.resultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.resultFunc(state.todos, 0) + ) +}) diff --git a/test/identityFunctionCheck.test.ts b/test/identityFunctionCheck.test.ts new file mode 100644 index 000000000..d2f60967d --- /dev/null +++ b/test/identityFunctionCheck.test.ts @@ -0,0 +1,167 @@ +import { createSelector, setGlobalDevModeChecks } from 'reselect' +import type { LocalTestContext, RootState } from './testUtils' +import { localTest } from './testUtils' + +describe('identityFunctionCheck', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const identityFunction = vi.fn((state: T) => state) + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction + ) + + afterEach(() => { + consoleSpy.mockClear() + identityFunction.mockClear() + badSelector.clearCache() + badSelector.memoizedResultFunc.clearCache() + }) + afterAll(() => { + consoleSpy.mockRestore() + }) + localTest( + 'calls the result function twice, and warns to console if result is the same as argument', + ({ state }) => { + const goodSelector = createSelector( + [(state: RootState) => state], + state => state.todos + ) + + expect(goodSelector(state)).toBe(state.todos) + + expect(consoleSpy).not.toHaveBeenCalled() + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + } + ) + + localTest('disables check if global setting is set to never', ({ state }) => { + setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledOnce() + + expect(consoleSpy).not.toHaveBeenCalled() + + setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) + }) + + localTest( + 'disables check if specified in the selector options', + ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction, + { devModeChecks: { identityFunctionCheck: 'never' } } + ) + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledOnce() + + expect(consoleSpy).not.toHaveBeenCalled() + } + ) + + localTest('disables check in production', ({ state }) => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledOnce() + + expect(consoleSpy).not.toHaveBeenCalled() + + process.env.NODE_ENV = originalEnv + }) + + localTest('allows running the check only once', ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction, + { devModeChecks: { identityFunctionCheck: 'once' } } + ) + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + const newState = { ...state } + + expect(badSelector(newState)).toBe(newState) + + expect(identityFunction).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) + + localTest('allows always running the check', () => { + const badSelector = createSelector([state => state], identityFunction, { + devModeChecks: { identityFunctionCheck: 'always' } + }) + + const state = {} + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(badSelector({ ...state })).toStrictEqual(state) + + expect(identityFunction).toHaveBeenCalledTimes(4) + + expect(consoleSpy).toHaveBeenCalledTimes(2) + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(4) + + expect(consoleSpy).toHaveBeenCalledTimes(2) + }) + + localTest('runs once when devModeChecks is an empty object', ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction, + { devModeChecks: {} } + ) + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + const newState = { ...state } + + expect(badSelector(newState)).toBe(newState) + + expect(identityFunction).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) + + localTest('uses the memoize provided', ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state.todos], + identityFunction + ) + expect(badSelector(state)).toBe(state.todos) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(badSelector({ ...state })).not.toBe(state) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) +}) diff --git a/test/inputStabilityCheck.spec.ts b/test/inputStabilityCheck.spec.ts index 3dc0d28e6..f419f4387 100644 --- a/test/inputStabilityCheck.spec.ts +++ b/test/inputStabilityCheck.spec.ts @@ -1,7 +1,7 @@ import { createSelector, defaultMemoize, - setInputStabilityCheckEnabled + setGlobalDevModeChecks } from 'reselect' import { shallowEqual } from 'react-redux' @@ -51,25 +51,25 @@ describe('inputStabilityCheck', () => { }) it('disables check if global setting is changed', () => { - setInputStabilityCheckEnabled('never') + setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) expect(addNums(1, 2)).toBe(3) - expect(unstableInput).toHaveBeenCalledTimes(1) + expect(unstableInput).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() - setInputStabilityCheckEnabled('once') + setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) }) it('disables check if specified in the selector options', () => { const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { - inputStabilityCheck: 'never' + devModeChecks: { inputStabilityCheck: 'never' } }) expect(addNums(1, 2)).toBe(3) - expect(unstableInput).toHaveBeenCalledTimes(1) + expect(unstableInput).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() }) @@ -90,7 +90,49 @@ describe('inputStabilityCheck', () => { it('allows running the check only once', () => { const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { - inputStabilityCheck: 'once' + devModeChecks: { inputStabilityCheck: 'once' } + }) + + expect(addNums(1, 2)).toBe(3) + + expect(unstableInput).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(addNums(2, 2)).toBe(4) + + expect(unstableInput).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) + + it('allows always running the check', () => { + const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { + devModeChecks: { inputStabilityCheck: 'always' } + }) + + expect(addNums(1, 2)).toBe(3) + + expect(unstableInput).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(addNums(2, 2)).toBe(4) + + expect(unstableInput).toHaveBeenCalledTimes(4) + + expect(consoleSpy).toHaveBeenCalledTimes(2) + + expect(addNums(1, 2)).toBe(3) + + expect(unstableInput).toHaveBeenCalledTimes(4) + + expect(consoleSpy).toHaveBeenCalledTimes(2) + }) + + it('runs once when devModeChecks is an empty object', () => { + const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { + devModeChecks: {} }) expect(addNums(1, 2)).toBe(3) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 0f872c62a..15619f03a 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -12,7 +12,13 @@ import { import type { OutputSelector, OutputSelectorFields } from 'reselect' import type { RootState } from './testUtils' -import { addTodo, deepClone, localTest, toggleCompleted } from './testUtils' +import { + addTodo, + deepClone, + 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 @@ -41,7 +47,8 @@ describe('Basic selector behavior', () => { test('basic selector', () => { const selector = createSelector( (state: StateA) => state.a, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -59,7 +66,8 @@ describe('Basic selector behavior', () => { test("don't pass extra parameters to inputSelector when only called with the state", () => { const selector = createSelector( (...params: any[]) => params.length, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({})).toBe(1) }) @@ -103,21 +111,13 @@ describe('Basic selector behavior', () => { ) }) - describe('performance checks', () => { - const originalEnv = process.env.NODE_ENV - - beforeAll(() => { - process.env.NODE_ENV = 'production' - }) - afterAll(() => { - process.env.NODE_ENV = originalEnv - }) + const isCoverage = process.env.COVERAGE - test('basic selector cache hit performance', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } + describe.skipIf(isCoverage)('performance checks', () => { + beforeAll(setEnvToProd) + // don't run performance tests for coverage + test.skipIf(isCoverage)('basic selector cache hit performance', () => { const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, @@ -126,7 +126,7 @@ describe('Basic selector behavior', () => { const state1 = { a: 1, b: 2 } const start = performance.now() - for (let i = 0; i < 1000000; i++) { + for (let i = 0; i < 1_000_000; i++) { selector(state1) } const totalTime = performance.now() - start @@ -137,34 +137,35 @@ describe('Basic selector behavior', () => { expect(totalTime).toBeLessThan(2000) }) - test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - - const selector = createSelector( - (state: StateAB) => state.a, - (state: StateAB) => state.b, - (a, b) => a + b - ) + // don't run performance tests for coverage + test.skipIf(isCoverage)( + 'basic selector cache hit performance for state changes but shallowly equal selector args', + () => { + const selector = createSelector( + (state: StateAB) => state.a, + (state: StateAB) => state.b, + (a, b) => a + b + ) - const start = new Date() - for (let i = 0; i < numOfStates; i++) { - selector(states[i]) - } - const totalTime = new Date().getTime() - start.getTime() + const start = new Date() + for (let i = 0; i < numOfStates; i++) { + selector(states[i]) + } + const totalTime = new Date().getTime() - start.getTime() - expect(selector(states[0])).toBe(3) - expect(selector.recomputations()).toBe(1) + expect(selector(states[0])).toBe(3) + expect(selector.recomputations()).toBe(1) - // Expected a million calls to a selector with the same arguments to take less than 1 second - expect(totalTime).toBeLessThan(2000) - }) + // Expected a million calls to a selector with the same arguments to take less than 1 second + expect(totalTime).toBeLessThan(2000) + } + ) }) test('memoized composite arguments', () => { const selector = createSelector( (state: StateSub) => state.sub, - sub => sub + sub => sub, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { sub: { a: 1 } } expect(selector(state1)).toEqual({ a: 1 }) @@ -214,7 +215,7 @@ describe('Basic selector behavior', () => { ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') - expect(called).toBe(2) + expect(called).toBe(3) }) test('memoizes previous result before exception', () => { @@ -225,7 +226,8 @@ describe('Basic selector behavior', () => { called++ if (a > 1) throw Error('test error') return a - } + }, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1 } const state2 = { a: 2 } @@ -240,7 +242,8 @@ describe('Combining selectors', () => { test('chained selector', () => { const selector1 = createSelector( (state: StateSub) => state.sub, - sub => sub + sub => sub, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const selector2 = createSelector(selector1, sub => sub.a) const state1 = { sub: { a: 1 } } @@ -301,7 +304,8 @@ describe('Combining selectors', () => { ) const selector = createOverridenSelector( (state: StateA) => state.a, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({ a: 1 })).toBe(1) expect(selector({ a: 2 })).toBe(1) // yes, really true @@ -405,8 +409,8 @@ describe('Customizing selectors', () => { (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { - inputStabilityCheck: 'always', memoize: defaultMemoize, + devModeChecks: { inputStabilityCheck: 'always' }, memoizeOptions: { equalityCheck: (a, b) => false, resultEqualityCheck: (a, b) => false @@ -1103,7 +1107,7 @@ describe('argsMemoize and memoize', () => { users => { return users.user.details.preferences.notifications.push.frequency }, - { inputStabilityCheck: 'never' } + { devModeChecks: { inputStabilityCheck: 'never' } } ) const start = performance.now() for (let i = 0; i < 10_000_000; i++) { diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index c493cc71e..281af2507 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -6,7 +6,11 @@ describe('createSelector exposed utils', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { memoize: defaultMemoize, argsMemoize: defaultMemoize } + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + devModeChecks: { identityFunctionCheck: 'never' } + } ) expect(selector({ a: 1 })).toBe(1) expect(selector({ a: 1 })).toBe(1) diff --git a/test/testUtils.ts b/test/testUtils.ts index 12c557f5b..530484f44 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 { - AnyFunction, - OutputSelector, - Selector, - SelectorArray, - Simplify -} from '../src/types' +import type { AnyFunction, OutputSelector, Simplify } from '../src/types' export interface Todo { id: number @@ -572,3 +566,11 @@ export const expensiveComputation = (times = 1_000_000) => { // Do nothing } } + +export const setEnvToProd = () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + return () => { + process.env.NODE_ENV = originalEnv + } +} diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 338441e54..8a68e3e17 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -1,4 +1,5 @@ -import { createSelectorCreator, weakMapMemoize } from 'reselect' +import { createSelector, createSelectorCreator, weakMapMemoize } from 'reselect' +import { setEnvToProd } 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 @@ -30,7 +31,8 @@ describe('Basic selector behavior with weakMapMemoize', () => { // console.log('Selector test') const selector = createSelector( (state: StateA) => state.a, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -48,7 +50,8 @@ describe('Basic selector behavior with weakMapMemoize', () => { test("don't pass extra parameters to inputSelector when only called with the state", () => { const selector = createSelector( (...params: any[]) => params.length, - a => a + a => a, + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({})).toBe(1) }) @@ -92,54 +95,6 @@ describe('Basic selector behavior with weakMapMemoize', () => { ) }) - test('basic selector cache hit performance', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - - const selector = createSelector( - (state: StateAB) => state.a, - (state: StateAB) => state.b, - (a, b) => a + b - ) - const state1 = { a: 1, b: 2 } - - const start = performance.now() - for (let i = 0; i < 1000000; i++) { - selector(state1) - } - const totalTime = performance.now() - start - - expect(selector(state1)).toBe(3) - expect(selector.recomputations()).toBe(1) - // Expected a million calls to a selector with the same arguments to take less than 2 seconds - expect(totalTime).toBeLessThan(200) - }) - - test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - - const selector = createSelector( - (state: StateAB) => state.a, - (state: StateAB) => state.b, - (a, b) => a + b - ) - - const start = performance.now() - for (let i = 0; i < 1000000; i++) { - selector(states[i]) - } - const totalTime = performance.now() - start - - expect(selector(states[0])).toBe(3) - expect(selector.recomputations()).toBe(1) - - // Expected a million calls to a selector with the same arguments to take less than 1 second - expect(totalTime).toBeLessThan(2000) - }) - test('memoized composite arguments', () => { const selector = createSelector( (state: StateSub) => state.sub, @@ -193,7 +148,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') - expect(called).toBe(2) + expect(called).toBe(3) }) test('memoizes previous result before exception', () => { @@ -204,7 +159,8 @@ describe('Basic selector behavior with weakMapMemoize', () => { called++ if (a > 1) throw Error('test error') return a - } + }, + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1 } const state2 = { a: 2 } @@ -214,3 +170,57 @@ describe('Basic selector behavior with weakMapMemoize', () => { expect(called).toBe(2) }) }) + +const isCoverage = process.env.COVERAGE + +// don't run performance tests for coverage +describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { + beforeAll(setEnvToProd) + + test('basic selector cache hit performance', () => { + const selector = createSelector( + (state: StateAB) => state.a, + (state: StateAB) => state.b, + (a, b) => a + b, + { devModeChecks: { identityFunctionCheck: 'never' } } + ) + const state1 = { a: 1, b: 2 } + + const start = performance.now() + for (let i = 0; i < 1_000_000; i++) { + selector(state1) + } + const totalTime = performance.now() - start + + expect(selector(state1)).toBe(3) + expect(selector.recomputations()).toBe(1) + // Expected a million calls to a selector with the same arguments to take less than 1 second + expect(totalTime).toBeLessThan(2000) + }) + + test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { + const selector = createSelector( + (state: StateAB) => state.a, + (state: StateAB) => state.b, + (a, b) => a + b, + { + devModeChecks: { + identityFunctionCheck: 'never', + inputStabilityCheck: 'never' + } + } + ) + + const start = performance.now() + for (let i = 0; i < 1_000_000; i++) { + selector(states[i]) + } + const totalTime = performance.now() - start + + expect(selector(states[0])).toBe(3) + expect(selector.recomputations()).toBe(1) + + // Expected a million calls to a selector with the same arguments to take less than 1 second + expect(totalTime).toBeLessThan(2000) + }) +}) diff --git a/type-tests/argsMemoize.test-d.ts b/type-tests/argsMemoize.test-d.ts index 2ed343480..b95bcbcf1 100644 --- a/type-tests/argsMemoize.test-d.ts +++ b/type-tests/argsMemoize.test-d.ts @@ -712,7 +712,7 @@ describe('memoize and argsMemoize', () => { (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, - inputStabilityCheck: 'never', + devModeChecks: { inputStabilityCheck: 'never' }, memoize: memoizeOne, argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index 808f4c081..a9f7b4c8e 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -687,7 +687,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, - inputStabilityCheck: 'never', + devModeChecks: { inputStabilityCheck: 'never' }, memoize: memoizeOne, argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] diff --git a/yarn.lock b/yarn.lock index 50b53803a..720ad626b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,9 +56,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/android-arm64@npm:0.19.7" +"@esbuild/android-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-arm64@npm:0.19.8" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -70,9 +70,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/android-arm@npm:0.19.7" +"@esbuild/android-arm@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-arm@npm:0.19.8" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -84,9 +84,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/android-x64@npm:0.19.7" +"@esbuild/android-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-x64@npm:0.19.8" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -98,9 +98,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/darwin-arm64@npm:0.19.7" +"@esbuild/darwin-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/darwin-arm64@npm:0.19.8" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -112,9 +112,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/darwin-x64@npm:0.19.7" +"@esbuild/darwin-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/darwin-x64@npm:0.19.8" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -126,9 +126,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/freebsd-arm64@npm:0.19.7" +"@esbuild/freebsd-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/freebsd-arm64@npm:0.19.8" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -140,9 +140,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/freebsd-x64@npm:0.19.7" +"@esbuild/freebsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/freebsd-x64@npm:0.19.8" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -154,9 +154,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-arm64@npm:0.19.7" +"@esbuild/linux-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-arm64@npm:0.19.8" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -168,9 +168,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-arm@npm:0.19.7" +"@esbuild/linux-arm@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-arm@npm:0.19.8" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -182,9 +182,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-ia32@npm:0.19.7" +"@esbuild/linux-ia32@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-ia32@npm:0.19.8" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -196,9 +196,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-loong64@npm:0.19.7" +"@esbuild/linux-loong64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-loong64@npm:0.19.8" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -210,9 +210,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-mips64el@npm:0.19.7" +"@esbuild/linux-mips64el@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-mips64el@npm:0.19.8" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -224,9 +224,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-ppc64@npm:0.19.7" +"@esbuild/linux-ppc64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-ppc64@npm:0.19.8" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -238,9 +238,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-riscv64@npm:0.19.7" +"@esbuild/linux-riscv64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-riscv64@npm:0.19.8" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -252,9 +252,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-s390x@npm:0.19.7" +"@esbuild/linux-s390x@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-s390x@npm:0.19.8" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -266,9 +266,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-x64@npm:0.19.7" +"@esbuild/linux-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-x64@npm:0.19.8" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -280,9 +280,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/netbsd-x64@npm:0.19.7" +"@esbuild/netbsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/netbsd-x64@npm:0.19.8" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -294,9 +294,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/openbsd-x64@npm:0.19.7" +"@esbuild/openbsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/openbsd-x64@npm:0.19.8" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -308,9 +308,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/sunos-x64@npm:0.19.7" +"@esbuild/sunos-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/sunos-x64@npm:0.19.8" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -322,9 +322,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/win32-arm64@npm:0.19.7" +"@esbuild/win32-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-arm64@npm:0.19.8" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -336,9 +336,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/win32-ia32@npm:0.19.7" +"@esbuild/win32-ia32@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-ia32@npm:0.19.8" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -350,9 +350,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/win32-x64@npm:0.19.7" +"@esbuild/win32-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-x64@npm:0.19.8" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -565,86 +565,86 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.5.2" +"@rollup/rollup-android-arm-eabi@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.6.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-android-arm64@npm:4.5.2" +"@rollup/rollup-android-arm64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-android-arm64@npm:4.6.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-darwin-arm64@npm:4.5.2" +"@rollup/rollup-darwin-arm64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.6.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-darwin-x64@npm:4.5.2" +"@rollup/rollup-darwin-x64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.6.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.5.2" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.6.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.5.2" +"@rollup/rollup-linux-arm64-gnu@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.6.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.5.2" +"@rollup/rollup-linux-arm64-musl@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.6.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.5.2" +"@rollup/rollup-linux-x64-gnu@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.6.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.5.2" +"@rollup/rollup-linux-x64-musl@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.6.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.5.2" +"@rollup/rollup-win32-arm64-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.6.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.5.2" +"@rollup/rollup-win32-ia32-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.6.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.5.2" +"@rollup/rollup-win32-x64-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.6.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -766,13 +766,13 @@ __metadata: linkType: hard "@types/react@npm:*, @types/react@npm:^18.2.38": - version: 18.2.38 - resolution: "@types/react@npm:18.2.38" + version: 18.2.39 + resolution: "@types/react@npm:18.2.39" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 71f8c167173d32252be8b2d3c1c76b3570b94d2fbbd139da86d146be453626f5777e12c2781559119637520dbef9f91cffe968f67b5901618f29226d49fad326 + checksum: 9bcb1f1f060f1bf8f4730fb1c7772d0323a6e707f274efee3b976c40d92af4677df4d88e9135faaacf34e13e02f92ef24eb7d0cbcf7fb75c1883f5623ccb19f4 languageName: node linkType: hard @@ -1337,22 +1337,22 @@ __metadata: linkType: hard "cacache@npm:^18.0.0": - version: 18.0.0 - resolution: "cacache@npm:18.0.0" + version: 18.0.1 + resolution: "cacache@npm:18.0.1" dependencies: "@npmcli/fs": ^3.1.0 fs-minipass: ^3.0.0 glob: ^10.2.2 lru-cache: ^10.0.1 minipass: ^7.0.3 - minipass-collect: ^1.0.2 + minipass-collect: ^2.0.1 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 p-map: ^4.0.0 ssri: ^10.0.0 tar: ^6.1.11 unique-filename: ^3.0.0 - checksum: 2cd6bf15551abd4165acb3a4d1ef0593b3aa2fd6853ae16b5bb62199c2faecf27d36555a9545c0e07dd03347ec052e782923bdcece724a24611986aafb53e152 + checksum: 5a0b3b2ea451a0379814dc1d3c81af48c7c6db15cd8f7d72e028501ae0036a599a99bbac9687bfec307afb2760808d1c7708e9477c8c70d2b166e7d80b162a23 languageName: node linkType: hard @@ -1932,31 +1932,31 @@ __metadata: linkType: hard "esbuild@npm:^0.19.3": - version: 0.19.7 - resolution: "esbuild@npm:0.19.7" - dependencies: - "@esbuild/android-arm": 0.19.7 - "@esbuild/android-arm64": 0.19.7 - "@esbuild/android-x64": 0.19.7 - "@esbuild/darwin-arm64": 0.19.7 - "@esbuild/darwin-x64": 0.19.7 - "@esbuild/freebsd-arm64": 0.19.7 - "@esbuild/freebsd-x64": 0.19.7 - "@esbuild/linux-arm": 0.19.7 - "@esbuild/linux-arm64": 0.19.7 - "@esbuild/linux-ia32": 0.19.7 - "@esbuild/linux-loong64": 0.19.7 - "@esbuild/linux-mips64el": 0.19.7 - "@esbuild/linux-ppc64": 0.19.7 - "@esbuild/linux-riscv64": 0.19.7 - "@esbuild/linux-s390x": 0.19.7 - "@esbuild/linux-x64": 0.19.7 - "@esbuild/netbsd-x64": 0.19.7 - "@esbuild/openbsd-x64": 0.19.7 - "@esbuild/sunos-x64": 0.19.7 - "@esbuild/win32-arm64": 0.19.7 - "@esbuild/win32-ia32": 0.19.7 - "@esbuild/win32-x64": 0.19.7 + version: 0.19.8 + resolution: "esbuild@npm:0.19.8" + dependencies: + "@esbuild/android-arm": 0.19.8 + "@esbuild/android-arm64": 0.19.8 + "@esbuild/android-x64": 0.19.8 + "@esbuild/darwin-arm64": 0.19.8 + "@esbuild/darwin-x64": 0.19.8 + "@esbuild/freebsd-arm64": 0.19.8 + "@esbuild/freebsd-x64": 0.19.8 + "@esbuild/linux-arm": 0.19.8 + "@esbuild/linux-arm64": 0.19.8 + "@esbuild/linux-ia32": 0.19.8 + "@esbuild/linux-loong64": 0.19.8 + "@esbuild/linux-mips64el": 0.19.8 + "@esbuild/linux-ppc64": 0.19.8 + "@esbuild/linux-riscv64": 0.19.8 + "@esbuild/linux-s390x": 0.19.8 + "@esbuild/linux-x64": 0.19.8 + "@esbuild/netbsd-x64": 0.19.8 + "@esbuild/openbsd-x64": 0.19.8 + "@esbuild/sunos-x64": 0.19.8 + "@esbuild/win32-arm64": 0.19.8 + "@esbuild/win32-ia32": 0.19.8 + "@esbuild/win32-x64": 0.19.8 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -2004,7 +2004,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: a5d979224d47ae0cc6685447eb8f1ceaf7b67f5eaeaac0246f4d589ff7d81b08e4502a6245298d948f13e9b571ac8556a6d83b084af24954f762b1cfe59dbe55 + checksum: 1dff99482ecbfcc642ec66c71e4dc5c73ce6aef68e8158a4937890b570e86a95959ac47e0f14785ba70df5a673ae4289df88a162e9759b02367ed28074cee8ba languageName: node linkType: hard @@ -3407,12 +3407,12 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" dependencies: - minipass: ^3.0.0 - checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 + minipass: ^7.0.3 + checksum: b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 languageName: node linkType: hard @@ -4308,21 +4308,21 @@ __metadata: linkType: hard "rollup@npm:^4.2.0": - version: 4.5.2 - resolution: "rollup@npm:4.5.2" - dependencies: - "@rollup/rollup-android-arm-eabi": 4.5.2 - "@rollup/rollup-android-arm64": 4.5.2 - "@rollup/rollup-darwin-arm64": 4.5.2 - "@rollup/rollup-darwin-x64": 4.5.2 - "@rollup/rollup-linux-arm-gnueabihf": 4.5.2 - "@rollup/rollup-linux-arm64-gnu": 4.5.2 - "@rollup/rollup-linux-arm64-musl": 4.5.2 - "@rollup/rollup-linux-x64-gnu": 4.5.2 - "@rollup/rollup-linux-x64-musl": 4.5.2 - "@rollup/rollup-win32-arm64-msvc": 4.5.2 - "@rollup/rollup-win32-ia32-msvc": 4.5.2 - "@rollup/rollup-win32-x64-msvc": 4.5.2 + version: 4.6.0 + resolution: "rollup@npm:4.6.0" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.6.0 + "@rollup/rollup-android-arm64": 4.6.0 + "@rollup/rollup-darwin-arm64": 4.6.0 + "@rollup/rollup-darwin-x64": 4.6.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.6.0 + "@rollup/rollup-linux-arm64-gnu": 4.6.0 + "@rollup/rollup-linux-arm64-musl": 4.6.0 + "@rollup/rollup-linux-x64-gnu": 4.6.0 + "@rollup/rollup-linux-x64-musl": 4.6.0 + "@rollup/rollup-win32-arm64-msvc": 4.6.0 + "@rollup/rollup-win32-ia32-msvc": 4.6.0 + "@rollup/rollup-win32-x64-msvc": 4.6.0 fsevents: ~2.3.2 dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -4353,7 +4353,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 0cf68670556753c290e07492c01ea7ce19b7d178b903a518c908f7f9a90c0abee4e91c3089849f05a47040fd59c5ac0f10a58e4ee704cf7b03b24667e72c368a + checksum: f0325de87cc70086297415d2780d99f6ba7f56605aa3174ae33dff842aab11c4f5206a1718b1a4872df090589ab71a3b60fdb383b7ee59cb276ad0752c3214c7 languageName: node linkType: hard