Skip to content

Commit

Permalink
Merge 4af9f3a into 42c2f41
Browse files Browse the repository at this point in the history
  • Loading branch information
spautz committed Oct 14, 2020
2 parents 42c2f41 + 4af9f3a commit 98c888b
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 82 deletions.
85 changes: 60 additions & 25 deletions packages/core/src/dynamicSelectorForState.ts
@@ -1,6 +1,8 @@
import {
RESULT_ENTRY__STATE_OPTIONS,
RESULT_ENTRY__STATE,
RESULT_ENTRY__ALLOW_EXECUTION,
RESULT_ENTRY__RECORD_DEPENDENCIES,
RESULT_ENTRY__STATE_DEPENDENCIES,
RESULT_ENTRY__CALL_DEPENDENCIES,
RESULT_ENTRY__HAS_RETURN_VALUE,
Expand All @@ -23,8 +25,6 @@ import {
hasAnyStateDependencyChanged,
popCallStackEntry,
pushCallStackEntry,
getTopCallStackEntryWithState,
isFullEntry,
} from './internals';
import {
DynamicSelectorArgsWithoutState,
Expand Down Expand Up @@ -52,7 +52,7 @@ const dynamicSelectorForState = <StateType = any>(
const addStateToArguments = (
args: DynamicSelectorArgsWithState<StateType> | DynamicSelectorArgsWithoutState,
): DynamicSelectorArgsWithState<StateType> => {
const parentCaller = getTopCallStackEntryWithState();
const parentCaller = getTopCallStackEntry();

if (parentCaller) {
if (parentCaller[RESULT_ENTRY__STATE_OPTIONS] !== stateOptions) {
Expand All @@ -74,9 +74,14 @@ const dynamicSelectorForState = <StateType = any>(
innerFn: DynamicSelectorInnerFn<ReturnType>,
options?: Partial<DynamicSelectorOptions<ReturnType, StateType>>,
): DynamicSelectorFn<ReturnType> => {
const { compareResult, createResultCache, displayName, getKeyForParams, onException } = options
? { ...defaultSelectorOptions, ...options }
: defaultSelectorOptions;
const {
compareResult,
createResultCache,
debug,
displayName,
getKeyForParams,
onException,
} = options ? { ...defaultSelectorOptions, ...options } : defaultSelectorOptions;

const resultCache: DynamicSelectorResultCache = createResultCache();

Expand All @@ -95,24 +100,35 @@ const dynamicSelectorForState = <StateType = any>(
): DynamicSelectorResultEntry => {
const paramKey = getKeyForParams(params);
const previousResult = resultCache.get(paramKey);
const parentCaller = getTopCallStackEntry();

const recordDependencies = parentCaller[RESULT_ENTRY__RECORD_DEPENDENCIES];
const allowExecution = parentCaller[RESULT_ENTRY__ALLOW_EXECUTION];

let nextResult: DynamicSelectorResultEntry = createResultEntry(
stateOptions,
state,
allowExecution,
recordDependencies,
previousResult,
);
const parentCaller = getTopCallStackEntry();

const allowExecution = parentCaller !== false;
// When called as part of the call-dependency-check of another selector, we only log a depCheck instead of
// a full invoke and result
let debugInfo: DynamicSelectorDebugInfo = null;

if (process.env.NODE_ENV !== 'production') {
debugInfo = nextResult[RESULT_ENTRY__DEBUG_INFO];
if (isFullEntry(parentCaller)) {
debugInvoked(debugInfo);
if (!debugInfo) {
console.error('Internal error: no debugInfo for dynamic selector in development mode');
} else {
debugDepCheck(nextResult[RESULT_ENTRY__DEBUG_INFO]);
debugInfo._verbose = debug && (typeof debug === 'string' ? debug : displayName);

if (recordDependencies && allowExecution) {
debugInvoked(debugInfo);
} else {
debugDepCheck(debugInfo);
}
}
}

Expand All @@ -122,11 +138,14 @@ const dynamicSelectorForState = <StateType = any>(
const [
,
previousState,
,
,
previousStateDependencies,
previousCallDependencies,
hasPreviousReturnValue,
] = previousResult;

/* istanbul ignore next */
if (false) {
// This block is here ONLY to catch possible errors if the structure of `previousResult` changes
const checkType_previousState: DynamicSelectorResultEntry[typeof RESULT_ENTRY__STATE] = previousState;
Expand Down Expand Up @@ -159,6 +178,8 @@ const dynamicSelectorForState = <StateType = any>(
}
}

// debugLogVerbose(debugInfo, 'canUsePreviousResult?', canUsePreviousResult, previousResult);

// This is the block where we decide what the overall result was: skipped, phantom, full, or aborted
if (canUsePreviousResult && previousResult) {
debugSkippedRun(debugInfo);
Expand Down Expand Up @@ -229,8 +250,8 @@ const dynamicSelectorForState = <StateType = any>(

// At this point we're done with *this* selector: nextResult has everything we need, and debugInfo has been logged.

// We still need to register this selectorFn as a dependency of the parent (if any)
if (isFullEntry(parentCaller)) {
// We still need to register this selectorFn as a dependency of the parent (if any).
if (recordDependencies) {
parentCaller[RESULT_ENTRY__CALL_DEPENDENCIES].push(
createCallDependency(outerFn, params, nextResult[RESULT_ENTRY__RETURN_VALUE]),
);
Expand All @@ -254,7 +275,7 @@ const dynamicSelectorForState = <StateType = any>(
// This makes the rest of the code much simpler: put a fake entry onto the call stack.
// That way, the selectorFn does not know whether or not it's the root, and we don't need to wrap all its
// stack-related and dependency-related work in if/else blocks.
const rootResult = createResultEntry(stateOptions, argsWithState[0]);
const rootResult = createResultEntry(stateOptions, argsWithState[0], true, true);
pushCallStackEntry(rootResult);
}
const result = evaluateSelector(...argsWithState);
Expand Down Expand Up @@ -297,18 +318,37 @@ const dynamicSelectorForState = <StateType = any>(
return null;
};

// Common code for getCachedResult & hasCachedResult
const evaluateSelectorReadOnly = (
args: DynamicSelectorArgsWithState | DynamicSelectorArgsWithoutState,
): DynamicSelectorResultEntry => {
const parentCaller = getTopCallStackEntry();
const argsWithState = addStateToArguments(args);

const rootResult = createResultEntry(stateOptions, argsWithState[0], false, false);
if (parentCaller && parentCaller[RESULT_ENTRY__RECORD_DEPENDENCIES]) {
// If our parent is checking our cache result, record our dependencies onto the parent instead
rootResult[RESULT_ENTRY__RECORD_DEPENDENCIES] = true;
rootResult[RESULT_ENTRY__STATE_DEPENDENCIES] =
parentCaller[RESULT_ENTRY__STATE_DEPENDENCIES];
rootResult[RESULT_ENTRY__CALL_DEPENDENCIES] = parentCaller[RESULT_ENTRY__CALL_DEPENDENCIES];
}

pushCallStackEntry(rootResult);
const result = evaluateSelector(...argsWithState);
popCallStackEntry();

return result;
};

/**
* This is just like the main outerFn, except it prohibits all selectors (this and its dependencies) from
* re-executing.
*/
outerFn.getCachedResult = ((
...args: DynamicSelectorArgsWithState | DynamicSelectorArgsWithoutState
): ReturnType | undefined => {
const argsWithState = addStateToArguments(args);

pushCallStackEntry(false);
const result = evaluateSelector(...argsWithState);
popCallStackEntry();
const result = evaluateSelectorReadOnly(args);

if (result[RESULT_ENTRY__HAS_RETURN_VALUE]) {
return result[RESULT_ENTRY__RETURN_VALUE];
Expand All @@ -319,12 +359,7 @@ const dynamicSelectorForState = <StateType = any>(
outerFn.hasCachedResult = ((
...args: DynamicSelectorArgsWithState | DynamicSelectorArgsWithoutState
): boolean => {
const argsWithState = addStateToArguments(args);

pushCallStackEntry(false);
const result = evaluateSelector(...argsWithState);
popCallStackEntry();

const result = evaluateSelectorReadOnly(args);
return result[RESULT_ENTRY__HAS_RETURN_VALUE];
}) as DynamicSelectorFn<boolean>;

Expand Down
21 changes: 2 additions & 19 deletions packages/core/src/internals/callStack.ts
Expand Up @@ -11,27 +11,10 @@ import { DynamicSelectorResultEntry } from './resultCache';
*
* This single instance is used forever across all selectors.
*/
const callStack: Array<DynamicSelectorResultEntry | boolean> = [];

const isFullEntry = (
entryOrBool: DynamicSelectorResultEntry | boolean,
): entryOrBool is DynamicSelectorResultEntry => entryOrBool !== !!entryOrBool;
const callStack: Array<DynamicSelectorResultEntry> = [];

const getTopCallStackEntry = () => callStack[callStack.length - 1];
const getTopCallStackEntryWithState = (): DynamicSelectorResultEntry | undefined => {
let index = callStack.length - 1;
while (index >= 0 && !isFullEntry(callStack[index])) {
index--;
}
return callStack[index] as DynamicSelectorResultEntry;
};
const pushCallStackEntry = callStack.push.bind(callStack);
const popCallStackEntry = callStack.pop.bind(callStack);

export {
isFullEntry,
getTopCallStackEntry,
getTopCallStackEntryWithState,
pushCallStackEntry,
popCallStackEntry,
};
export { getTopCallStackEntry, pushCallStackEntry, popCallStackEntry };
19 changes: 19 additions & 0 deletions packages/core/src/internals/debugInfo.ts
@@ -1,5 +1,6 @@
// Because this is ONLY used in dev mode, it's stored as a normal object instead of an array
export type DynamicSelectorDebugInfo = {
_verbose?: boolean | string;
depCheckCount: number;
invokeCount: number;
skippedRunCount: number;
Expand All @@ -22,44 +23,62 @@ const createDebugInfo = (): DynamicSelectorDebugInfo => {
return null;
};

const debugLogVerbose = (
debugInfo: DynamicSelectorDebugInfo,
label: string,
...moreInfo: Array<any>
) => {
if (process.env.NODE_ENV !== 'production' && debugInfo && debugInfo._verbose) {
const labelPrefix = typeof debugInfo._verbose === 'string' ? `${debugInfo._verbose}: ` : '';
console.log(labelPrefix + label, ...moreInfo, debugInfo);
}
};

const debugDepCheck = (debugInfo: DynamicSelectorDebugInfo) => {
if (process.env.NODE_ENV !== 'production' && debugInfo) {
debugInfo.depCheckCount++;
debugLogVerbose(debugInfo, 'Begin DepCheck');
}
};

const debugInvoked = (debugInfo: DynamicSelectorDebugInfo) => {
if (process.env.NODE_ENV !== 'production' && debugInfo) {
debugInfo.invokeCount++;
debugLogVerbose(debugInfo, 'Begin Invoke');
}
};

const debugSkippedRun = (debugInfo: DynamicSelectorDebugInfo) => {
if (process.env.NODE_ENV !== 'production' && debugInfo) {
debugInfo.skippedRunCount++;
debugLogVerbose(debugInfo, 'Skipped!');
}
};

const debugPhantomRun = (debugInfo: DynamicSelectorDebugInfo) => {
if (process.env.NODE_ENV !== 'production' && debugInfo) {
debugInfo.phantomRunCount++;
debugLogVerbose(debugInfo, 'Phantom!');
}
};

const debugFullRun = (debugInfo: DynamicSelectorDebugInfo) => {
if (process.env.NODE_ENV !== 'production' && debugInfo) {
debugInfo.fullRunCount++;
debugLogVerbose(debugInfo, 'Full run!');
}
};

const debugAbortedRun = (debugInfo: DynamicSelectorDebugInfo) => {
if (process.env.NODE_ENV !== 'production' && debugInfo) {
debugInfo.abortedRunCount++;
debugLogVerbose(debugInfo, 'Aborted!');
}
};

export {
createDebugInfo,
debugLogVerbose,
debugDepCheck,
debugInvoked,
debugSkippedRun,
Expand Down
19 changes: 7 additions & 12 deletions packages/core/src/internals/dependencies.ts
@@ -1,6 +1,6 @@
import { DynamicSelectorFn, DynamicSelectorParams, DynamicSelectorStateGetFn } from '../types';
import {
DynamicSelectorResultEntry,
createDepCheckEntry,
RESULT_ENTRY__HAS_RETURN_VALUE,
RESULT_ENTRY__RETURN_VALUE,
} from './resultCache';
Expand Down Expand Up @@ -66,7 +66,7 @@ const hasAnyCallDependencyChanged = (
const numPreviousCallDependencies = previousCallDependencies.length;
if (numPreviousCallDependencies) {
// We're just checking dependencies, not re-registering them, so put a dummy entry on the call stack.
pushCallStackEntry(allowExecution);
pushCallStackEntry(createDepCheckEntry(allowExecution));

for (let i = 0; i < numPreviousCallDependencies; i += 1) {
const [
Expand All @@ -75,6 +75,7 @@ const hasAnyCallDependencyChanged = (
dependencyReturnValue,
] = previousCallDependencies[i];

/* istanbul ignore next */
if (false) {
// This block is here ONLY to catch possible errors if the structure of `previousCallDependencies` changes
const checkType_selectorFn: DynamicSelectorCallDependency[typeof CALL_DEPENDENCY__SELECTOR_FN] = dependencySelectorFn;
Expand All @@ -85,17 +86,11 @@ const hasAnyCallDependencyChanged = (

// Does our dependency have anything new? Let's run it to find out.
const result = dependencySelectorFn._callDirect(state, dependencyParams, ...otherArgs);
const [, , , , hasReturnValue, newReturnValue] = result;
const hasReturnValue = result[RESULT_ENTRY__HAS_RETURN_VALUE];
const newReturnValue = result[RESULT_ENTRY__RETURN_VALUE];

if (false) {
// This block is here ONLY to catch possible errors if the structure of `result` changes
const checkType_hasReturnValue: DynamicSelectorResultEntry[typeof RESULT_ENTRY__HAS_RETURN_VALUE] = hasReturnValue;
const checkType_newReturnValue: DynamicSelectorResultEntry[typeof RESULT_ENTRY__RETURN_VALUE] = newReturnValue;
console.log({ checkType_hasReturnValue, checkType_newReturnValue });
}

if (hasReturnValue && newReturnValue !== dependencyReturnValue) {
// Something returned a new value: that's a real change.
if (!hasReturnValue || newReturnValue !== dependencyReturnValue) {
// Something either failed to return a value, or it returned something new.
// (We use strict equality -- not compareResult -- because compareResult was already used to decide whether to
// return the exact prior value)
popCallStackEntry();
Expand Down

0 comments on commit 98c888b

Please sign in to comment.