@graphistry/falcor
Version:
A JavaScript library for efficient data fetching.
368 lines (322 loc) • 14.9 kB
JavaScript
var arr = new Array(2);
var onValue = require('./onValue');
var FalcorJSON = require('./FalcorJSON');
var isExpired = require('../../isExpired');
var onValueType = require('../onValueType');
var originalOnMissing = require('../onMissing');
var getReferenceTarget = require('./getReferenceTarget');
var onMaterialize = require('../onMaterializeFlatBuffer');
var NullInPathError = require('../../../errors/NullInPathError');
var InvalidKeySetError = require('../../../errors/InvalidKeySetError');
var getHashCode = require('@graphistry/falcor-path-utils/lib/getHashCode');
var flatBufferToPaths = require('@graphistry/falcor-path-utils/lib/flatBufferToPaths');
module.exports = walkPathAndBuildOutput;
/* eslint-disable camelcase */
/* eslint-disable no-cond-assign */
/* eslint-disable no-constant-condition */
function walkPathAndBuildOutput(root, node, jsonArg, path,
depth, seed, results, requestedPath,
optimizedPath, optimizedLength,
fromReferenceArg, referenceContainer,
modelRoot, expired, expireImmediate,
branchSelector, boxValues, materialized,
hasDataSource, treatErrorsAsValues,
allowFromWhenceYouCame) {
var json = jsonArg, 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 (undefined === node ||
undefined !== (type = node.$type) ||
undefined === path) {
arr[1] = node === undefined;
arr[0] = onValueType(node, type, json,
path, depth, seed, results,
requestedPath, depth,
optimizedPath, optimizedLength,
fromReference, modelRoot, expired, expireImmediate,
branchSelector, boxValues, materialized, hasDataSource,
treatErrorsAsValues, onValue, onMissing, onMaterialize);
return arr;
}
var f_meta, f_old_keys, f_new_keys, f_code = '';
var next, nextKey,
keyset, keyIsRange,
keys = path['$keys'],
nodeAbsPath, jsonAbsPath,
nextDepth = depth + 1, rangeEnd,
nextJSON, nextReferenceContainer,
nextOptimizedLength, nextOptimizedPath,
optimizedLengthNext = optimizedLength + 1,
refContainerAbsPath, refContainerRefPath;
if (allowFromWhenceYouCame && referenceContainer) {
refContainerRefPath = referenceContainer.value;
refContainerAbsPath = referenceContainer[f_abs_path];
}
if (!json || 'object' !== typeof json) {
json = undefined;
} else if (f_meta = json[f_meta_data]) {
nodeAbsPath = node[f_abs_path];
jsonAbsPath = f_meta[f_meta_abs_path];
if (!branchSelector && !(json instanceof FalcorJSON)) {
json.__proto__ = {};
json.__proto__[f_meta_data] = f_meta;
json.__proto__.__proto__ = FalcorJSON.prototype;
}
if (!arrayEqual(nodeAbsPath, jsonAbsPath)) {
f_meta['$code'] = '';
f_meta[f_meta_status] = hasDataSource && 'pending' || 'incomplete';
f_meta[f_meta_abs_path] = nodeAbsPath;
f_meta[f_meta_version] = node[f_version];
refContainerRefPath && (f_meta[f_meta_deref_to] = refContainerRefPath);
refContainerAbsPath && (f_meta[f_meta_deref_from] = refContainerAbsPath);
if (f_old_keys = f_meta[f_meta_keys]) {
f_meta[f_meta_keys] = Object.create(null);
for (nextKey in f_old_keys) {
if (f_old_keys[nextKey]) {
delete json[nextKey];
}
}
}
arr[0] = json;
arr[1] = true;
return arr;
} else if (!(
f_meta[f_meta_version] !== node[f_version] ||
f_meta['$code'] !== path['$code'])) {
results.hasValue = true;
arr[0] = json;
arr[1] = false;
return arr;
}
f_old_keys = f_meta[f_meta_keys];
f_meta[f_meta_abs_path] = nodeAbsPath;
f_meta[f_meta_version] = node[f_version];
refContainerRefPath && (f_meta[f_meta_deref_to] = refContainerRefPath);
refContainerAbsPath && (f_meta[f_meta_deref_from] = refContainerAbsPath);
}
f_new_keys = Object.create(null);
var keysIndex = -1;
var keysLength = keys.length;
var nextPath, nextPathKey,
hasMissingPath = false;
iteratingKeyset:
while (++keysIndex < keysLength) {
keyset = keys[keysIndex];
nextPath = path[keysIndex];
// 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 (nextPath !== undefined) {
throw new NullInPathError();
}
f_code = '' + getHashCode('' + f_code + 'null');
continue;
}
// If the keyset is a primitive value, we've found our `nextKey`.
else if ('object' !== typeof keyset) {
nextKey = keyset;
rangeEnd = undefined;
keyIsRange = false;
nextPathKey = nextKey;
}
// If the Keyset isn't null or primitive, 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;
nextPathKey = '[' + nextKey + '..' + rangeEnd + ']';
}
// Now that we have the next key, step down one level in the cache.
do {
fromReference = false;
nextJSON = json && json[nextKey];
nextOptimizedPath = optimizedPath;
nextOptimizedLength = optimizedLengthNext;
nextReferenceContainer = referenceContainer;
next = node[nextKey];
requestedPath[depth] = nextKey;
optimizedPath[optimizedLength] = nextKey;
if (nextPath === undefined) {
walkPathAndBuildOutput(
root, next, nextJSON, nextPath, nextDepth, seed,
results, requestedPath, nextOptimizedPath,
nextOptimizedLength, fromReference, nextReferenceContainer,
modelRoot, expired, expireImmediate, branchSelector, boxValues,
materialized, hasDataSource, treatErrorsAsValues, allowFromWhenceYouCame
);
if (arr[1] === true) {
hasMissingPath = true;
}
if ((nextJSON = arr[0]) === undefined && !materialized) {
if (json && json.hasOwnProperty(nextKey)) {
delete json[nextKey];
}
continue;
}
}
else {
// If we encounter a ref, inline the reference target and continue
// evaluating the path.
if (next &&
// 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)) {
// 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, expireImmediate);
next = refTarget[0];
fromReference = true;
nextOptimizedPath = refTarget[1];
nextReferenceContainer = refTarget[2];
nextOptimizedLength = nextOptimizedPath.length;
refTarget[0] = refTarget[1] = refTarget[2] = undefined;
}
// Continue following the path
// Inspect the return value from the step and determine whether to
// create or write into the JSON branch at this level.
//
// 1. If the next node was a leaf value, nextJSON is the value.
// 2. If the next node was a missing path, nextJSON is undefined.
// 3. If the next node was a branch, then nextJSON will either be an
// Object or undefined. If nextJSON is undefined, all paths under
// this step resolved to missing paths. If it's an Object, then
// at least one path resolved to a successful leaf value.
//
// This check defers creating branches until we see at least one
// cache hit. Otherwise, don't waste the cycles creating a branch
// if everything underneath is a cache miss.
walkPathAndBuildOutput(
root, next, nextJSON, nextPath, nextDepth, seed,
results, requestedPath, nextOptimizedPath,
nextOptimizedLength, fromReference, nextReferenceContainer,
modelRoot, expired, expireImmediate, branchSelector, boxValues,
materialized, hasDataSource, treatErrorsAsValues, allowFromWhenceYouCame
);
if (arr[1] === true) {
hasMissingPath = true;
}
if ((nextJSON = arr[0]) === undefined) {
if (json && json.hasOwnProperty(nextKey)) {
delete json[nextKey];
}
continue;
}
}
// The json value will initially be undefined. If we're here,
// then at least one leaf value was encountered, so create a
// branch to contain it.
if (f_meta === undefined) {
f_meta = {};
f_meta[f_meta_version] = node[f_version];
f_meta[f_meta_abs_path] = node[f_abs_path];
refContainerRefPath && (f_meta[f_meta_deref_to] = refContainerRefPath);
refContainerAbsPath && (f_meta[f_meta_deref_from] = refContainerAbsPath);
json = {};
json[f_meta_data] = f_meta;
json.__proto__ = FalcorJSON.prototype;
// Empower developers to instrument branch node creation by
// providing a custom function. If they do, delegate branch
// node creation to them.
if (branchSelector) {
json = branchSelector(json);
} else {
var tmp = json;
json = {};
json.__proto__ = tmp;
tmp = undefined;
}
}
f_new_keys[nextKey] = true;
if (f_old_keys && (nextKey in f_old_keys)) {
f_old_keys[nextKey] = false;
}
// Set the reported branch or leaf into this branch.
json[nextKey] = nextJSON;
}
// Re-enter the inner loop and continue iterating the Range, or exit
// here if we encountered a Key.
while (keyIsRange && ++nextKey <= rangeEnd);
f_code = '' + getHashCode('' + f_code +
( !hasMissingPath ? nextPathKey : '') +
( nextPath ? nextPath['$code'] : ''));
}
if (f_meta) {
f_meta['$code'] = f_code;
f_meta[f_meta_keys] = f_new_keys;
if (!hasMissingPath) {
f_meta[f_meta_status] = 'resolved';
} else if (hasDataSource) {
f_meta[f_meta_status] = 'pending';
} else if (!materialized) {
f_meta[f_meta_status] = 'incomplete';
} else {
f_meta[f_meta_status] = 'resolved';
}
if (f_old_keys) {
for (nextKey in f_old_keys) {
if (f_old_keys[nextKey]) {
delete json[nextKey];
}
}
}
}
// `json` will be a branch if any cache hits, or undefined if all cache misses
arr[0] = json;
arr[1] = hasMissingPath;
return arr;
}
/* eslint-enable */
function onMissing(path, depth, results,
requestedPath, requestedLength, fromReference,
optimizedPath, optimizedLength, reportMissing,
reportMaterialized, json, branchSelector,
boxValues, onMaterialize, modelRoot) {
if (reportMaterialized) {
return onMaterialize(json, path, depth, depth,
branchSelector, boxValues, modelRoot, reportMissing,
results, requestedPath, optimizedPath, optimizedLength,
fromReference, onMissing);
}
var paths = path ? flatBufferToPaths(path) : [[]];
var rPath = requestedPath.slice(0, requestedLength);
return paths.forEach(function(restPath) {
requestedLength = depth + restPath.length;
return originalOnMissing(rPath.concat(restPath), depth, results,
requestedPath, requestedLength, fromReference,
optimizedPath, optimizedLength, reportMissing,
false, json, branchSelector,
boxValues, onMaterialize, modelRoot);
});
}
function arrayEqual(xs, ys) {
if (xs === ys) {
return true;
}
var len = xs.length;
if (len !== ys.length) {
return false;
}
while (~--len) {
if (xs[len] !== ys[len]) {
return false;
}
}
return true;
}