Skip to content

Commit

Permalink
feat: Add resetCache, with more tests (#4)
Browse files Browse the repository at this point in the history
* Add resetCache

* More tests
  • Loading branch information
spautz committed Oct 15, 2020
1 parent 7963789 commit f39492d
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 15 deletions.
4 changes: 0 additions & 4 deletions packages/core/src/createDynamicSelector.ts
Expand Up @@ -13,15 +13,11 @@ const createDefaultCache = (): DynamicSelectorResultCache => {
let resultCache: Record<string, DynamicSelectorResultEntry> = {};

return {
has: (paramKey) => resultCache.hasOwnProperty(paramKey),
get: (paramKey) => resultCache[paramKey],
getAll: () => resultCache,
set: (paramKey, newEntry) => {
resultCache[paramKey] = newEntry;
},
reset: () => {
resultCache = {};
},
};
};

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/dynamicSelectorForState.ts
Expand Up @@ -83,7 +83,7 @@ const dynamicSelectorForState = <StateType = any>(
onError,
} = options ? { ...defaultSelectorOptions, ...options } : defaultSelectorOptions;

const resultCache: DynamicSelectorResultCache = createResultCache();
let resultCache: DynamicSelectorResultCache = createResultCache();

let outerFn: DynamicSelectorFn;

Expand Down Expand Up @@ -264,6 +264,7 @@ const dynamicSelectorForState = <StateType = any>(
}

resultCache.set(paramKey, nextResult);

return nextResult;
};

