diff --git a/src/types.ts b/src/types.ts index e4bd854cd..7342aaff8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -464,7 +464,7 @@ export type FunctionType = Extract */ export type ExtractReturnType = { [Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number] - ? FallbackIfUnknown, any> + ? FallbackIfUnknown, any>, any> : never } diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts new file mode 100644 index 000000000..210b489d7 --- /dev/null +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -0,0 +1,213 @@ +import type { OutputSelector, Selector } from 'reselect' +import { createSelector, defaultMemoize } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + countRecomputations, + expensiveComputation, + logFunctionInfo, + logSelectorRecomputations, + resetSelector, + runMultipleTimes, + setFunctionNames, + setupStore, + toggleCompleted, + toggleRead +} from '../testUtils' + +describe('Less vs more computation in input selectors', () => { + const store = setupStore() + const runSelector = (selector: Selector) => { + runMultipleTimes(selector, 100, store.getState()) + } + const selectorLessInInput = createSelector( + [(state: RootState) => state.todos], + todos => { + expensiveComputation() + return todos.filter(todo => todo.completed) + } + ) + const selectorMoreInInput = createSelector( + [ + (state: RootState) => { + expensiveComputation() + return state.todos + } + ], + todos => todos.filter(todo => todo.completed) + ) + + const nonMemoized = countRecomputations((state: RootState) => { + expensiveComputation() + return state.todos.filter(todo => todo.completed) + }) + const commonOptions: Options = { + iterations: 10, + time: 0 + } + setFunctionNames({ selectorLessInInput, selectorMoreInInput, nonMemoized }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleRead(1)) + }, + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorLessInInput, + () => { + runSelector(selectorLessInInput) + }, + createOptions(selectorLessInInput, commonOptions) + ) + bench( + selectorMoreInInput, + () => { + runSelector(selectorMoreInInput) + }, + createOptions(selectorMoreInInput, commonOptions) + ) + bench( + nonMemoized, + () => { + runSelector(nonMemoized) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + nonMemoized.resetRecomputations() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo(nonMemoized, nonMemoized.recomputations()) + } + } + } + } + ) +}) + +// This benchmark is made to test to see at what point it becomes beneficial +// to use reselect to memoize a function that is a plain field accessor. +describe('Reselect vs standalone memoization for field access', () => { + const store = setupStore() + const runSelector = (selector: Selector) => { + runMultipleTimes(selector, 1_000_000, store.getState()) + } + const commonOptions: Options = { + // warmupIterations: 0, + // warmupTime: 0, + // iterations: 10, + // time: 0 + } + const fieldAccessorWithReselect = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + const fieldAccessorWithMemoize = countRecomputations( + defaultMemoize((state: RootState) => { + return state.users.appSettings + }) + ) + const nonMemoizedAccessor = countRecomputations( + (state: RootState) => state.users.appSettings + ) + + setFunctionNames({ + fieldAccessorWithReselect, + fieldAccessorWithMemoize, + nonMemoizedAccessor + }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + fieldAccessorWithReselect, + () => { + runSelector(fieldAccessorWithReselect) + }, + createOptions(fieldAccessorWithReselect, commonOptions) + ) + bench( + fieldAccessorWithMemoize, + () => { + runSelector(fieldAccessorWithMemoize) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + fieldAccessorWithMemoize.resetRecomputations() + fieldAccessorWithMemoize.clearCache() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo( + fieldAccessorWithMemoize, + fieldAccessorWithMemoize.recomputations() + ) + } + } + } + } + ) + bench( + nonMemoizedAccessor, + () => { + runSelector(nonMemoizedAccessor) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + nonMemoizedAccessor.resetRecomputations() + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logFunctionInfo( + nonMemoizedAccessor, + nonMemoizedAccessor.recomputations() + ) + } + } + } + } + ) +}) diff --git a/test/benchmarks/reselect.bench.ts b/test/benchmarks/reselect.bench.ts new file mode 100644 index 000000000..28d23a94c --- /dev/null +++ b/test/benchmarks/reselect.bench.ts @@ -0,0 +1,304 @@ +import { + createSelector, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import type { Options } from 'tinybench' +import { bench, describe } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +describe('Memoize methods comparison', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + const selectorArgsWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: autotrackMemoize } + ) + const selectorBothWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const selectorBothAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: RootState) => { + return state.todos.map(({ id }) => id) + } + setFunctionNames({ + selectorDefault, + selectorAutotrack, + selectorWeakMap, + selectorArgsAutotrack, + nonMemoizedSelector, + selectorArgsWeakMap + }) + bench( + selectorDefault, + () => { + selectorDefault(state) + }, + commonOptions + ) + bench( + selectorAutotrack, + () => { + selectorAutotrack(state) + }, + commonOptions + ) + bench( + selectorWeakMap, + () => { + selectorWeakMap(state) + }, + commonOptions + ) + bench( + selectorArgsAutotrack, + () => { + selectorArgsAutotrack(state) + }, + commonOptions + ) + bench( + selectorArgsWeakMap, + () => { + selectorArgsWeakMap(state) + }, + commonOptions + ) + bench( + selectorBothWeakMap, + () => { + selectorBothWeakMap(state) + }, + commonOptions + ) + bench( + selectorBothAutotrack, + () => { + selectorBothAutotrack(state) + }, + commonOptions + ) + bench( + nonMemoizedSelector, + () => { + nonMemoizedSelector(state) + }, + commonOptions + ) +}) + +describe('Cached vs non-cached length in for loops', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const store = setupStore() + const state = store.getState() + const { todos } = state + const { length } = todos + bench( + 'length not cached', + () => { + for (let i = 0; i < todos.length; i++) { + todos[i].completed + todos[i].id + } + }, + commonOptions + ) + bench( + 'length cached', + () => { + for (let i = 0; i < length; i++) { + todos[i].completed + todos[i].id + } + }, + commonOptions + ) + bench( + 'length and arg cached', + () => { + for (let i = 0; i < length; i++) { + const arg = todos[i] + arg.completed + arg.id + } + }, + commonOptions + ) +}) + +describe.todo('nested field access', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const selectorDefault1 = createSelector( + (state: RootState) => state.users.user, + user => user.details.preferences.notifications.push.frequency + ) + const nonMemoizedSelector = (state: RootState) => + state.users.user.details.preferences.notifications.push.frequency + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + commonOptions + ) + bench( + 'nonMemoizedSelector', + () => { + nonMemoizedSelector(state) + }, + commonOptions + ) + bench( + 'selectorDefault1', + () => { + selectorDefault1(state) + }, + commonOptions + ) +}) + +describe.todo('simple field access', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const selectorDefault1 = createSelector( + (state: RootState) => state.users.user, + user => user.details.preferences.notifications.push.frequency + ) + const selectorDefault2 = createSelector( + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const nonMemoizedSelector = (state: RootState) => + state.users.user.details.preferences.notifications.push.frequency + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + commonOptions + ) + bench( + 'nonMemoizedSelector', + () => { + nonMemoizedSelector(state) + }, + commonOptions + ) + bench( + 'selectorDefault1', + () => { + selectorDefault1(state) + }, + commonOptions + ) + bench( + 'selectorDefault2', + () => { + selectorDefault2(state) + }, + commonOptions + ) +}) + +describe.todo('field accessors', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const store = setupStore() + const selectorDefault = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + const nonMemoizedSelector = (state: RootState) => state.users.appSettings + setFunctionNames({ selectorDefault, nonMemoizedSelector }) + bench( + selectorDefault, + () => { + selectorDefault(store.getState()) + }, + { ...commonOptions } + ) + bench( + nonMemoizedSelector, + () => { + nonMemoizedSelector(store.getState()) + }, + { ...commonOptions } + ) +}) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts new file mode 100644 index 000000000..a64bb93f5 --- /dev/null +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -0,0 +1,486 @@ +import type { OutputSelector, Selector } from 'reselect' +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector, + weakMapMemoize +} from 'reselect' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + logSelectorRecomputations, + resetSelector, + setFunctionNames, + setupStore +} from '../testUtils' + +import type { Options } from 'tinybench' + +describe('Parametric selectors: weakMapMemoize vs others', () => { + const store = setupStore() + const state = store.getState() + const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectorDefaultWithCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithArgsCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithBothCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id), + { memoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { memoize: autotrackMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: autotrackMemoize } + ) + const selectorBothAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } + ) + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.find(todo => todo.id === id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const nonMemoizedSelector = (state: RootState, id: number) => { + return state.todos.find(todo => todo.id === id) + } + setFunctionNames({ + selectorDefault, + selectorDefaultWithCacheSize, + selectorDefaultWithArgsCacheSize, + selectorDefaultWithBothCacheSize, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap, + selectorAutotrack, + selectorArgsAutotrack, + selectorBothAutotrack, + nonMemoizedSelector + }) + + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorDefaultWithCacheSize, + () => { + runSelector(selectorDefaultWithCacheSize) + }, + createOptions(selectorDefaultWithCacheSize, commonOptions) + ) + bench( + selectorDefaultWithArgsCacheSize, + () => { + runSelector(selectorDefaultWithArgsCacheSize) + }, + createOptions(selectorDefaultWithArgsCacheSize, commonOptions) + ) + bench( + selectorDefaultWithBothCacheSize, + () => { + runSelector(selectorDefaultWithBothCacheSize) + }, + createOptions(selectorDefaultWithBothCacheSize, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) + ) + bench( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + createOptions(selectorArgsWeakMap, commonOptions) + ) + bench( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + createOptions(selectorBothWeakMap, commonOptions) + ) + bench( + selectorAutotrack, + () => { + runSelector(selectorAutotrack) + }, + createOptions(selectorAutotrack, commonOptions) + ) + bench( + selectorArgsAutotrack, + () => { + runSelector(selectorArgsAutotrack) + }, + createOptions(selectorArgsAutotrack, commonOptions) + ) + bench( + selectorBothAutotrack, + () => { + runSelector(selectorBothAutotrack) + }, + createOptions(selectorBothAutotrack, commonOptions) + ) + bench( + nonMemoizedSelector, + () => { + runSelector(nonMemoizedSelector) + }, + { ...commonOptions } + ) +}) + +// describe('weakMapMemoize vs defaultMemoize with maxSize', () => { +// const store = setupStore() +// const state = store.getState() +// const arrayOfNumbers = Array.from({ length: 30 }, (num, index) => index) +// const commonOptions: Options = { +// iterations: 10, +// time: 0 +// } +// const runSelector = (selector: S) => { +// arrayOfNumbers.forEach(num => { +// selector(state, num) +// }) +// arrayOfNumbers.forEach(num => { +// selector(state, num) +// }) +// } +// const selectorDefaultWithCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoizeOptions: { maxSize: 30 } } +// ) +// const selectorDefaultWithArgsCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoizeOptions: { maxSize: 30 } } +// ) +// const selectorDefaultWithBothCacheSize = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } +// ) +// const selectorWeakMap = createSelector( +// [(state: RootState) => state.todos, (state: RootState, id: number) => id], +// (todos, id) => todos.map(todo => todo.id === id), +// { memoize: weakMapMemoize } +// ) +// const selectorArgsWeakMap = createSelector( +// (state: RootState) => state.todos, +// (state: RootState, id: number) => id, +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoize: weakMapMemoize } +// ) +// const selectorBothWeakMap = createSelector( +// (state: RootState) => state.todos, +// (state: RootState, id: number) => id, +// (todos, id) => todos.map(todo => todo.id === id), +// { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } +// ) +// const nonMemoizedSelector = (state: RootState, id: number) => { +// return state.todos.map(todo => todo.id === id) +// } +// setFunctionNames({ +// selectorDefaultWithCacheSize, +// selectorDefaultWithArgsCacheSize, +// selectorDefaultWithBothCacheSize, +// selectorWeakMap, +// selectorArgsWeakMap, +// selectorBothWeakMap, +// nonMemoizedSelector +// }) +// const createOptions = ( +// selector: S, +// commonOptions: Options = {} +// ) => { +// const options: Options = { +// setup: (task, mode) => { +// if (mode === 'warmup') return +// resetSelector(selector) +// task.opts = { +// afterAll: () => { +// logSelectorRecomputations(selector) +// } +// } +// } +// } +// return { ...commonOptions, ...options } +// } +// bench( +// selectorDefaultWithCacheSize, +// () => { +// runSelector(selectorDefaultWithCacheSize) +// }, +// createOptions(selectorDefaultWithCacheSize, commonOptions) +// ) +// bench( +// selectorDefaultWithArgsCacheSize, +// () => { +// runSelector(selectorDefaultWithArgsCacheSize) +// }, +// createOptions(selectorDefaultWithArgsCacheSize, commonOptions) +// ) +// bench( +// selectorDefaultWithBothCacheSize, +// () => { +// runSelector(selectorDefaultWithBothCacheSize) +// }, +// createOptions(selectorDefaultWithBothCacheSize, commonOptions) +// ) +// bench( +// selectorWeakMap, +// () => { +// runSelector(selectorWeakMap) +// }, +// createOptions(selectorWeakMap, commonOptions) +// ) +// bench( +// selectorArgsWeakMap, +// () => { +// runSelector(selectorArgsWeakMap) +// }, +// createOptions(selectorArgsWeakMap, commonOptions) +// ) +// bench( +// selectorBothWeakMap, +// () => { +// runSelector(selectorBothWeakMap) +// }, +// createOptions(selectorBothWeakMap, commonOptions) +// ) +// bench( +// nonMemoizedSelector, +// () => { +// runSelector(nonMemoizedSelector) +// }, +// { ...commonOptions } +// ) +// }) + +describe('Simple selectors: weakMapMemoize vs others', () => { + const store = setupStore() + const commonOptions: Options = { + // warmupIterations: 0, + // warmupTime: 0, + // iterations: 10, + // time: 0 + } + const selectTodoIdsDefault = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selectTodoIdsWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectTodoIdsAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + + setFunctionNames({ + selectTodoIdsDefault, + selectTodoIdsWeakMap, + selectTodoIdsAutotrack + }) + + const createOptions = (selector: S) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + resetSelector(selector) + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + + bench( + selectTodoIdsDefault, + () => { + selectTodoIdsDefault(store.getState()) + }, + createOptions(selectTodoIdsDefault) + ) + bench( + selectTodoIdsWeakMap, + () => { + selectTodoIdsWeakMap(store.getState()) + }, + createOptions(selectTodoIdsWeakMap) + ) + bench( + selectTodoIdsAutotrack, + () => { + selectTodoIdsAutotrack(store.getState()) + }, + createOptions(selectTodoIdsAutotrack) + ) +}) + +describe.skip('weakMapMemoize memory leak', () => { + const store = setupStore() + const state = store.getState() + const arrayOfNumbers = Array.from( + { length: 2_000_000 }, + (num, index) => index + ) + const commonOptions: Options = { + warmupIterations: 0, + warmupTime: 0, + iterations: 1, + time: 0 + } + const runSelector = (selector: S) => { + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + arrayOfNumbers.forEach(num => { + selector(state, num) + }) + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorArgsWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + setFunctionNames({ + selectorDefault, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap + }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + afterAll: () => { + logSelectorRecomputations(selector) + } + } + } + } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) + ) + bench.skip( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + createOptions(selectorArgsWeakMap, commonOptions) + ) + bench.skip( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + createOptions(selectorBothWeakMap, commonOptions) + ) +}) diff --git a/test/testUtils.ts b/test/testUtils.ts index faeb41447..1dee0e602 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,440 +1,551 @@ -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' - -interface Todo { - id: number - title: string - description: string - completed: boolean -} - -interface Alert { - id: number - message: string - type: string - read: boolean -} - -interface BillingAddress { - street: string - city: string - state: string - zip: string -} - -interface Address extends BillingAddress { - billing: BillingAddress -} - -interface PushNotification { - enabled: boolean - frequency: string -} - -interface Notifications { - email: boolean - sms: boolean - push: PushNotification -} - -interface Preferences { - newsletter: boolean - notifications: Notifications -} - -interface Login { - lastLogin: string - loginCount: number -} - -interface UserDetails { - name: string - email: string - address: Address - preferences: Preferences -} - -interface User { - id: number - details: UserDetails - status: string - login: Login -} - -interface AppSettings { - theme: string - language: string -} - -interface UserState { - user: User - appSettings: AppSettings -} - -// For long arrays -const todoState = [ - { - id: 0, - title: 'Buy groceries', - description: 'Milk, bread, eggs, and fruits', - completed: false - }, - { - id: 1, - title: 'Schedule dentist appointment', - description: 'Check available slots for next week', - completed: false - }, - { - id: 2, - title: 'Convince the cat to get a job', - description: 'Need extra income for cat treats', - completed: false - }, - { - id: 3, - title: 'Figure out if plants are plotting world domination', - description: 'That cactus looks suspicious...', - completed: false - }, - { - id: 4, - title: 'Practice telekinesis', - description: 'Try moving the remote without getting up', - completed: false - }, - { - id: 5, - title: 'Determine location of El Dorado', - description: 'Might need it for the next vacation', - completed: false - }, - { - id: 6, - title: 'Master the art of invisible potato juggling', - description: 'Great party trick', - completed: false - } -] - -export const createTodoItem = (id: number) => { - return { - id, - title: `Task ${id}`, - description: `Description for task ${id}`, - completed: false - } -} - -export const pushToTodos = (howMany: number) => { - const { length: todoStateLength } = todoState - const limit = howMany + todoStateLength - for (let i = todoStateLength; i < limit; i++) { - todoState.push(createTodoItem(i)) - } -} - -pushToTodos(200) - -const alertState = [ - { - id: 0, - message: 'You have an upcoming meeting at 3 PM.', - type: 'reminder', - read: false - }, - { - id: 1, - message: 'New software update available.', - type: 'notification', - read: false - }, - { - id: 3, - message: - 'The plants have been watered, but keep an eye on that shifty cactus.', - type: 'notification', - read: false - }, - { - id: 4, - message: - 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', - type: 'reminder', - read: false - }, - { - id: 5, - message: - 'Expedition to El Dorado is postponed. The treasure map is being updated.', - type: 'notification', - read: false - }, - { - id: 6, - message: - 'Invisible potato juggling championship is tonight. May the best mime win.', - type: 'reminder', - read: false - } -] - -// For nested fields tests -const userState: UserState = { - user: { - id: 0, - details: { - name: 'John Doe', - email: 'john.doe@example.com', - address: { - street: '123 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345', - billing: { - street: '456 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345' - } - }, - preferences: { - newsletter: true, - notifications: { - email: true, - sms: false, - push: { - enabled: true, - frequency: 'daily' - } - } - } - }, - status: 'active', - login: { - lastLogin: '2023-04-30T12:34:56Z', - loginCount: 123 - } - }, - appSettings: { - theme: 'dark', - language: 'en-US' - } -} - -const todoSlice = createSlice({ - name: 'todos', - initialState: todoState, - reducers: { - toggleCompleted: (state, action: PayloadAction) => { - const todo = state.find(todo => todo.id === action.payload) - if (todo) { - todo.completed = !todo.completed - } - }, - - addTodo: (state, action: PayloadAction>) => { - const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - state.push({ - ...action.payload, - id: newId, - completed: false - }) - }, - - removeTodo: (state, action: PayloadAction) => { - return state.filter(todo => todo.id !== action.payload) - }, - - updateTodo: (state, action: PayloadAction) => { - const index = state.findIndex(todo => todo.id === action.payload.id) - if (index !== -1) { - state[index] = action.payload - } - }, - - clearCompleted: state => { - return state.filter(todo => !todo.completed) - } - } -}) - -const alertSlice = createSlice({ - name: 'alerts', - initialState: alertState, - reducers: { - markAsRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = true - } - }, - - toggleRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = !alert.read - } - }, - - addAlert: (state, action: PayloadAction>) => { - const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - state.push({ - ...action.payload, - id: newId - }) - }, - - removeAlert: (state, action: PayloadAction) => { - return state.filter(alert => alert.id !== action.payload) - } - } -}) - -const userSlice = createSlice({ - name: 'users', - initialState: userState, - reducers: { - setUserName: (state, action: PayloadAction) => { - state.user.details.name = action.payload - }, - - setUserEmail: (state, action: PayloadAction) => { - state.user.details.email = action.payload - }, - - setAppTheme: (state, action: PayloadAction) => { - state.appSettings.theme = action.payload - }, - - updateUserStatus: (state, action: PayloadAction) => { - state.user.status = action.payload - }, - - updateLoginDetails: ( - state, - action: PayloadAction<{ lastLogin: string; loginCount: number }> - ) => { - state.user.login = { ...state.user.login, ...action.payload } - }, - - updateUserAddress: (state, action: PayloadAction
) => { - state.user.details.address = { - ...state.user.details.address, - ...action.payload - } - }, - - updateBillingAddress: (state, action: PayloadAction) => { - state.user.details.address.billing = { - ...state.user.details.address.billing, - ...action.payload - } - }, - - toggleNewsletterSubscription: state => { - state.user.details.preferences.newsletter = - !state.user.details.preferences.newsletter - }, - - setNotificationPreferences: ( - state, - action: PayloadAction - ) => { - state.user.details.preferences.notifications = { - ...state.user.details.preferences.notifications, - ...action.payload - } - }, - - updateAppLanguage: (state, action: PayloadAction) => { - state.appSettings.language = action.payload - } - } -}) - -const rootReducer = combineReducers({ - [todoSlice.name]: todoSlice.reducer, - [alertSlice.name]: alertSlice.reducer, - [userSlice.name]: userSlice.reducer -}) - -export const setupStore = (preloadedState?: Partial) => { - return configureStore({ reducer: rootReducer, preloadedState }) -} - -export type AppStore = Simplify> - -export type RootState = ReturnType - -export interface LocalTestContext { - store: AppStore - state: RootState -} - -export const { markAsRead, addAlert, removeAlert, toggleRead } = - alertSlice.actions - -export const { - toggleCompleted, - addTodo, - removeTodo, - updateTodo, - clearCompleted -} = todoSlice.actions - -export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions - -// Since Node 16 does not support `structuredClone` -export const deepClone = (object: T): T => - JSON.parse(JSON.stringify(object)) - -export const setFunctionName = (func: AnyFunction, name: string) => { - Object.defineProperty(func, 'name', { value: name }) -} - -export const setFunctionNames = (funcObject: Record) => { - Object.entries(funcObject).forEach(([key, value]) => - setFunctionName(value, key) - ) -} - -const store = setupStore() -const state = store.getState() - -export const localTest = test.extend({ - store, - state -}) - -export const resetSelector = ( - selector: S -) => { - selector.clearCache() - selector.resetRecomputations() - selector.resetDependencyRecomputations() - selector.memoizedResultFunc.clearCache() -} - -export const logRecomputations = < - S extends OutputSelector ->( - selector: S -) => { - console.log( - `${selector.name} result function recalculated:`, - selector.recomputations(), - `time(s)`, - `input selectors recalculated:`, - selector.dependencyRecomputations(), - `time(s)` - ) -} +import type { PayloadAction } from '@reduxjs/toolkit' +import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' +import { test } from 'vitest' +import type { AnyFunction, OutputSelector, Simplify } from '../src/types' + +interface Todo { + id: number + title: string + description: string + completed: boolean +} + +interface Alert { + id: number + message: string + type: string + read: boolean +} + +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +// For long arrays +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +// For long arrays +const todoState = [ + { + id: 0, + title: 'Buy groceries', + description: 'Milk, bread, eggs, and fruits', + completed: false + }, + { + id: 1, + title: 'Schedule dentist appointment', + description: 'Check available slots for next week', + completed: false + }, + { + id: 2, + title: 'Convince the cat to get a job', + description: 'Need extra income for cat treats', + completed: false + }, + { + id: 3, + title: 'Figure out if plants are plotting world domination', + description: 'That cactus looks suspicious...', + completed: false + }, + { + id: 4, + title: 'Practice telekinesis', + description: 'Try moving the remote without getting up', + completed: false + }, + { + id: 5, + title: 'Determine location of El Dorado', + description: 'Might need it for the next vacation', + completed: false + }, + { + id: 6, + title: 'Master the art of invisible potato juggling', + description: 'Great party trick', + completed: false + } +] + +export const createTodoItem = (id: number) => { + return { + id, + title: `Task ${id}`, + description: `Description for task ${id}`, + completed: false + } +} + +export const pushToTodos = (howMany: number) => { + const { length: todoStateLength } = todoState + const limit = howMany + todoStateLength + for (let i = todoStateLength; i < limit; i++) { + todoState.push(createTodoItem(i)) + } +} + +pushToTodos(200) + +const alertState = [ + { + id: 0, + message: 'You have an upcoming meeting at 3 PM.', + type: 'reminder', + read: false + }, + { + id: 1, + message: 'New software update available.', + type: 'notification', + read: false + }, + { + id: 3, + message: + 'The plants have been watered, but keep an eye on that shifty cactus.', + type: 'notification', + read: false + }, + { + id: 4, + message: + 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', + type: 'reminder', + read: false + }, + { + id: 5, + message: + 'Expedition to El Dorado is postponed. The treasure map is being updated.', + type: 'notification', + read: false + }, + { + id: 6, + message: + 'Invisible potato juggling championship is tonight. May the best mime win.', + type: 'reminder', + read: false + } +] + +// For nested fields tests +const userState: UserState = { + user: { + id: 0, + details: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + street: '123 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345', + billing: { + street: '456 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345' + } + }, + preferences: { + newsletter: true, + notifications: { + email: true, + sms: false, + push: { + enabled: true, + frequency: 'daily' + } + } + } + }, + status: 'active', + login: { + lastLogin: '2023-04-30T12:34:56Z', + loginCount: 123 + } + }, + appSettings: { + theme: 'dark', + language: 'en-US' + } +} + +const todoSlice = createSlice({ + name: 'todos', + initialState: todoState, + reducers: { + toggleCompleted: (state, action: PayloadAction) => { + const todo = state.find(todo => todo.id === action.payload) + if (todo) { + todo.completed = !todo.completed + } + }, + + addTodo: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId, + completed: false + }) + }, + + removeTodo: (state, action: PayloadAction) => { + return state.filter(todo => todo.id !== action.payload) + }, + + updateTodo: (state, action: PayloadAction) => { + const index = state.findIndex(todo => todo.id === action.payload.id) + if (index !== -1) { + state[index] = action.payload + } + }, + + clearCompleted: state => { + return state.filter(todo => !todo.completed) + } + } +}) + +const alertSlice = createSlice({ + name: 'alerts', + initialState: alertState, + reducers: { + markAsRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = true + } + }, + + toggleRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = !alert.read + } + }, + + addAlert: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId + }) + }, + + removeAlert: (state, action: PayloadAction) => { + return state.filter(alert => alert.id !== action.payload) + } + } +}) + +const userSlice = createSlice({ + name: 'users', + initialState: userState, + reducers: { + setUserName: (state, action: PayloadAction) => { + state.user.details.name = action.payload + }, + + setUserEmail: (state, action: PayloadAction) => { + state.user.details.email = action.payload + }, + + setAppTheme: (state, action: PayloadAction) => { + state.appSettings.theme = action.payload + }, + + updateUserStatus: (state, action: PayloadAction) => { + state.user.status = action.payload + }, + + updateLoginDetails: ( + state, + action: PayloadAction<{ lastLogin: string; loginCount: number }> + ) => { + state.user.login = { ...state.user.login, ...action.payload } + }, + + updateUserAddress: (state, action: PayloadAction
) => { + state.user.details.address = { + ...state.user.details.address, + ...action.payload + } + }, + + updateBillingAddress: (state, action: PayloadAction) => { + state.user.details.address.billing = { + ...state.user.details.address.billing, + ...action.payload + } + }, + + toggleNewsletterSubscription: state => { + state.user.details.preferences.newsletter = + !state.user.details.preferences.newsletter + }, + + setNotificationPreferences: ( + state, + action: PayloadAction + ) => { + state.user.details.preferences.notifications = { + ...state.user.details.preferences.notifications, + ...action.payload + } + }, + + updateAppLanguage: (state, action: PayloadAction) => { + state.appSettings.language = action.payload + } + } +}) + +const rootReducer = combineReducers({ + [todoSlice.name]: todoSlice.reducer, + [alertSlice.name]: alertSlice.reducer, + [userSlice.name]: userSlice.reducer +}) + +export const setupStore = (preloadedState?: Partial) => { + return configureStore({ reducer: rootReducer, preloadedState }) +} + +export type AppStore = Simplify> + +export type RootState = ReturnType + +export interface LocalTestContext { + store: AppStore + state: RootState +} + +export const { markAsRead, addAlert, removeAlert, toggleRead } = + alertSlice.actions + +export const { + toggleCompleted, + addTodo, + removeTodo, + updateTodo, + clearCompleted +} = todoSlice.actions + +export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions + +// Since Node 16 does not support `structuredClone` +export const deepClone = (object: T): T => + JSON.parse(JSON.stringify(object)) + +export const setFunctionName = (func: AnyFunction, name: string) => { + Object.defineProperty(func, 'name', { value: name }) +} + +export const setFunctionNames = (funcObject: Record) => { + Object.entries(funcObject).forEach(([key, value]) => + setFunctionName(value, key) + ) +} + +const store = setupStore() +const state = store.getState() + +export const localTest = test.extend({ + store, + state +}) + +export const resetSelector = (selector: S) => { + selector.clearCache() + selector.resetRecomputations() + selector.resetDependencyRecomputations() + selector.memoizedResultFunc.clearCache() +} + +export const logRecomputations = (selector: S) => { + console.log( + `${selector.name} result function recalculated:`, + selector.recomputations(), + `time(s)`, + `input selectors recalculated:`, + selector.dependencyRecomputations(), + `time(s)` + ) +} + +export const logSelectorRecomputations = ( + selector: S +) => { + console.log( + `\x1B[32m\x1B[1m${selector.name}\x1B[0m result function recalculated:`, + `\x1B[33m${selector.recomputations().toLocaleString('en-US')}\x1B[0m`, + 'time(s)', + `input selectors recalculated:`, + `\x1B[33m${selector + .dependencyRecomputations() + .toLocaleString('en-US')}\x1B[0m`, + 'time(s)' + ) +} + +export const logFunctionInfo = (func: AnyFunction, recomputations: number) => { + console.log( + `\x1B[32m\x1B[1m${func.name}\x1B[0m was called:`, + recomputations, + 'time(s)' + ) +} + +export const safeApply = ( + func: (...args: Params) => Result, + args: Params +) => func.apply(null, args) + +export const countRecomputations = < + Params extends any[], + Result, + AdditionalFields +>( + func: ((...args: Params) => Result) & AdditionalFields +) => { + let recomputations = 0 + const wrapper = (...args: Params) => { + recomputations++ + return safeApply(func, args) + } + return Object.assign( + wrapper, + { + recomputations: () => recomputations, + resetRecomputations: () => (recomputations = 0) + }, + func + ) +} + +export const runMultipleTimes = ( + func: (...args: Params) => any, + times: number, + ...args: Params +) => { + for (let i = 0; i < times; i++) { + safeApply(func, args) + } +} + +export const expensiveComputation = (times = 1_000_000) => { + for (let i = 0; i < times; i++) { + // Do nothing + } +} diff --git a/yarn.lock b/yarn.lock index 5f3be759d..1f4467bb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,11 +13,11 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.9.2": - version: 7.23.2 - resolution: "@babel/runtime@npm:7.23.2" + version: 7.23.4 + resolution: "@babel/runtime@npm:7.23.4" dependencies: regenerator-runtime: ^0.14.0 - checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb + checksum: 8eb6a6b2367f7d60e7f7dd83f477cc2e2fdb169e5460694d7614ce5c730e83324bcf29251b70940068e757ad1ee56ff8073a372260d90cad55f18a825caf97cd languageName: node linkType: hard @@ -28,9 +28,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/android-arm64@npm:0.19.6" +"@esbuild/android-arm64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/android-arm64@npm:0.19.7" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -42,9 +42,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/android-arm@npm:0.19.6" +"@esbuild/android-arm@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/android-arm@npm:0.19.7" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -56,9 +56,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/android-x64@npm:0.19.6" +"@esbuild/android-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/android-x64@npm:0.19.7" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -70,9 +70,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/darwin-arm64@npm:0.19.6" +"@esbuild/darwin-arm64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/darwin-arm64@npm:0.19.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -84,9 +84,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/darwin-x64@npm:0.19.6" +"@esbuild/darwin-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/darwin-x64@npm:0.19.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -98,9 +98,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/freebsd-arm64@npm:0.19.6" +"@esbuild/freebsd-arm64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/freebsd-arm64@npm:0.19.7" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -112,9 +112,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/freebsd-x64@npm:0.19.6" +"@esbuild/freebsd-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/freebsd-x64@npm:0.19.7" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -126,9 +126,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-arm64@npm:0.19.6" +"@esbuild/linux-arm64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-arm64@npm:0.19.7" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -140,9 +140,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-arm@npm:0.19.6" +"@esbuild/linux-arm@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-arm@npm:0.19.7" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -154,9 +154,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-ia32@npm:0.19.6" +"@esbuild/linux-ia32@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-ia32@npm:0.19.7" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -168,9 +168,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-loong64@npm:0.19.6" +"@esbuild/linux-loong64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-loong64@npm:0.19.7" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -182,9 +182,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-mips64el@npm:0.19.6" +"@esbuild/linux-mips64el@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-mips64el@npm:0.19.7" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -196,9 +196,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-ppc64@npm:0.19.6" +"@esbuild/linux-ppc64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-ppc64@npm:0.19.7" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -210,9 +210,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-riscv64@npm:0.19.6" +"@esbuild/linux-riscv64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-riscv64@npm:0.19.7" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -224,9 +224,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-s390x@npm:0.19.6" +"@esbuild/linux-s390x@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-s390x@npm:0.19.7" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -238,9 +238,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/linux-x64@npm:0.19.6" +"@esbuild/linux-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/linux-x64@npm:0.19.7" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -252,9 +252,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/netbsd-x64@npm:0.19.6" +"@esbuild/netbsd-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/netbsd-x64@npm:0.19.7" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -266,9 +266,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/openbsd-x64@npm:0.19.6" +"@esbuild/openbsd-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/openbsd-x64@npm:0.19.7" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -280,9 +280,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/sunos-x64@npm:0.19.6" +"@esbuild/sunos-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/sunos-x64@npm:0.19.7" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -294,9 +294,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/win32-arm64@npm:0.19.6" +"@esbuild/win32-arm64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/win32-arm64@npm:0.19.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -308,9 +308,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/win32-ia32@npm:0.19.6" +"@esbuild/win32-ia32@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/win32-ia32@npm:0.19.7" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -322,9 +322,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.19.6": - version: 0.19.6 - resolution: "@esbuild/win32-x64@npm:0.19.6" +"@esbuild/win32-x64@npm:0.19.7": + version: 0.19.7 + resolution: "@esbuild/win32-x64@npm:0.19.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -537,86 +537,86 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.5.0" +"@rollup/rollup-android-arm-eabi@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.5.2" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-android-arm64@npm:4.5.0" +"@rollup/rollup-android-arm64@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-android-arm64@npm:4.5.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.5.0" +"@rollup/rollup-darwin-arm64@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-darwin-arm64@npm:4.5.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.5.0" +"@rollup/rollup-darwin-x64@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-darwin-x64@npm:4.5.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.5.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.5.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.5.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.5.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.5.0" +"@rollup/rollup-linux-arm64-musl@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.5.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.5.0" +"@rollup/rollup-linux-x64-gnu@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.5.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.5.0" +"@rollup/rollup-linux-x64-musl@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.5.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.5.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.5.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.5.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.5.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.5.0": - version: 4.5.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.5.0" +"@rollup/rollup-win32-x64-msvc@npm:4.5.2": + version: 4.5.2 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.5.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -638,9 +638,9 @@ __metadata: linkType: hard "@types/chai@npm:*, @types/chai@npm:^4.3.5": - version: 4.3.10 - resolution: "@types/chai@npm:4.3.10" - checksum: cb9ebe31f5da2d72c4b9362ec4efb33497355372270163c0290f6b9c389934ff178dac933be6b2911a125f15972c0379603736ea83ad10bfca933b6aaf6c0c5b + version: 4.3.11 + resolution: "@types/chai@npm:4.3.11" + checksum: d0c05fe5d02b2e6bbca2bd4866a2ab20a59cf729bc04af0060e7a3277eaf2fb65651b90d4c74b0ebf1d152b4b1d49fa8e44143acef276a2bbaa7785fbe5642d3 languageName: node linkType: hard @@ -672,9 +672,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.175": - version: 4.14.201 - resolution: "@types/lodash@npm:4.14.201" - checksum: 484be655298e9b2dc2d218ea934071b2ea31e4a531c561dd220dbda65237e8d08c20dc2d457ac24f29be7fe167415bf7bb9360ea0d80bdb8b0f0ec8d8db92fae + version: 4.14.202 + resolution: "@types/lodash@npm:4.14.202" + checksum: a91acf3564a568c6f199912f3eb2c76c99c5a0d7e219394294213b3f2d54f672619f0fde4da22b29dc5d4c31457cd799acc2e5cb6bd90f9af04a1578483b6ff7 languageName: node linkType: hard @@ -686,36 +686,36 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 20.9.2 - resolution: "@types/node@npm:20.9.2" + version: 20.10.0 + resolution: "@types/node@npm:20.10.0" dependencies: undici-types: ~5.26.4 - checksum: 5bbb8fb2248fc5c5c4071d9809fb9af85997677c07124d65665202b53283a3b7bdff26fb844e9ee407e3847dfce6399c2b01e3329ea44a4b720647b1b987c678 + checksum: face395140d6f2f1755b91fdd3b697cf56aeb9e2514529ce88d56e56f261ad65be7269d863520a9406d73c338699ea68b418e8677584de0c1efeed09539b6f97 languageName: node linkType: hard "@types/prop-types@npm:*": - version: 15.7.10 - resolution: "@types/prop-types@npm:15.7.10" - checksum: 39ecc2d9e439ed16b32937a08d98b84ed4a70f53bcd52c8564c0cd7a36fe1004ca83a1fb94b13c1b7a5c048760f06445c3c6a91a6972c8eff652c0b50c9424b1 + version: 15.7.11 + resolution: "@types/prop-types@npm:15.7.11" + checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 languageName: node linkType: hard "@types/react@npm:*": - version: 18.2.37 - resolution: "@types/react@npm:18.2.37" + version: 18.2.38 + resolution: "@types/react@npm:18.2.38" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 2d2599f1a09e4f678509161fea8baeaf76d21deee460f4f3ccc1ca431ebe85f896d7d0b906127de17e97ed57240cec61955eb97d0b5d9cbf4e97fd6620b1acdb + checksum: 71f8c167173d32252be8b2d3c1c76b3570b94d2fbbd139da86d146be453626f5777e12c2781559119637520dbef9f91cffe968f67b5901618f29226d49fad326 languageName: node linkType: hard "@types/scheduler@npm:*": - version: 0.16.6 - resolution: "@types/scheduler@npm:0.16.6" - checksum: 4cec89727584a50c66a07c322469a4d9e64f5b0117691f36afd4ceae75741c0038a6e107c05e515511d5358b5897becbe065b6e4560664cb1b16f6754915043d + version: 0.16.8 + resolution: "@types/scheduler@npm:0.16.8" + checksum: 6c091b096daa490093bf30dd7947cd28e5b2cd612ec93448432b33f724b162587fed9309a0acc104d97b69b1d49a0f3fc755a62282054d62975d53d7fd13472d languageName: node linkType: hard @@ -1689,31 +1689,31 @@ __metadata: linkType: hard "esbuild@npm:^0.19.3": - version: 0.19.6 - resolution: "esbuild@npm:0.19.6" - dependencies: - "@esbuild/android-arm": 0.19.6 - "@esbuild/android-arm64": 0.19.6 - "@esbuild/android-x64": 0.19.6 - "@esbuild/darwin-arm64": 0.19.6 - "@esbuild/darwin-x64": 0.19.6 - "@esbuild/freebsd-arm64": 0.19.6 - "@esbuild/freebsd-x64": 0.19.6 - "@esbuild/linux-arm": 0.19.6 - "@esbuild/linux-arm64": 0.19.6 - "@esbuild/linux-ia32": 0.19.6 - "@esbuild/linux-loong64": 0.19.6 - "@esbuild/linux-mips64el": 0.19.6 - "@esbuild/linux-ppc64": 0.19.6 - "@esbuild/linux-riscv64": 0.19.6 - "@esbuild/linux-s390x": 0.19.6 - "@esbuild/linux-x64": 0.19.6 - "@esbuild/netbsd-x64": 0.19.6 - "@esbuild/openbsd-x64": 0.19.6 - "@esbuild/sunos-x64": 0.19.6 - "@esbuild/win32-arm64": 0.19.6 - "@esbuild/win32-ia32": 0.19.6 - "@esbuild/win32-x64": 0.19.6 + 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 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -1761,7 +1761,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: b5f6e19c9f3e5302ffea4ad0ba39e17f7eed09f342f04d5561cfa491a69334095655ac2a9166c29a80da14af35cfcaaaf7751f8b2bad870d49ccdb8817921f37 + checksum: a5d979224d47ae0cc6685447eb8f1ceaf7b67f5eaeaac0246f4d589ff7d81b08e4502a6245298d948f13e9b571ac8556a6d83b084af24954f762b1cfe59dbe55 languageName: node linkType: hard @@ -2999,11 +2999,9 @@ __metadata: linkType: hard "lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.2 - resolution: "lru-cache@npm:10.0.2" - dependencies: - semver: ^7.3.5 - checksum: 83ad0e899d79f48574bdda131fe8157c6d65cbd073a6e78e0d1a3467a85dce1ef4d8dc9fd618a56c57a068271501c81d54471e13f84dd121e046b155ed061ed4 + version: 10.1.0 + resolution: "lru-cache@npm:10.1.0" + checksum: 58056d33e2500fbedce92f8c542e7c11b50d7d086578f14b7074d8c241422004af0718e08a6eaae8705cee09c77e39a61c1c79e9370ba689b7010c152e6a76ab languageName: node linkType: hard @@ -3951,21 +3949,21 @@ __metadata: linkType: hard "rollup@npm:^4.2.0": - version: 4.5.0 - resolution: "rollup@npm:4.5.0" - dependencies: - "@rollup/rollup-android-arm-eabi": 4.5.0 - "@rollup/rollup-android-arm64": 4.5.0 - "@rollup/rollup-darwin-arm64": 4.5.0 - "@rollup/rollup-darwin-x64": 4.5.0 - "@rollup/rollup-linux-arm-gnueabihf": 4.5.0 - "@rollup/rollup-linux-arm64-gnu": 4.5.0 - "@rollup/rollup-linux-arm64-musl": 4.5.0 - "@rollup/rollup-linux-x64-gnu": 4.5.0 - "@rollup/rollup-linux-x64-musl": 4.5.0 - "@rollup/rollup-win32-arm64-msvc": 4.5.0 - "@rollup/rollup-win32-ia32-msvc": 4.5.0 - "@rollup/rollup-win32-x64-msvc": 4.5.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 fsevents: ~2.3.2 dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -3996,7 +3994,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 942f08783bf45e623d177c3b58701e463952b9388822d4f8aadf4e3b465141837ad7f8c85737f7709c2dd0063782c9ce528f71dd33ca6dc3a2d112991d6cc097 + checksum: 0cf68670556753c290e07492c01ea7ce19b7d178b903a518c908f7f9a90c0abee4e91c3089849f05a47040fd59c5ac0f10a58e4ee704cf7b03b24667e72c368a languageName: node linkType: hard @@ -4738,8 +4736,8 @@ __metadata: linkType: hard "vite@npm:^3.0.0 || ^4.0.0 || ^5.0.0-0, vite@npm:^3.1.0 || ^4.0.0 || ^5.0.0-0": - version: 5.0.0 - resolution: "vite@npm:5.0.0" + version: 5.0.2 + resolution: "vite@npm:5.0.2" dependencies: esbuild: ^0.19.3 fsevents: ~2.3.3 @@ -4773,7 +4771,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 1f953b062593b072f0e718384e1ff3307b548235ff8c4016fcaa85c09568eb0ba8cd8cfd80e99d940d3bea296b4661b1d0384fe5cb9a996d3e935feb69259755 + checksum: 28784e10ae40f20afdafb81f09439800b2ba48b4e9f14720cb97a8bc47537369b91dc0d1ea955543dd5a443277d4af4718df9e95a9062276167c428fbc931566 languageName: node linkType: hard