@graphistry/falcor
Version:
A JavaScript library for efficient data fetching.
226 lines (198 loc) • 10.1 kB
JavaScript
var isArray = Array.isArray;
var clone = require('../../clone');
var onValue = require('./onValue');
var inlineValue = require('./inlineValue');
var isExpired = require('../../isExpired');
var onValueType = require('../onValueType');
var onMaterialize = require('../onMaterialize');
var originalOnMissing = require('../onMissing');
var getReferenceTarget = require('./getReferenceTarget');
var NullInPathError = require('../../../errors/NullInPathError');
var InvalidKeySetError = require('../../../errors/InvalidKeySetError');
var materializedAtom = require('@graphistry/falcor-path-utils/lib/support/materializedAtom');
module.exports = walkPathAndBuildOutput;
/* eslint-disable no-cond-assign */
/* eslint-disable no-constant-condition */
function walkPathAndBuildOutput(root, node, path,
depth, seed, results,
requestedPath, requestedLength,
optimizedPath, optimizedLength,
fromReferenceArg, modelRoot, expired, expireImmediate,
boxValues, materialized, hasDataSource, treatErrorsAsValues) {
var type, refTarget;
var fromReference = fromReferenceArg;
// ============ Check for base cases ================
// If there's nowhere to go, we've reached a terminal node, or hit
// the end of the path, stop now. Either build missing paths or report the value.
if (node === undefined || (
type = node.$type) || (
depth === requestedLength)) {
return onValueType(node, type, seed,
path, depth, seed, results,
requestedPath, requestedLength,
optimizedPath, optimizedLength,
fromReference, modelRoot, expired, expireImmediate,
undefined, boxValues, materialized, hasDataSource,
treatErrorsAsValues, onValue, onMissing, onMaterialize);
}
var next, nextKey,
keyset, keyIsRange,
nextDepth = depth + 1,
rangeEnd, keysOrRanges,
keysetIndex = -1, keysetLength = 0,
nextOptimizedLength, nextOptimizedPath,
optimizedLengthNext = optimizedLength + 1;
keyset = path[depth];
// If `null` appears before the end of the path, throw an error.
// If `null` is at the end of the path, but the reference doesn't
// point to a sentinel value, throw an error.
//
// Inserting `null` at the end of the path indicates the target of a ref
// should be returned, rather than the ref itself. When `null` is the last
// key, the path is lengthened by one, ensuring that if a ref is encountered
// just before the `null` key, the reference is followed before terminating.
if (null === keyset) {
if (nextDepth < requestedLength) {
throw new NullInPathError();
}
return undefined;
}
// Iterate over every key in the keyset. This loop is perhaps a bit clever,
// but we do it this way because this is a performance-sensitive code path.
// This loop simulates a recursive function if we encounter a Keyset that
// contains Keys or Ranges. This is accomplished by a nifty dance between an
// outer loop and an inner loop.
//
// The outer loop is responsible for identifying if the value at this depth
// is a Key, Range, or Keyset. If it encounters a Keyset, the `keysetIndex`,
// `keysetLength`, and `keysOrRanges` variables are assigned and the outer
// loop restarts. If it encounters a Key or Range, `nextKey`, `keyIsRange`,
// and `rangeEnd` are assigned values which signal whether the inner loop
// should iterate a Range or exit after the first run.
//
// The inner loop steps `nextKey` one level down in the cache. If a Range
// was encountered in the outer loop, the inner loop will iterate until the
// Range has been exhausted. If a Key was encountered, the inner loop exits
// after the first execution.
//
// After the inner loop exits, the outer loop iterates the `keysetIndex`
// until the Keyset is exhausted. `keysetIndex` and `keysetLength` are
// initialized to -1 and 0 respectively, so if a Keyset wasn't encountered
// at this depth in the path, then the outer loop exits after one execution.
iteratingKeyset: do {
// If the keyset is a primitive value, we've found our `nextKey`.
if ('object' !== typeof keyset) {
nextKey = keyset;
rangeEnd = undefined;
keyIsRange = false;
}
// If we encounter a Keyset, either iterate through the Keys and Ranges,
// or throw an error if we're already iterating a Keyset. Keysets cannot
// contain other Keysets.
else if (isArray(keyset)) {
// If we've already encountered an Array keyset, throw an error.
if (keysOrRanges !== undefined) {
throw new InvalidKeySetError(path, keysOrRanges);
}
keysetIndex = 0;
keysOrRanges = keyset;
keysetLength = keyset.length;
// If an Array of keys or ranges is empty, terminate the graph walk
// and return the json constructed so far. An example of an empty
// Keyset is: ['lolomo', [], 'summary']. This should short circuit
// without building missing paths.
if (0 === keysetLength) {
break iteratingKeyset;
}
keyset = keysOrRanges[keysetIndex];
// Assign `keyset` to the first value in the Keyset. Re-entering the
// outer loop mimics a singly-recursive function call.
continue iteratingKeyset;
}
// If the Keyset isn't a primitive or Array, then it must be a Range.
else {
rangeEnd = keyset.to;
nextKey = keyset.from || 0;
if ('number' !== typeof rangeEnd) {
rangeEnd = nextKey + (keyset.length || 0) - 1;
}
if ((rangeEnd - nextKey) < 0) {
break iteratingKeyset;
}
keyIsRange = true;
}
// Now that we have the next key, step down one level in the cache.
do {
fromReference = false;
nextOptimizedPath = optimizedPath;
nextOptimizedLength = optimizedLengthNext;
next = node[nextKey];
requestedPath[depth] = nextKey;
optimizedPath[optimizedLength] = nextKey;
// If we encounter a ref, inline the reference target and continue
// evaluating the path.
if (next &&
nextDepth < requestedLength &&
// If the reference is expired, it will be invalidated and
// reported as missing in the next call to walkPath below.
next.$type === $ref && !isExpired(next, expireImmediate)) {
// Write the cloned ref value into the jsonGraph at the
// optimized path. JSONGraph must always clone references.
seed && inlineValue(clone(next), optimizedPath, nextOptimizedLength, seed);
// Retrieve the reference target and next referenceContainer (if
// this reference points to other references) and continue
// following the path. If the reference resolves to a missing
// path or leaf node, it will be handled in the next call to
// walkPath.
refTarget = getReferenceTarget(root, next, modelRoot, seed, expireImmediate);
next = refTarget[0];
fromReference = true;
nextOptimizedPath = refTarget[1];
nextOptimizedLength = nextOptimizedPath.length;
refTarget[0] = refTarget[1] = undefined;
}
walkPathAndBuildOutput(
root, next, path, nextDepth, seed,
results, requestedPath, requestedLength, nextOptimizedPath,
nextOptimizedLength, fromReference, modelRoot, expired, expireImmediate,
boxValues, materialized, hasDataSource, treatErrorsAsValues
);
}
// Re-enter the inner loop and continue iterating the Range, or exit
// here if we encountered a Key.
while (keyIsRange && ++nextKey <= rangeEnd);
// If we've exhausted the Keyset (or never encountered one at all),
// exit the outer loop.
if (++keysetIndex === keysetLength) {
break iteratingKeyset;
}
// Otherwise get the next Key or Range from the Keyset and re-enter the
// outer loop from the top.
keyset = keysOrRanges[keysetIndex];
} while (true);
return undefined;
}
/* eslint-enable */
function onMissing(path, depth, results,
requestedPath, requestedLength, fromReference,
optimizedPath, optimizedLength, reportMissing,
reportMaterialized, seed, branchSelector,
boxValues, onMaterialize, modelRoot) {
var json, isLeaf;
if (seed && reportMaterialized) {
(seed.paths || (seed.paths = [])).push(
(isLeaf = 0 === requestedLength - depth) &&
// depth + 1 if fromReference === true
requestedPath.slice(0, depth + !!fromReference) ||
requestedPath.slice(0, depth).concat(path
.slice(depth, requestedLength + !!fromReference))
);
json = inlineValue(isLeaf && clone(materializedAtom) || undefined,
optimizedPath, optimizedLength, seed, !isLeaf);
}
return originalOnMissing(path, depth, results,
requestedPath, requestedLength, fromReference,
optimizedPath, optimizedLength, reportMissing,
!isLeaf && reportMaterialized, json,
branchSelector, true, onMaterialize, modelRoot);
}