Expand Down Expand Up @@ -313,7 +314,7 @@ const dynamicSelectorForState = <StateType = any>(
* DO NOT USE THIS.
* This is only for debugging purposes
*/
outerFn._innerFn = innerFn;
outerFn._fn = innerFn;

outerFn.getDebugInfo = (params: DynamicSelectorParams): DynamicSelectorDebugInfo => {
if (process.env.NODE_ENV !== 'production') {
Expand All @@ -333,6 +334,13 @@ const dynamicSelectorForState = <StateType = any>(
const argsWithState = addStateToArguments(args);
const rootResult = createResultEntry(stateOptions, argsWithState[0], false, false);

// const paramKey = getKeyForParams(argsWithState[1]);
// const previousResult = resultCache.get(paramKey);
// if (!previousResult) {
// // Don't even bother checking if there's _nothing_ to check
// return rootResult;
// }

pushCallStackEntry(rootResult);
const result = evaluateSelector(...argsWithState);
popCallStackEntry();
Expand Down Expand Up @@ -374,11 +382,22 @@ const dynamicSelectorForState = <StateType = any>(
return result[RESULT_ENTRY__HAS_RETURN_VALUE];
}) as DynamicSelectorFn<boolean>;

outerFn.resetCache = () => {
if (process.env.NODE_ENV !== 'production' && getTopCallStackEntry()) {
// @TODO: Add a way to mute this warning
console.warn(
'Called resetCache while selectors are running: this will probably cause unexpected results',
);
}

outerFn._rc = resultCache = createResultCache();
};

/**
* DO NOT USE THIS.
* This is only for debugging purposes
*/
outerFn._resultCache = resultCache;
outerFn._rc = resultCache;

outerFn.isDynamicSelector = true;

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/internals/dependencies.ts
@@ -1,10 +1,10 @@
import { DynamicSelectorFn, DynamicSelectorParams, DynamicSelectorStateGetFn } from '../types';
import { popCallStackEntry, pushCallStackEntry } from './callStack';
import {
createDepCheckEntry,
RESULT_ENTRY__HAS_RETURN_VALUE,
RESULT_ENTRY__RETURN_VALUE,
} from './resultCache';
import { popCallStackEntry, pushCallStackEntry } from './callStack';
import { DynamicSelectorFn, DynamicSelectorParams, DynamicSelectorStateGetFn } from '../types';

/**
* We track wo types of dependencies:
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/internals/resultCache.ts
@@ -1,7 +1,7 @@
import { getTopCallStackEntry } from './callStack';
import { createDebugInfo, DynamicSelectorDebugInfo } from './debugInfo';
import { DynamicSelectorCallDependencies, DynamicSelectorStateDependencies } from './dependencies';
import { DynamicSelectorStateOptions } from '../types';
import { getTopCallStackEntry } from './callStack';

/**
* This is where things happen: this tracks everything about a single Dynamic Selector call: what was called,
Expand Down Expand Up @@ -46,11 +46,11 @@ export const RESULT_ENTRY__ERROR = 8 as const;
export const RESULT_ENTRY__DEBUG_INFO = 9 as const;

export type DynamicSelectorResultCache = {
has: (paramKey: string) => boolean;
get: (paramKey: string) => DynamicSelectorResultEntry | undefined;
set: (paramKey: string, newEntry: DynamicSelectorResultEntry) => void;
getAll?: () => Record<string, DynamicSelectorResultEntry>;
reset?: () => void;
// getAll?: () => Record<string, DynamicSelectorResultEntry>;
// has?: (paramKey: string) => boolean;
// reset?: () => void;
[propName: string]: any;
};

Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/types.ts
Expand Up @@ -96,12 +96,13 @@ export type DynamicSelectorFnWithoutState<ReturnType = any> = (
export type DynamicSelectorFn<ReturnType = any> = ((
...args: DynamicSelectorArgsWithState | DynamicSelectorArgsWithoutState
) => ReturnType) & {
_innerFn: DynamicSelectorInnerFn<ReturnType>;
_fn: DynamicSelectorInnerFn<ReturnType>;
_dc: DynamicSelectorFnWithState<ReturnType>;
_resultCache: DynamicSelectorResultCache;
_rc: DynamicSelectorResultCache;
displayName: string;
getDebugInfo: (params?: DynamicSelectorParams) => DynamicSelectorDebugInfo;
getCachedResult: DynamicSelectorFn<ReturnType>;
hasCachedResult: DynamicSelectorFn<boolean>;
isDynamicSelector: true;
resetCache: () => void;
};
32 changes: 32 additions & 0 deletions packages/core/tests/basicParams.test.ts
Expand Up @@ -2,6 +2,38 @@ import { createDynamicSelector } from '../src';
import DebugInfoCheckUtil from './util/debugInfoCheckUtil';

describe('basic params', () => {
test('params exist', () => {
const selector = createDynamicSelector((getState, path?: string) => {
return getState(path || null);
});

let state = { a: 1, b: 2, c: 3 };

expect(selector(state)).toEqual({ a: 1, b: 2, c: 3 });

expect(selector.hasCachedResult(state, 'a')).toEqual(false);
expect(selector.hasCachedResult(state, 'b')).toEqual(false);
expect(selector.hasCachedResult(state, 'c')).toEqual(false);
expect(selector.hasCachedResult(state, 'd')).toEqual(false);
expect(selector.hasCachedResult(state)).toEqual(true);

expect(selector(state, 'a')).toEqual(1);
expect(selector(state, 'b')).toEqual(2);
expect(selector(state, 'c')).toEqual(3);

expect(selector.hasCachedResult(state, 'a')).toEqual(true);
expect(selector.hasCachedResult(state, 'b')).toEqual(true);
expect(selector.hasCachedResult(state, 'c')).toEqual(true);
expect(selector.hasCachedResult(state, 'd')).toEqual(false);
expect(selector.hasCachedResult(state)).toEqual(true);

expect(selector.getCachedResult(state, 'a')).toEqual(1);
expect(selector.getCachedResult(state, 'b')).toEqual(2);
expect(selector.getCachedResult(state, 'c')).toEqual(3);
expect(selector.getCachedResult(state, 'd')).toEqual(undefined);
expect(selector.getCachedResult(state)).toEqual({ a: 1, b: 2, c: 3 });
});

test('get value from state with params', () => {
const selector = createDynamicSelector((getState, path: string) => {
return getState(path);
Expand Down
25 changes: 25 additions & 0 deletions packages/core/tests/cachedResults.test.ts
Expand Up @@ -151,4 +151,29 @@ describe('accessing cached results', () => {
expect(childSelector.hasCachedResult(state)).toEqual(true);
expect(childSelector.getCachedResult(state)).toEqual(9);
});

test('resetCache', () => {
const childSelector = createDynamicSelector((getState) => {
return getState('a');
});
const parentSelector = createDynamicSelector((_getState, multiplier?: number) => {
return childSelector() * (multiplier == null ? 1 : multiplier);
});

let state = { a: 3 };

expect(parentSelector(state, 3)).toEqual(9);
expect(parentSelector.hasCachedResult(state, 3)).toEqual(true);
expect(parentSelector.getCachedResult(state, 3)).toEqual(9);
expect(childSelector.hasCachedResult(state)).toEqual(true);
expect(childSelector.getCachedResult(state)).toEqual(3);

childSelector.resetCache();
state = { a: 3 };

// Because the child was reset, the parent isn't cached any more, even though normally the non-change in
// state.a would be fine
expect(parentSelector.hasCachedResult(state, 3)).toEqual(false);
expect(childSelector.hasCachedResult(state)).toEqual(false);
});
});
62 changes: 62 additions & 0 deletions packages/core/tests/freezeResult.test.ts
@@ -0,0 +1,62 @@
import { createDynamicSelector } from '../src';
import DebugInfoCheckUtil from './util/debugInfoCheckUtil';

describe('freeze result', () => {
test('sorted array', () => {
const sortedArraySelector = createDynamicSelector((getState) => {
const rawArray = getState('list');
return [...rawArray].sort((a, b) => a - b);
});
const checkSortedArraySelector = new DebugInfoCheckUtil(sortedArraySelector);

let state = { list: [1, 4, 7, 3, 2, 6, 9, 8, 5] };

const result1 = sortedArraySelector(state);
expect(result1).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
checkSortedArraySelector.expectInvoked('run');

// Same state, really, but this will cause a rerun
state = { list: [1, 4, 7, 3, 2, 6, 9, 8, 5] };

const result2 = sortedArraySelector(state);
expect(result2).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(result2).toStrictEqual(result1);
checkSortedArraySelector.expectInvoked('phantom');

// Once more, with a genuinely different input
state = { list: [6, 9, 1, 4, 7, 3, 8, 5, 2] };

const result3 = sortedArraySelector(state);
expect(result3).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(result3).toStrictEqual(result1);
checkSortedArraySelector.expectInvoked('phantom');
});

test('recovery after exception', () => {
const sortedArraySelector = createDynamicSelector(
(getState) => {
const rawArray = getState('list');
return [...rawArray].sort((a, b) => a - b);
},
{
onError: () => [],
},
);
const checkSortedArraySelector = new DebugInfoCheckUtil(sortedArraySelector);

let state: any = { list: [] };

const result1 = sortedArraySelector(state);
expect(result1).toEqual([]);
checkSortedArraySelector.expectInvoked('run');

// Oops, we can't sort a nonexistent value
state = {};

// But it's ok: onError recovers with an empty array, which is still detected as a phantom run
const result2 = sortedArraySelector(state);
expect(result2).toEqual([]);
expect(result2).toStrictEqual(result1);
checkSortedArraySelector.expectInvoked('aborted');
});
});
72 changes: 72 additions & 0 deletions packages/core/tests/recursion.test.ts
@@ -0,0 +1,72 @@
import { createDynamicSelector, DynamicSelectorFn } from '../src';
import DebugInfoCheckUtil from './util/debugInfoCheckUtil';

describe('recursion', () => {
test('Fibonacci(3)', () => {
const fibonacciSelector: DynamicSelectorFn = createDynamicSelector((_getState, num: number) => {
if (num < 1) {
return 0;
} else if (num === 1) {
return 1;
}
return fibonacciSelector(num - 1) + fibonacciSelector(num - 2);
});

const fibonacciSelectorCheck1 = new DebugInfoCheckUtil(fibonacciSelector, 1);
const fibonacciSelectorCheck2 = new DebugInfoCheckUtil(fibonacciSelector, 2);
const fibonacciSelectorCheck3 = new DebugInfoCheckUtil(fibonacciSelector, 3);

let state = {};

expect(fibonacciSelector(state, 3)).toEqual(2);

fibonacciSelectorCheck3.expectInvoked('run');
fibonacciSelectorCheck2.expectInvoked('run');
fibonacciSelectorCheck1.expectMultiple([
['invoked', 'run'],
['invoked', 'skipped'],
]);
});

test('Fibonacci(6)', () => {
const fibonacciSelector: DynamicSelectorFn = createDynamicSelector((_getState, num: number) => {
if (num < 1) {
return 0;
} else if (num === 1) {
return 1;
}
return fibonacciSelector(num - 1) + fibonacciSelector(num - 2);
});

const fibonacciSelectorCheck1 = new DebugInfoCheckUtil(fibonacciSelector, 1);
const fibonacciSelectorCheck2 = new DebugInfoCheckUtil(fibonacciSelector, 2);
const fibonacciSelectorCheck3 = new DebugInfoCheckUtil(fibonacciSelector, 3);
const fibonacciSelectorCheck4 = new DebugInfoCheckUtil(fibonacciSelector, 4);
const fibonacciSelectorCheck5 = new DebugInfoCheckUtil(fibonacciSelector, 5);
const fibonacciSelectorCheck6 = new DebugInfoCheckUtil(fibonacciSelector, 6);

let state = {};

expect(fibonacciSelector(state, 6)).toEqual(8);

// Because of the caching, none will be called more than twice
fibonacciSelectorCheck6.expectInvoked('run');
fibonacciSelectorCheck5.expectInvoked('run');
fibonacciSelectorCheck4.expectMultiple([
['invoked', 'run'],
['invoked', 'skipped'],
]);
fibonacciSelectorCheck3.expectMultiple([
['invoked', 'run'],
['invoked', 'skipped'],
]);
fibonacciSelectorCheck2.expectMultiple([
['invoked', 'run'],
['invoked', 'skipped'],
]);
fibonacciSelectorCheck1.expectMultiple([
['invoked', 'run'],
['invoked', 'skipped'],
]);
});
});

0 comments on commit f39492d

Please sign in to comment.