UNPKG

@graphistry/falcor

Version:
226 lines (198 loc) 10.1 kB
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); }