reselect-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +473 -0
  3. package/dist/cjs/index.js +6 -0
  4. package/dist/cjs/reselectKit.development.cjs +683 -0
  5. package/dist/cjs/reselectKit.development.cjs.map +1 -0
  6. package/dist/cjs/reselectKit.production.min.cjs +2 -0
  7. package/dist/cjs/reselectKit.production.min.cjs.map +1 -0
  8. package/dist/reselectKit.d.ts +248 -0
  9. package/dist/reselectKit.mjs +678 -0
  10. package/dist/reselectKit.mjs.map +1 -0
  11. package/package.json +70 -0
  12. package/src/__data__/state.ts +97 -0
  13. package/src/__tests__/createAdaptedSelector.test.ts +134 -0
  14. package/src/__tests__/createBoundSelector.test.ts +351 -0
  15. package/src/__tests__/createCachedSequenceSelector.test.ts +24 -0
  16. package/src/__tests__/createCachedStructuredSelector.test.ts +29 -0
  17. package/src/__tests__/createChainSelector.test.ts +404 -0
  18. package/src/__tests__/createEmptySelector.test.ts +99 -0
  19. package/src/__tests__/createPathSelector.test.ts +81 -0
  20. package/src/__tests__/createPropSelector.test.ts +36 -0
  21. package/src/__tests__/createSegmentSelector.test.ts +31 -0
  22. package/src/__tests__/createSequenceSelector.test.ts +62 -0
  23. package/src/_helpers/__tests__/arePathsEqual.test.ts +27 -0
  24. package/src/_helpers/__tests__/getObjectPaths.test.ts +56 -0
  25. package/src/_helpers/__tests__/temporaryAssign.test.ts +59 -0
  26. package/src/_helpers/arePathsEqual.ts +15 -0
  27. package/src/_helpers/defineDynamicSelectorName.ts +10 -0
  28. package/src/_helpers/generateSelectorKey.ts +15 -0
  29. package/src/_helpers/getObjectPaths.ts +13 -0
  30. package/src/_helpers/getSelectorName.ts +7 -0
  31. package/src/_helpers/isCachedSelector.ts +9 -0
  32. package/src/_helpers/isObject.ts +3 -0
  33. package/src/_helpers/stringifyFunction.ts +3 -0
  34. package/src/_helpers/temporaryAssign.ts +80 -0
  35. package/src/_hooks/__tests__/useMemoWith.test.ts +31 -0
  36. package/src/_hooks/__tests__/useSelector.test.ts +87 -0
  37. package/src/_hooks/useMemoWith.ts +11 -0
  38. package/src/_hooks/useSelector.ts +34 -0
  39. package/src/cache/TreeCache.ts +126 -0
  40. package/src/cache/__tests__/TreeCacheObject.test.ts +188 -0
  41. package/src/cache/__tests__/intervalMapCache.test.ts +133 -0
  42. package/src/cache/intervalMapCache.ts +97 -0
  43. package/src/createAdaptedSelector.ts +75 -0
  44. package/src/createBoundSelector.ts +197 -0
  45. package/src/createCachedSequenceSelector.ts +18 -0
  46. package/src/createCachedStructuredSelector.ts +44 -0
  47. package/src/createChainSelector.ts +230 -0
  48. package/src/createEmptySelector.ts +27 -0
  49. package/src/createPathSelector.ts +208 -0
  50. package/src/createPropSelector.ts +28 -0
  51. package/src/createSegmentSelector.ts +6 -0
  52. package/src/createSequenceSelector.ts +15 -0
  53. package/src/createStructuredSelector.ts +3 -0
  54. package/src/debug/__tests__/debug.test.ts +20 -0
  55. package/src/debug/debug.ts +7 -0
  56. package/src/debug/withDebugName.ts +28 -0
  57. package/src/index.ts +26 -0
  58. package/src/keys/__tests__/arrayComposeKeySelectors.test.ts +52 -0
  59. package/src/keys/__tests__/createKeySelectorCreator.test.ts +246 -0
  60. package/src/keys/__tests__/stringComposeKeySelectors.test.ts +52 -0
  61. package/src/keys/arrayComposeKeySelectors.ts +20 -0
  62. package/src/keys/createKeySelectorComposer.ts +68 -0
  63. package/src/keys/createKeySelectorCreator.ts +106 -0
  64. package/src/keys/defaultKeySelector.ts +1 -0
  65. package/src/keys/stringComposeKeySelectors.ts +15 -0
  66. package/src/types.ts +43 -0
  67. package/src/vitest-globals.d.ts +1 -0
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import { createSelector } from '@veksa/reselect';
3
+ import { commonState, State } from '../__data__/state';
4
+ import { createSequenceSelector } from '../createSequenceSelector';
5
+
6
+ describe('createSequenceSelector', () => {
7
+ test('combines input selector results into an ordered array', () => {
8
+ const selector = createSequenceSelector<State, number | undefined>([
9
+ (state) => state.persons.currentPersonId,
10
+ (state) => state.messages.currentMessageId,
11
+ ]);
12
+
13
+ expect(selector(commonState)).toEqual([1, 200]);
14
+ });
15
+
16
+ test('memoizes the result while inputs are unchanged', () => {
17
+ const selector = createSequenceSelector<State, number | undefined>([
18
+ (state) => state.persons.currentPersonId,
19
+ ]);
20
+
21
+ const first = selector(commonState);
22
+ const second = selector(commonState);
23
+
24
+ expect(second).toBe(first);
25
+ });
26
+
27
+ test('recomputes when an input selector result changes', () => {
28
+ const selector = createSequenceSelector<State, number | undefined>([
29
+ (state) => state.persons.currentPersonId,
30
+ ]);
31
+
32
+ const first = selector(commonState);
33
+ const nextState: State = {
34
+ ...commonState,
35
+ persons: { ...commonState.persons, currentPersonId: 2 },
36
+ };
37
+ const second = selector(nextState);
38
+
39
+ expect(second).not.toBe(first);
40
+ expect(second).toEqual([2]);
41
+ });
42
+
43
+ test('passes props through to parametric input selectors', () => {
44
+ const selector = createSequenceSelector<State, { personId: number }, number>([
45
+ (_state, props) => props.personId,
46
+ (state) => state.persons.currentPersonId ?? 0,
47
+ ]);
48
+
49
+ expect(selector(commonState, { personId: 7 })).toEqual([7, 1]);
50
+ });
51
+
52
+ test('uses the provided selector creator', () => {
53
+ const selectorCreator = vi.fn(createSelector);
54
+
55
+ createSequenceSelector<State, number | undefined>(
56
+ [(state) => state.persons.currentPersonId],
57
+ selectorCreator as unknown as typeof createSelector,
58
+ );
59
+
60
+ expect(selectorCreator).toHaveBeenCalledTimes(1);
61
+ });
62
+ });
@@ -0,0 +1,27 @@
1
+ import { arePathsEqual } from '../arePathsEqual';
2
+
3
+ describe('arePathsEqual', () => {
4
+ test('should return false if paths have different length', () => {
5
+ const path = ['first', 'second'];
6
+ const anotherPath = ['123'];
7
+
8
+ const actual = arePathsEqual(path, anotherPath);
9
+ expect(actual).toBeFalsy();
10
+ });
11
+
12
+ test('should return false if paths have different elements', () => {
13
+ const path = ['first', 'second'];
14
+ const anotherPath = ['first', 'third'];
15
+
16
+ const actual = arePathsEqual(path, anotherPath);
17
+ expect(actual).toBeFalsy();
18
+ });
19
+
20
+ test('should return true if paths have equal elements', () => {
21
+ const path = ['first', 'second'];
22
+ const anotherPath = ['first', 'second'];
23
+
24
+ const actual = arePathsEqual(path, anotherPath);
25
+ expect(actual).toBeTruthy();
26
+ });
27
+ });
@@ -0,0 +1,56 @@
1
+ import { getObjectPaths } from '../getObjectPaths';
2
+
3
+ describe('getObjectPaths', () => {
4
+ test('should extract keys from object', () => {
5
+ const obj = {
6
+ key1: 1,
7
+ key2: 'sting',
8
+ key3: true,
9
+ key4: undefined,
10
+ key5: null,
11
+ };
12
+
13
+ const actual = getObjectPaths(obj);
14
+
15
+ expect(actual).toEqual([['key1'], ['key2'], ['key3'], ['key4'], ['key5']]);
16
+ });
17
+
18
+ test('should extract keys from nested object', () => {
19
+ const obj = {
20
+ key1: {
21
+ key2: {
22
+ key3: undefined,
23
+ },
24
+ key4: {
25
+ key5: 1,
26
+ },
27
+ key6: {
28
+ key7: 'sting',
29
+ key8: null,
30
+ },
31
+ },
32
+ };
33
+
34
+ const actual = getObjectPaths(obj);
35
+
36
+ expect(actual).toEqual([
37
+ ['key1', 'key2', 'key3'],
38
+ ['key1', 'key4', 'key5'],
39
+ ['key1', 'key6', 'key7'],
40
+ ['key1', 'key6', 'key8'],
41
+ ]);
42
+ });
43
+
44
+ test('should work with array', () => {
45
+ const obj = {
46
+ key1: [{ key2: { key3: [1] } }, 2],
47
+ };
48
+
49
+ const actual = getObjectPaths(obj);
50
+
51
+ expect(actual).toEqual([
52
+ ['key1', '0', 'key2', 'key3', '0'],
53
+ ['key1', '1'],
54
+ ]);
55
+ });
56
+ });
@@ -0,0 +1,59 @@
1
+ import { temporaryAssign } from '../temporaryAssign';
2
+
3
+ describe('temporaryAssign', () => {
4
+ test('should delete field if it was not in target object', () => {
5
+ const target = {
6
+ field1: 'some value',
7
+ };
8
+ const source = {
9
+ field1: 'another value',
10
+ field2: 'second value',
11
+ };
12
+
13
+ const { rollback } = temporaryAssign(target, source);
14
+
15
+ const expected = {
16
+ field1: 'another value',
17
+ field2: 'second value',
18
+ };
19
+
20
+ expect(target).toEqual(expected);
21
+
22
+ rollback();
23
+
24
+ const rollbackExpected = {
25
+ field1: 'some value',
26
+ };
27
+
28
+ expect(target).toEqual(rollbackExpected);
29
+ });
30
+
31
+ test('should set undefined field if field was undefined in target object', () => {
32
+ const target = {
33
+ field1: 'some value',
34
+ field2: undefined,
35
+ } as Record<string, unknown>;
36
+ const source = {
37
+ field1: 'another value',
38
+ field2: 'second value',
39
+ };
40
+
41
+ const { rollback } = temporaryAssign(target, source);
42
+
43
+ const expected = {
44
+ field1: 'another value',
45
+ field2: 'second value',
46
+ };
47
+
48
+ expect(target).toEqual(expected);
49
+
50
+ rollback();
51
+
52
+ const rollbackExpected = {
53
+ field1: 'some value',
54
+ field2: undefined,
55
+ } as Record<string, unknown>;
56
+
57
+ expect(target).toEqual(rollbackExpected);
58
+ });
59
+ });
@@ -0,0 +1,15 @@
1
+ import { Path } from '../types';
2
+
3
+ export const arePathsEqual = (path: Path, anotherPath: Path) => {
4
+ if (path.length !== anotherPath.length) {
5
+ return false;
6
+ }
7
+
8
+ for (let i = 0; i < path.length; i += 1) {
9
+ if (path[i] !== anotherPath[i]) {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ return true;
15
+ };
@@ -0,0 +1,10 @@
1
+ export const defineDynamicSelectorName = (selector: unknown, selectorNameGetter: () => string) => {
2
+ let overriddenSelectorName: string;
3
+ Object.defineProperty(selector, 'selectorName', {
4
+ configurable: true,
5
+ get: () => overriddenSelectorName ?? selectorNameGetter(),
6
+ set: (value: string) => {
7
+ overriddenSelectorName = value;
8
+ },
9
+ });
10
+ };
@@ -0,0 +1,15 @@
1
+ import { stringifyFunction } from './stringifyFunction';
2
+
3
+ export const generateSelectorKey = (selector: unknown) => {
4
+ const dependencies = (selector as { dependencies?: unknown[] }).dependencies ?? [];
5
+
6
+ let result = stringifyFunction(selector);
7
+
8
+ for (let i = dependencies.length - 1; i >= 0; i -= 1) {
9
+ const dependency = dependencies[i];
10
+
11
+ result += stringifyFunction(dependency);
12
+ }
13
+
14
+ return result;
15
+ };
@@ -0,0 +1,13 @@
1
+ import { Path } from '../types';
2
+ import { isObject } from './isObject';
3
+
4
+ export const getObjectPaths = (value: unknown, parentPath: Path = []): Path[] => {
5
+ if (!isObject(value)) {
6
+ return [parentPath];
7
+ }
8
+
9
+ return Object.keys(value).reduce<Path[]>((result, key) => {
10
+ const paths = getObjectPaths(value[key], parentPath.concat([key]));
11
+ return result.concat(paths);
12
+ }, []);
13
+ };
@@ -0,0 +1,7 @@
1
+ export const getSelectorName = (selector: { name: string; selectorName?: string }): string => {
2
+ if ('selectorName' in selector && selector.selectorName) {
3
+ return selector.selectorName;
4
+ }
5
+
6
+ return selector.name;
7
+ };
@@ -0,0 +1,9 @@
1
+ import { CachedSelector } from '../types';
2
+
3
+ export const isCachedSelector = (selector: unknown): selector is CachedSelector => {
4
+ return (
5
+ selector instanceof Object &&
6
+ 'keySelector' in selector &&
7
+ typeof (selector as CachedSelector).keySelector === 'function'
8
+ );
9
+ };
@@ -0,0 +1,3 @@
1
+ export const isObject = (value: unknown): value is Record<string, unknown> => {
2
+ return value !== null && typeof value === 'object';
3
+ };
@@ -0,0 +1,3 @@
1
+ export const stringifyFunction = (source: unknown) => {
2
+ return String(source).replace(/\s+/g, ' ').trim();
3
+ };
@@ -0,0 +1,80 @@
1
+ const noop = () => undefined;
2
+
3
+ export function temporaryAssign(
4
+ target: Record<string, unknown>,
5
+ source: Record<string, unknown>,
6
+ ): {
7
+ result: Record<string, unknown>;
8
+ rollback: () => void;
9
+ } {
10
+ if (typeof target !== 'object' || target === null) {
11
+ return {
12
+ result: source,
13
+ rollback: noop,
14
+ };
15
+ }
16
+
17
+ // Both containers and the closure are allocated only once something actually has
18
+ // to be restored. This runs on every call of every bound selector, and the usual
19
+ // case is a binding whose keys the target does not have — one entry in
20
+ // `deleteProps`, nothing in `temporary` — while a re-entrant call that finds the
21
+ // binding already applied changes nothing at all and can share `noop`.
22
+ let temporary: Record<string, unknown> | null = null;
23
+ let deleteProps: string[] | null = null;
24
+
25
+ for (const prop in source as unknown as object) {
26
+ if (Object.prototype.hasOwnProperty.call(source, prop)) {
27
+ const targetProp = target[prop];
28
+ const sourceProp = source[prop];
29
+
30
+ if (targetProp !== sourceProp) {
31
+ if (Object.prototype.hasOwnProperty.call(target, prop)) {
32
+ if (temporary === null) {
33
+ temporary = {};
34
+ }
35
+ temporary[prop] = targetProp;
36
+ } else {
37
+ if (deleteProps === null) {
38
+ deleteProps = [];
39
+ }
40
+ deleteProps.push(prop);
41
+ }
42
+
43
+ target[prop] = sourceProp;
44
+ }
45
+ }
46
+ }
47
+
48
+ if (temporary === null && deleteProps === null) {
49
+ return {
50
+ result: target,
51
+ rollback: noop,
52
+ };
53
+ }
54
+
55
+ const restore = temporary;
56
+ const remove = deleteProps;
57
+
58
+ const rollback = () => {
59
+ if (restore !== null) {
60
+ for (const prop in restore) {
61
+ if (Object.prototype.hasOwnProperty.call(restore, prop)) {
62
+ target[prop] = restore[prop];
63
+ }
64
+ }
65
+ }
66
+
67
+ if (remove !== null) {
68
+ for (const prop of remove) {
69
+ if (Object.prototype.hasOwnProperty.call(target, prop)) {
70
+ delete target[prop];
71
+ }
72
+ }
73
+ }
74
+ };
75
+
76
+ return {
77
+ result: target,
78
+ rollback,
79
+ };
80
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useMemoWith } from '../useMemoWith';
4
+
5
+ describe('useMemoWith', () => {
6
+ const byId = (a: { id: number }, b: { id: number }) => a.id === b.id;
7
+
8
+ test('keeps the previous value while the comparator reports equality', () => {
9
+ const { result, rerender } = renderHook(({ value }) => useMemoWith(value, byId), {
10
+ initialProps: { value: { id: 1, tag: 'a' } },
11
+ });
12
+
13
+ const first = result.current;
14
+ rerender({ value: { id: 1, tag: 'b' } });
15
+
16
+ expect(result.current).toBe(first);
17
+ });
18
+
19
+ test('updates the value when the comparator reports inequality', () => {
20
+ const { result, rerender } = renderHook(({ value }) => useMemoWith(value, byId), {
21
+ initialProps: { value: { id: 1 } },
22
+ });
23
+
24
+ const first = result.current;
25
+ const next = { id: 2 };
26
+ rerender({ value: next });
27
+
28
+ expect(result.current).not.toBe(first);
29
+ expect(result.current).toBe(next);
30
+ });
31
+ });
@@ -0,0 +1,87 @@
1
+ import { createElement, ReactNode } from 'react';
2
+ import { describe, expect, test, vi } from 'vitest';
3
+ import { renderHook } from '@testing-library/react';
4
+ import { Provider } from '@veksa/react-redux';
5
+ import { useSelector } from '../useSelector';
6
+
7
+ // `children` is passed positionally (oxlint forbids it as a prop), so relax the
8
+ // Provider's prop type to make `children` optional in the props object.
9
+ const StoreProvider = Provider as unknown as (props: {
10
+ store: unknown;
11
+ children?: ReactNode;
12
+ }) => ReactNode;
13
+
14
+ type State = { count: number };
15
+
16
+ const createMockStore = (state: State) => {
17
+ const listeners = new Set<() => void>();
18
+
19
+ const store = {
20
+ getState: () => state,
21
+ dispatch: (action: unknown) => action,
22
+ subscribe: (listener: () => void) => {
23
+ listeners.add(listener);
24
+ return () => listeners.delete(listener);
25
+ },
26
+ replaceReducer: () => undefined,
27
+ };
28
+
29
+ return store as unknown as never;
30
+ };
31
+
32
+ const wrapperWith =
33
+ (store: never) =>
34
+ ({ children }: { children: ReactNode }) =>
35
+ createElement(StoreProvider, { store }, children);
36
+
37
+ describe('useSelector', () => {
38
+ const state: State = { count: 5 };
39
+
40
+ test('returns the selector result for the current state and props', () => {
41
+ const store = createMockStore(state);
42
+
43
+ const { result } = renderHook(
44
+ () =>
45
+ useSelector((s: State, props: { factor: number }) => s.count * props.factor, { factor: 2 }),
46
+ { wrapper: wrapperWith(store) },
47
+ );
48
+
49
+ expect(result.current).toBe(10);
50
+ });
51
+
52
+ test('stabilizes shallow-equal props between renders', () => {
53
+ const store = createMockStore(state);
54
+ const selector = vi.fn((s: State, props: { factor: number }) => s.count * props.factor);
55
+
56
+ const { rerender } = renderHook(({ props }) => useSelector(selector, props), {
57
+ wrapper: wrapperWith(store),
58
+ initialProps: { props: { factor: 2 } },
59
+ });
60
+
61
+ rerender({ props: { factor: 2 } });
62
+
63
+ const propsArgs = selector.mock.calls.map((call) => call[1]);
64
+ // A new-but-shallow-equal props object must not reach the selector as a new reference.
65
+ expect(new Set(propsArgs).size).toBe(1);
66
+ });
67
+
68
+ test('unfreezes frozen props before passing them to the selector', () => {
69
+ const store = createMockStore(state);
70
+ let receivedProps: { factor: number } | undefined;
71
+
72
+ renderHook(
73
+ () =>
74
+ useSelector(
75
+ (s: State, props: { factor: number }) => {
76
+ receivedProps = props;
77
+ return s.count;
78
+ },
79
+ Object.freeze({ factor: 2 }),
80
+ ),
81
+ { wrapper: wrapperWith(store) },
82
+ );
83
+
84
+ expect(Object.isFrozen(receivedProps)).toBe(false);
85
+ expect(receivedProps?.factor).toBe(2);
86
+ });
87
+ });
@@ -0,0 +1,11 @@
1
+ import { useRef } from 'react';
2
+
3
+ export const useMemoWith = <T>(value: T, comparator: (a: T, b: T) => boolean) => {
4
+ const ref = useRef<T>(value);
5
+
6
+ if (!comparator(value, ref.current)) {
7
+ ref.current = value;
8
+ }
9
+
10
+ return ref.current;
11
+ };
@@ -0,0 +1,34 @@
1
+ import { shallowEqual, useSelector as useReduxSelector } from '@veksa/react-redux';
2
+ import { useMemoWith } from './useMemoWith';
3
+ import { useCallback, useMemo } from 'react';
4
+
5
+ export function useSelector<S extends object, R>(selector: (state: S) => R): R;
6
+
7
+ export function useSelector<S extends object, P, R>(
8
+ selector: (state: S, props: P) => R,
9
+ props: P,
10
+ ): R;
11
+
12
+ export function useSelector(selector: Function, ...params: any[]) {
13
+ const props = params[0];
14
+ const memoizedProps = useMemoWith(props, shallowEqual);
15
+
16
+ // unfreeze props for performance optimization (see temporaryAssign.ts)
17
+ const unfrozenProps = useMemo(() => {
18
+ return Object.isFrozen(memoizedProps) ? { ...memoizedProps } : memoizedProps;
19
+ }, [memoizedProps]);
20
+
21
+ const memoizedPropsSelector = useCallback(
22
+ (state: unknown) => {
23
+ return selector(state, unfrozenProps);
24
+ },
25
+ [selector, unfrozenProps],
26
+ );
27
+
28
+ return useReduxSelector(memoizedPropsSelector, {
29
+ devModeChecks: {
30
+ stabilityCheck: 'never',
31
+ identityFunctionCheck: 'never',
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,126 @@
1
+ import { FlatObjectCache, ICacheObject } from '@veksa/re-reselect';
2
+
3
+ export type TreeCacheObjectOptions = {
4
+ cacheObjectCreator?: () => ICacheObject;
5
+ };
6
+
7
+ const normalizeKey = (key: unknown): unknown[] => (Array.isArray(key) ? key : [key]);
8
+
9
+ class TreeCacheNode {
10
+ public cache?: ICacheObject;
11
+
12
+ public selectorFn?: unknown;
13
+ }
14
+
15
+ export class TreeCache implements ICacheObject {
16
+ private readonly cacheObjectCreator: () => ICacheObject;
17
+
18
+ private root: TreeCacheNode;
19
+
20
+ constructor(options: TreeCacheObjectOptions) {
21
+ this.cacheObjectCreator = options.cacheObjectCreator ?? (() => new FlatObjectCache());
22
+
23
+ const root = new TreeCacheNode();
24
+ root.cache = this.cacheObjectCreator();
25
+ this.root = root;
26
+ }
27
+
28
+ public clear() {
29
+ const root = new TreeCacheNode();
30
+ root.cache = this.cacheObjectCreator();
31
+ this.root = root;
32
+ }
33
+
34
+ public get(key: unknown) {
35
+ // A scalar key is the common case and `normalizeKey` would wrap it in a fresh
36
+ // array on every lookup. The tree walk for one level is a single cache read, so
37
+ // it is spelled out rather than allocated for.
38
+ if (!Array.isArray(key)) {
39
+ const cacheResponse: unknown = this.root.cache?.get(key);
40
+
41
+ return cacheResponse instanceof TreeCacheNode ? cacheResponse.selectorFn : undefined;
42
+ }
43
+
44
+ let currentNode = this.root;
45
+
46
+ for (let i = 0; i < key.length; i += 1) {
47
+ const item = key[i];
48
+ const cacheResponse: unknown = currentNode.cache?.get(item);
49
+
50
+ if (cacheResponse instanceof TreeCacheNode) {
51
+ currentNode = cacheResponse;
52
+ } else {
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ return currentNode.selectorFn;
58
+ }
59
+
60
+ public set(key: unknown, selectorFn: unknown) {
61
+ const keyPath = normalizeKey(key);
62
+ let currentNode = this.root;
63
+
64
+ for (let i = 0; i < keyPath.length; i += 1) {
65
+ const item = keyPath[i];
66
+
67
+ if (!currentNode.cache) {
68
+ currentNode.cache = this.cacheObjectCreator();
69
+ }
70
+
71
+ const cacheResponse: unknown = currentNode.cache.get(item);
72
+
73
+ if (cacheResponse instanceof TreeCacheNode) {
74
+ currentNode = cacheResponse;
75
+ } else {
76
+ const node = new TreeCacheNode();
77
+ currentNode.cache.set(item, node);
78
+ currentNode = node;
79
+ }
80
+ }
81
+
82
+ currentNode.selectorFn = selectorFn;
83
+ }
84
+
85
+ public remove(key: unknown) {
86
+ const keyPath = normalizeKey(key);
87
+ let currentNode = this.root;
88
+
89
+ for (let i = 0; i < keyPath.length; i += 1) {
90
+ const item = keyPath[i];
91
+ const cacheResponse: unknown = currentNode.cache?.get(item);
92
+
93
+ if (cacheResponse instanceof TreeCacheNode) {
94
+ currentNode = cacheResponse;
95
+ } else {
96
+ return;
97
+ }
98
+ }
99
+
100
+ currentNode.selectorFn = undefined;
101
+ }
102
+
103
+ /**
104
+ * Called by re-reselect on every cache operation, so neither the wrapping array
105
+ * nor the `every` callback is allocated here.
106
+ */
107
+ public isValidCacheKey(key: unknown) {
108
+ const rootCache = this.root.cache;
109
+
110
+ if (rootCache?.isValidCacheKey === undefined) {
111
+ return true;
112
+ }
113
+
114
+ if (!Array.isArray(key)) {
115
+ return rootCache.isValidCacheKey(key);
116
+ }
117
+
118
+ for (let i = 0; i < key.length; i += 1) {
119
+ if (!rootCache.isValidCacheKey(key[i])) {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ return true;
125
+ }
126
+ }