grafast
Version:
Cutting edge GraphQL planning and execution engine
962 lines (961 loc) • 61 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeBucket = executeBucket;
exports.newBucket = newBucket;
exports.bucketToString = bucketToString;
exports.batchExecutionValue = batchExecutionValue;
exports.unaryExecutionValue = unaryExecutionValue;
const tslib_1 = require("tslib");
const iterall_1 = require("iterall");
const assert = tslib_1.__importStar(require("../assert.js"));
const constants_js_1 = require("../constants.js");
const dev_js_1 = require("../dev.js");
const error_js_1 = require("../error.js");
const inspect_js_1 = require("../inspect.js");
const timeSource_js_1 = require("../timeSource.js");
const utils_js_1 = require("../utils.js");
/** Default recoverable/trappable flags (excluding NULL) */
const INHIBIT_OR_ERROR_FLAGS = constants_js_1.FLAG_INHIBITED | constants_js_1.FLAG_ERROR;
const timeoutError = Object.freeze(new error_js_1.SafeError("Execution timeout exceeded, please simplify or add limits to your request.", Object.freeze({ [constants_js_1.$$timeout]: true })));
function noop() {
/*noop*/
}
/**
* Takes a list of `results` (shorter than `resultCount`) and an object with
* errors and indexes; returns a list of length `resultCount` with the results
* from `results` but with errors injected at the indexes specified in
* `errors`.
*
* ASSERT: `results.length + Object.values(errors).length === resultCount`
*
* @internal
*/
function mergeErrorsBackIn(results, forcedValues, resultCount) {
const finalFlags = [];
const finalResults = [];
/** The index within `results`, which is shorter than `resultCount` */
let resultIndex = 0;
for (let finalIndex = 0; finalIndex < resultCount; finalIndex++) {
const flags = forcedValues.flags[finalIndex];
if (flags !== undefined) {
const value = forcedValues.results[finalIndex];
finalResults[finalIndex] = value;
finalFlags[finalIndex] = flags;
}
else {
finalResults[finalIndex] = results[resultIndex++];
finalFlags[finalIndex] = 0;
}
}
return { flags: finalFlags, results: finalResults };
}
/** @internal */
function executeBucket(bucket, requestContext) {
/**
* Execute the step directly; since there's no errors we can pass the
* dependencies through verbatim.
*/
function reallyExecuteStepWithoutFiltering(size, step, dependencies, extra) {
const results = executeOrStream(size, step, dependencies, extra);
const flags = (0, utils_js_1.arrayOfLength)(size, constants_js_1.NO_FLAGS);
if ((0, utils_js_1.isPromiseLike)(results)) {
return results.then((results) => ({ flags, results }));
}
else {
return { flags, results };
}
}
const { stopTime, eventEmitter, args } = requestContext;
const { middleware } = args;
const { metaByMetaKey, size, store, layerPlan: { phases }, } = bucket;
const phaseCount = phases.length;
// Like a `for(i = 0; i < phaseCount; i++)` loop with some `await`s in it, except it does promise
// handling manually so that it can complete synchronously (no promises) if
// possible.
const nextPhase = (phaseIndex) => {
if (phaseIndex >= phaseCount) {
return;
}
const phase = phases[phaseIndex];
const { _allSteps } = phase;
/**
* To ensure we don't enter a situation where an "unhandled" promise
* rejection causes Node to exit, we must process each completed step during the
* same tick in which it completes.
*/
let indexesPendingLoopOver = [];
let executePromises = null;
let executePromiseResultIndex = null;
const resultList = [];
if (phase.checkTimeout &&
stopTime !== null &&
timeSource_js_1.timeSource.now() >= stopTime) {
// ABORT!
if (phase.normalSteps !== undefined) {
const normalSteps = phase.normalSteps;
for (let normalStepIndex = 0, l = normalSteps.length; normalStepIndex < l; normalStepIndex++) {
const step = normalSteps[normalStepIndex].step;
const stepSize = step._isUnary ? 1 : bucket.size;
const r = timeoutError;
const results = (0, utils_js_1.arrayOfLength)(stepSize, r);
const flags = (0, utils_js_1.arrayOfLength)(stepSize, constants_js_1.FLAG_ERROR);
resultList[normalStepIndex] = { flags, results };
indexesPendingLoopOver.push(normalStepIndex);
// TODO: I believe we can remove this line?
bucket.flagUnion |= constants_js_1.FLAG_ERROR;
}
}
}
else if (phase.normalSteps !== undefined) {
const normalSteps = phase.normalSteps;
for (let normalStepIndex = 0, l = normalSteps.length; normalStepIndex < l; normalStepIndex++) {
const step = normalSteps[normalStepIndex].step;
const stepSize = step._isUnary ? 1 : bucket.size;
try {
const r = executeStep(step);
if ((0, utils_js_1.isPromiseLike)(r)) {
resultList[normalStepIndex] = undefined /* will populate shortly */;
if (!executePromises) {
executePromises = [r];
executePromiseResultIndex = [normalStepIndex];
}
else {
const newIndex = executePromises.push(r) - 1;
executePromiseResultIndex[newIndex] = normalStepIndex;
}
}
else {
resultList[normalStepIndex] = r;
indexesPendingLoopOver.push(normalStepIndex);
}
}
catch (e) {
const r = e;
const results = (0, utils_js_1.arrayOfLength)(stepSize, r);
const flags = (0, utils_js_1.arrayOfLength)(stepSize, constants_js_1.FLAG_ERROR);
resultList[normalStepIndex] = { flags, results };
indexesPendingLoopOver.push(normalStepIndex);
// TODO: I believe we can remove this line?
bucket.flagUnion |= constants_js_1.FLAG_ERROR;
}
}
}
const next = () => {
return nextPhase(phaseIndex + 1);
};
/**
* Loops over (and resets) the indexesPendingLoopOver array, ensuring that
* all errors are handled.
*/
const loopOverResults = () => {
if (indexesPendingLoopOver.length === 0)
return;
const indexesToProcess = indexesPendingLoopOver;
indexesPendingLoopOver = [];
// Validate executed steps
for (const allStepsIndex of indexesToProcess) {
const finishedStep = _allSteps[allStepsIndex];
const internalResult = resultList[allStepsIndex];
if (!internalResult) {
throw new Error(`GrafastInternalError<166ef53d-b80d-4bea-8b54-803c2694112a>: Result from ${finishedStep} should exist`);
}
const { /* flags, */ results } = internalResult;
const resultLength = results?.length;
const expectedSize = finishedStep._isUnary ? 1 : size;
if (resultLength !== expectedSize) {
if (!Array.isArray(results)) {
throw new Error(`Result from ${finishedStep} should be an array, instead received ${(0, inspect_js_1.inspect)(results, { colors: true })}`);
}
throw new Error(`Result array from ${finishedStep} should have length ${expectedSize}${finishedStep._isUnary ? " (because it's unary)" : ""}, instead it had length ${results.length}`);
}
if (finishedStep._isUnary) {
// Handled later
}
else {
bucket.store.set(finishedStep.id, batchExecutionValue((0, utils_js_1.arrayOfLength)(size)));
}
}
// Need to complete promises, check for errors, etc.
// **DO NOT THROW, DO NOT ALLOW AN ERROR TO BE RAISED!**
// **USE DEFENSIVE PROGRAMMING HERE!**
/** PROMISES ADDED HERE MUST NOT REJECT */
let promises;
let pendingPromises;
let pendingPromiseIndexes;
// TODO: it seems that if this throws an error it results in a permanent
// hang of defers? In the mean time... Don't throw any errors here!
const success = (finishedStep, bucket, resultIndex, value, flags) => {
// Fast lanes
if (value == null) {
// null / undefined
const finalFlags = flags | constants_js_1.FLAG_NULL;
bucket.setResult(finishedStep, resultIndex, value, finalFlags);
return;
}
else if (typeof value !== "object") {
// non-objects
bucket.setResult(finishedStep, resultIndex, value, flags);
return;
}
else if ((0, error_js_1.isFlaggedValue)(value)) {
// flagged values
const finalFlags = flags | value.flags;
bucket.setResult(finishedStep, resultIndex, value.value, finalFlags);
return;
}
let valueIsAsyncIterable;
if (finishedStep._stepOptions.walkIterable &&
// PERF: do we want to handle arrays differently?
((valueIsAsyncIterable = (0, iterall_1.isAsyncIterable)(value)) || (0, iterall_1.isIterable)(value))) {
// PERF: we've already calculated this once; can we reference that again here?
const stream = evaluateStream(bucket, finishedStep);
if (!stream && !valueIsAsyncIterable && Array.isArray(value)) {
// Fast mode
const valueCount = value.length;
/** We only create a duplicate array if the source array contains promises */
let replacement = null;
for (let i = 0; i < valueCount; i++) {
const item = value[i];
if ((0, utils_js_1.isPromiseLike)(item)) {
if (replacement === null) {
// Copy up to here!
replacement = value.slice(0, i);
}
replacement[i] = null;
const index = i;
const promise = item.then((v) => void (replacement[index] = v), (e) => void (replacement[index] = (0, error_js_1.flagError)(e)));
if (!promises) {
promises = [promise];
}
else {
promises.push(promise);
}
}
else if (replacement !== null) {
replacement[i] = item;
}
}
const list = replacement === null ? value : replacement;
bucket.setResult(finishedStep, resultIndex, list, flags);
return;
}
const initialCount = stream?.initialCount ?? Infinity;
let iterator;
try {
iterator = valueIsAsyncIterable
? value[Symbol.asyncIterator]()
: value[Symbol.iterator]();
}
catch (e) {
bucket.setResult(finishedStep, resultIndex, e, flags | constants_js_1.FLAG_ERROR);
return;
}
// Here we track the iterator via the bucket, this allows us to
// ensure that the iterator is terminated even if the stream is never
// consumed (e.g. if an error is thrown/caught during execution of
// the output plan).
if (!bucket.iterators[resultIndex]) {
bucket.iterators[resultIndex] = new Set();
}
bucket.iterators[resultIndex].add(iterator);
if (initialCount === 0) {
// Optimization - defer everything
const arr = [];
arr[constants_js_1.$$streamMore] = iterator;
bucket.setResult(finishedStep, resultIndex, arr, flags);
}
else {
// Evaluate the first initialCount entries, rest is streamed.
const promise = (async () => {
try {
let valuesSeen = 0;
const arr = [];
/*
* We need to "shift" a few entries off the top of the
* iterator, but still keep it iterable for the later
* stream. To accomplish this we have to do manual
* looping
*/
let resultPromise;
while ((resultPromise = iterator.next())) {
const resolvedResult = await resultPromise;
if (resolvedResult.done) {
break;
}
try {
const v = resolvedResult.value;
if ((0, utils_js_1.isPromiseLike)(v)) {
arr.push(await v);
}
else {
arr.push(v);
}
}
catch (e) {
arr.push((0, error_js_1.flagError)(e));
}
if (++valuesSeen >= initialCount) {
// This is safe to do in the `while` since we checked
// the `0` entries condition in the optimization
// above.
arr[constants_js_1.$$streamMore] = iterator;
break;
}
}
bucket.setResult(finishedStep, resultIndex, arr, flags);
}
catch (e) {
bucket.setResult(finishedStep, resultIndex, e, flags | constants_js_1.FLAG_ERROR);
}
})();
if (!promises) {
promises = [promise];
}
else {
promises.push(promise);
}
}
}
else {
bucket.setResult(finishedStep, resultIndex, value, flags);
}
};
for (const allStepsIndex of indexesToProcess) {
const step = _allSteps[allStepsIndex];
const internalResult = resultList[allStepsIndex];
if (!internalResult) {
throw new Error(`GrafastInternalError<b82038d6-ca4e-4b55-8ed4-4d7c879f5dc2>: Result from ${step} should exist`);
}
const { flags, results } = internalResult;
const count = step._isUnary ? 1 : size;
for (let dataIndex = 0; dataIndex < count; dataIndex++) {
const val = results[dataIndex];
if (step.isSyncAndSafe || !(0, utils_js_1.isPromiseLike)(val)) {
if (flags[dataIndex] == null) {
throw new Error(`GraphileInternalError<75df71bb-0f76-4a98-9664-9167d502296a>: result for ${step} has no flag at index ${dataIndex} (value = ${(0, inspect_js_1.inspect)(val)})`);
}
success(step, bucket, dataIndex, val, flags[dataIndex]);
}
else {
if (!pendingPromises) {
pendingPromises = [val];
pendingPromiseIndexes = [{ s: allStepsIndex, i: dataIndex }];
}
else {
pendingPromises.push(val);
pendingPromiseIndexes.push({ s: allStepsIndex, i: dataIndex });
}
}
}
}
if (pendingPromises !== undefined) {
return Promise.allSettled(pendingPromises)
.then((resultSettledResult) => {
for (let i = 0, pendingPromisesLength = resultSettledResult.length; i < pendingPromisesLength; i++) {
const settledResult = resultSettledResult[i];
const { s: allStepsIndex, i: dataIndex } = pendingPromiseIndexes[i];
const finishedStep = _allSteps[allStepsIndex];
if (settledResult.status === "fulfilled") {
success(finishedStep, bucket, dataIndex, settledResult.value, constants_js_1.NO_FLAGS);
}
else {
const error = settledResult.reason;
bucket.setResult(finishedStep, dataIndex, error, constants_js_1.FLAG_ERROR);
}
}
return promises ? Promise.all(promises) : undefined;
})
.then(null, (e) => {
// THIS SHOULD NEVER HAPPEN!
console.error(`GrafastInternalError<1e9731b4-005e-4b0e-bc61-43baa62e6444>: this error should never occur! Please file an issue against grafast. Details: ${e}`);
bucket.flagUnion |= constants_js_1.FLAG_ERROR;
for (let i = 0, pendingPromisesLength = pendingPromises.length; i < pendingPromisesLength; i++) {
const { s: allStepsIndex, i: dataIndex } = pendingPromiseIndexes[i];
const finishedStep = _allSteps[allStepsIndex];
const error = new Error(`GrafastInternalError<1e9731b4-005e-4b0e-bc61-43baa62e6444>: error occurred whilst performing completedStep(${finishedStep.id})`);
bucket.setResult(finishedStep, dataIndex, error, constants_js_1.FLAG_ERROR);
}
});
}
else {
return promises ? Promise.all(promises) : undefined;
}
};
const runSyncSteps = () => {
const executedLength = resultList.length;
if (dev_js_1.isDev) {
assert.strictEqual(executedLength, phase.normalSteps?.length ?? 0, "Expected only and all normalSteps to have executed");
}
if (!phase.unbatchedSyncAndSafeSteps) {
return next();
}
const allStepsLength = _allSteps.length;
const extras = [];
for (let allStepsIndex = executedLength; allStepsIndex < allStepsLength; allStepsIndex++) {
const step = _allSteps[allStepsIndex];
const meta = step.metaKey !== undefined ? metaByMetaKey[step.metaKey] : undefined;
extras[allStepsIndex] = {
stopTime,
meta,
eventEmitter,
stream: evaluateStream(bucket, step),
_bucket: bucket,
_requestContext: requestContext,
};
if (step._isUnary) {
// Handled later
}
else {
bucket.store.set(step.id, batchExecutionValue((0, utils_js_1.arrayOfLength)(size)));
}
}
for (let dataIndex = 0; dataIndex < size; dataIndex++) {
stepLoop: for (let allStepsIndex = executedLength; allStepsIndex < allStepsLength; allStepsIndex++) {
const step = (0, utils_js_1.sudo)(_allSteps[allStepsIndex]);
// Unary steps only need to be processed once
if (step._isUnary && dataIndex !== 0) {
continue;
}
// Check if the side effect errored
const $sideEffect = step.implicitSideEffectStep;
if ($sideEffect) {
let currentPolymorphicPath;
if ($sideEffect.polymorphicPaths === null ||
(currentPolymorphicPath =
bucket.polymorphicPathList[dataIndex]) === null ||
$sideEffect.polymorphicPaths.has(currentPolymorphicPath)) {
const depExecutionValue = bucket.store.get($sideEffect.id);
if (depExecutionValue === undefined) {
throw new Error(`GrafastInternalError<fcc8d302-ac66-40e8-aaea-ee2c7e2b30b2>: failed to get result for side effect ${$sideEffect} which impacts ${step}`);
}
const depFlags = depExecutionValue._flagsAt(dataIndex);
if (depFlags & constants_js_1.FLAG_POLY_SKIPPED) {
/* noop */
}
else if (depFlags & constants_js_1.FLAG_ERROR) {
if (step._isUnary) {
// COPY the unary value
bucket.store.set(step.id, depExecutionValue);
bucket.flagUnion |= depFlags;
}
else {
const depVal = depExecutionValue.at(dataIndex);
bucket.setResult(step, dataIndex, depVal, depFlags);
}
continue stepLoop;
}
}
}
try {
const deps = [];
const extra = extras[allStepsIndex];
let forceIndexValue = undefined;
let rejectValue = undefined;
let indexFlags = constants_js_1.NO_FLAGS;
// Check polymorphism matches
const stepPolymorphicPaths = step.polymorphicPaths;
if (stepPolymorphicPaths !== null &&
(step._isUnary
? /*
* already asserted that it matches;
* !! search "f4ce2c83"
*/
false
: /* batch check */
!stepPolymorphicPaths.has(bucket.polymorphicPathList[dataIndex]))) {
indexFlags |= constants_js_1.FLAG_POLY_SKIPPED;
forceIndexValue = null;
}
else {
for (let i = 0, l = step.dependencies.length; i < l; i++) {
const $dep = step.dependencies[i];
const forbiddenFlags = step.dependencyForbiddenFlags[i];
const onReject = step.dependencyOnReject[i];
const depExecutionVal = bucket.store.get($dep.id);
if (depExecutionVal === undefined) {
throw new Error(`GrafastInternalError<480e7c98-a777-4efb-b826-c339129ccff8>: could not find value in ${bucket} for ${$dep}, dependency ${i} of ${step}`);
}
// Search for "f2b3b1b3" for similar block
const flags = depExecutionVal._flagsAt(dataIndex);
const disallowedFlags = flags & forbiddenFlags;
if (disallowedFlags) {
indexFlags |= disallowedFlags;
// If there's a reject behavior and we're FRESHLY rejected (weren't
// already inhibited), use that as a fallback.
// TODO: validate this.
// If dep is inhibited and we don't allow inhibited, copy through (null or error).
// If dep is inhibited and we do allow inhibited, but we're disallowed, use our onReject.
// If dep is not inhibited, but we're disallowed, use our onReject.
if (onReject !== undefined &&
(disallowedFlags & INHIBIT_OR_ERROR_FLAGS) === constants_js_1.NO_FLAGS) {
rejectValue ||= onReject;
}
if (forceIndexValue == null) {
if ((flags & constants_js_1.FLAG_ERROR) !== constants_js_1.NO_FLAGS) {
const v = depExecutionVal.at(dataIndex);
forceIndexValue = v;
}
else {
forceIndexValue = null;
}
}
else {
// First error wins, ignore this second error.
}
// End "f2b3b1b3" block
}
else if (forceIndexValue === undefined) {
const depVal = depExecutionVal.at(dataIndex);
let depFlags;
if ((bucket.flagUnion & constants_js_1.FLAG_ERROR) === constants_js_1.FLAG_ERROR &&
((depFlags = depExecutionVal._flagsAt(dataIndex)) &
constants_js_1.FLAG_ERROR) ===
constants_js_1.FLAG_ERROR) {
if (step._isUnary) {
// COPY the unary value
bucket.store.set(step.id, depExecutionVal);
bucket.flagUnion |= depFlags;
}
else {
bucket.setResult(step, dataIndex, depVal, constants_js_1.FLAG_ERROR);
}
continue stepLoop;
}
deps.push(depVal);
}
}
}
let stepResult, stepFlags;
if (forceIndexValue !== undefined) {
// Search for "17217999b7a7" for similar block
if ((indexFlags & constants_js_1.FLAG_POLY_SKIPPED) === constants_js_1.FLAG_POLY_SKIPPED) {
// Already skipped
}
else if (forceIndexValue == null && rejectValue != null) {
indexFlags |= constants_js_1.FLAG_ERROR;
forceIndexValue = rejectValue;
}
else {
indexFlags |= constants_js_1.FLAG_INHIBITED;
}
// End "17217999b7a7" block
stepResult = forceIndexValue;
stepFlags = indexFlags;
}
else {
const rawStepResult = step.unbatchedExecute(extra, ...deps);
if (typeof rawStepResult === "object" &&
rawStepResult !== null &&
(0, error_js_1.isFlaggedValue)(rawStepResult)) {
stepResult = rawStepResult.value;
stepFlags = rawStepResult.flags;
}
else {
stepResult = rawStepResult;
stepFlags = rawStepResult == null ? constants_js_1.FLAG_NULL : constants_js_1.NO_FLAGS;
}
}
// TODO: what if stepResult is _returned_ error (as opposed to
// thrown)?
// NOTE: we are in `runSyncSteps` so this step is guaranteed to
// be "isSyncAndSafe". As such, we don't need to worry about it
// returning an error (unsafe) or a promise (async), we only
// need to check if it's null.
bucket.setResult(step, dataIndex, stepResult, stepFlags);
}
catch (e) {
bucket.setResult(step, dataIndex, e, constants_js_1.FLAG_ERROR);
}
}
}
return next();
};
if (executePromises !== null) {
const processedPromises = [];
if (indexesPendingLoopOver.length > 0) {
// This **must be done in the same tick**
const promiseOrNot = loopOverResults();
if ((0, utils_js_1.isPromiseLike)(promiseOrNot)) {
processedPromises.push(promiseOrNot);
}
}
for (let i = 0, l = executePromises.length; i < l; i++) {
const executePromise = executePromises[i];
const index = executePromiseResultIndex[i];
processedPromises.push(executePromise.then((promiseResult) => {
resultList[index] = promiseResult;
indexesPendingLoopOver.push(index);
// We must loop over the results in the same tick in which the
// promise resolved.
return loopOverResults();
}));
}
return Promise.all(processedPromises).then(runSyncSteps);
}
else {
const promiseOrNot = loopOverResults();
if ((0, utils_js_1.isPromiseLike)(promiseOrNot)) {
return promiseOrNot.then(runSyncSteps);
}
else {
return runSyncSteps();
}
}
};
const promise = nextPhase(0);
if ((0, utils_js_1.isPromiseLike)(promise)) {
return promise.then(executeSamePhaseChildren);
}
else {
return executeSamePhaseChildren();
}
// Function definitions below here
function executeOrStream(count, step, values, extra) {
if (dev_js_1.isDev) {
if (step._isUnary && count !== 1) {
throw new Error(`GrafastInternalError<84a6cdfa-e8fe-4dea-85fe-9426a6a78027>: ${step} is a unary step, but we're attempting to pass it ${count} (!= 1) values`);
}
if (step.execute.length > 1) {
throw new Error(`${step} is using a legacy form of 'execute' which accepts multiple arguments, please see https://err.red/gev2`);
}
}
const executeDetails = {
indexMap: makeIndexMap(count),
indexForEach: makeIndexForEach(count),
count,
values,
extra,
stream: evaluateStream(bucket, step),
};
if (!step.isSyncAndSafe && middleware != null) {
return middleware.run("executeStep", { args, step, executeDetails }, executeStepFromEvent);
}
else {
return step.execute(executeDetails);
}
}
// Slow mode...
/**
* Execute the step, filtering out errors and entries with non-matching
* polymorphicPaths from the input dependencies and then padding the lists
* back out at the end.
*/
function reallyExecuteStepWithFiltering(step, dependenciesIncludingSideEffects, dependencyForbiddenFlags, dependencyOnReject, polymorphicPathList, extra) {
const expectedSize = step._isUnary ? 1 : size;
const forcedValues = {
flags: (0, utils_js_1.arrayOfLength)(expectedSize, undefined),
results: (0, utils_js_1.arrayOfLength)(expectedSize, undefined),
};
/**
* If there's errors/forbidden values, we must manipulate the arrays being
* passed into the step execution
*/
let needsTransform = false;
/** If all we see is errors, there's no need to execute! */
let newSize = 0;
const newPolymorphicPathList = [];
let stepPolymorphicPaths = step.polymorphicPaths;
const legitDepsCount = (0, utils_js_1.sudo)(step).dependencies.length;
const dependenciesIncludingSideEffectsCount = dependenciesIncludingSideEffects.length;
let dependencies = dependenciesIncludingSideEffectsCount > legitDepsCount
? dependenciesIncludingSideEffects.slice(0, legitDepsCount)
: dependenciesIncludingSideEffects;
// OPTIM: if unariesIncludingSideEffects.some(isGrafastError) then shortcut execution because everything fails
let hasPolyMatch = true;
if (step._isUnary && stepPolymorphicPaths !== null) {
// Check that at least one datapoint matches one of our paths
hasPolyMatch = false;
for (let dataIndex = 0; dataIndex < size; dataIndex++) {
if (stepPolymorphicPaths.has(polymorphicPathList[dataIndex])) {
hasPolyMatch = true;
break;
}
}
stepPolymorphicPaths = null;
}
for (let dataIndex = 0; dataIndex < expectedSize; dataIndex++) {
let forceIndexValue = hasPolyMatch
? undefined
: null;
let rejectValue = undefined;
let indexFlags = hasPolyMatch
? constants_js_1.NO_FLAGS
: constants_js_1.FLAG_POLY_SKIPPED;
if (stepPolymorphicPaths !== null &&
!stepPolymorphicPaths.has(polymorphicPathList[dataIndex])) {
indexFlags |= constants_js_1.FLAG_POLY_SKIPPED;
forceIndexValue = null;
}
else if (extra._bucket.flagUnion !== 0) {
for (let i = 0; i < dependenciesIncludingSideEffectsCount; i++) {
const depExecutionVal = dependenciesIncludingSideEffects[i];
const forbiddenFlags = dependencyForbiddenFlags[i];
const onReject = dependencyOnReject[i];
// Search for "f2b3b1b3" for similar block
const flags = depExecutionVal._flagsAt(dataIndex);
const disallowedFlags = flags & forbiddenFlags;
if (disallowedFlags) {
indexFlags |= disallowedFlags;
// If there's a reject behavior and we're FRESHLY rejected (weren't
// already inhibited), use that as a fallback.
// TODO: validate this.
// If dep is inhibited and we don't allow inhibited, copy through (null or error).
// If dep is inhibited and we do allow inhibited, but we're disallowed, use our onReject.
// If dep is not inhibited, but we're disallowed, use our onReject.
if (onReject !== undefined &&
(disallowedFlags & (constants_js_1.FLAG_INHIBITED | constants_js_1.FLAG_ERROR)) === constants_js_1.NO_FLAGS) {
rejectValue ||= onReject;
}
if (forceIndexValue == null) {
if ((flags & constants_js_1.FLAG_ERROR) !== 0) {
const v = depExecutionVal.at(dataIndex);
forceIndexValue = v;
}
else {
forceIndexValue = null;
}
}
else {
// First error wins, ignore this second error.
}
// End "f2b3b1b3" block
break;
}
}
}
else {
// All good
}
if (forceIndexValue !== undefined) {
if (!needsTransform) {
needsTransform = true;
// Clone up until this point, make mutable, don't add self
dependencies = dependencies.map((ev) => ev.isBatch
? // TODO: move this creation to happen once the full list is
// already built, ideally we shouldn't be mutating an execution
// value later.
batchExecutionValue(ev.entries.slice(0, dataIndex))
: ev);
}
// Search for "17217999b7a7" for similar block
if (forceIndexValue == null && rejectValue != null) {
indexFlags |= constants_js_1.FLAG_ERROR;
forceIndexValue = rejectValue;
}
else {
indexFlags |= constants_js_1.FLAG_INHIBITED;
}
// End "17217999b7a7" block
forcedValues.flags[dataIndex] = indexFlags;
forcedValues.results[dataIndex] = forceIndexValue;
}
else {
const newIndex = newSize++;
newPolymorphicPathList[newIndex] = polymorphicPathList[dataIndex];
if (needsTransform) {
// dependenciesWithoutErrors has limited content; add this non-error value
for (let depListIndex = 0; depListIndex < legitDepsCount; depListIndex++) {
const depList = dependencies[depListIndex];
if (depList.isBatch) {
const depVal = dependenciesIncludingSideEffects[depListIndex];
depList.entries.push(depVal.at(dataIndex));
depList._flags.push(depVal._flagsAt(dataIndex));
}
}
}
}
}
if (newSize === 0) {
// Everything is errors; we can skip execution
return forcedValues;
}
else if (needsTransform) {
const resultWithoutErrors = executeOrStream(newSize, step, dependencies, extra);
if ((0, utils_js_1.isPromiseLike)(resultWithoutErrors)) {
return resultWithoutErrors.then((r) => mergeErrorsBackIn(r, forcedValues, expectedSize));
}
else {
return mergeErrorsBackIn(resultWithoutErrors, forcedValues, expectedSize);
}
}
else {
if (dev_js_1.isDev) {
assert.ok(newSize === expectedSize, "GrafastInternalError<47fca4ab-069c-428f-8374-267479c7fd27>: Expected newSize to equal expectedSize");
}
return reallyExecuteStepWithoutFiltering(newSize, step, dependencies, extra);
}
}
/**
* This function MIGHT throw or reject, so be sure to handle that.
*/
function executeStep(step) {
// DELIBERATE SHADOWING!
const size = step._isUnary ? 1 : bucket.size;
try {
const meta = step.metaKey !== undefined ? metaByMetaKey[step.metaKey] : undefined;
const extra = {
stopTime,
meta,
eventEmitter,
stream: evaluateStream(bucket, step),
_bucket: bucket,
_requestContext: requestContext,
};
/** Only mutate this inside `addDependency` */
const _rawDependencies = [];
const _rawForbiddenFlags = [];
const _rawOnReject = [];
const dependencies = _rawDependencies;
let needsFiltering = false;
const defaultForbiddenFlags = (0, utils_js_1.sudo)(step).defaultForbiddenFlags;
const addDependency = ($dep, forbiddenFlags, onReject) => {
if (step._isUnary && !$dep._isUnary) {
throw new Error(`GrafastInternalError<58bc38e2-8722-4c19-ba38-fd01a020654b>: unary step ${step} cannot be made dependent on non-unary step ${$dep}!`);
}
const executionValue = store.get($dep.id);
if (executionValue === undefined) {
throw new Error(`GrafastInternalError<d9e9eb37-4251-4659-a545-4730826ecf0e>: ${$dep} data couldn't be found, but required by ${step} (with side effect ${step.implicitSideEffectStep})!`);
}
_rawDependencies.push(executionValue);
_rawForbiddenFlags.push(forbiddenFlags);
_rawOnReject.push(onReject);
if (!needsFiltering && (bucket.flagUnion & forbiddenFlags) !== 0) {
const flags = executionValue._getStateUnion();
needsFiltering = (flags & forbiddenFlags) !== 0;
}
};
const sstep = (0, utils_js_1.sudo)(step);
const depCount = sstep.dependencies.length;
if (depCount > 0) {
for (let i = 0, l = depCount; i < l; i++) {
const $dep = sstep.dependencies[i];
addDependency($dep, sstep.dependencyForbiddenFlags[i], sstep.dependencyOnReject[i]);
}
}
const $sideEffect = step.implicitSideEffectStep;
if ($sideEffect) {
if ($sideEffect._isUnary || !step._isUnary) {
addDependency($sideEffect, defaultForbiddenFlags, undefined);
}
}
if (dev_js_1.isDev &&
(step.layerPlan.reason.type === "polymorphic" ||
step.layerPlan.reason.type === "polymorphicPartition") &&
step.polymorphicPaths === null) {
throw new Error(`GrafastInternalError<c33328fe-6758-4699-8ac6-7be41ce58bfb>: a step without polymorphic paths cannot belong to a polymorphic bucket`);
}
needsFiltering ||= step._isSelectiveStep;
const result = needsFiltering
? reallyExecuteStepWithFiltering(step, dependencies, _rawForbiddenFlags, _rawOnReject, bucket.polymorphicPathList, extra)
: reallyExecuteStepWithoutFiltering(size, step, $sideEffect ? dependencies.slice(0, depCount) : dependencies, extra);
if ((0, utils_js_1.isPromiseLike)(result)) {
return result.then(null, (error) => {
// Don't need to do this here, it will be done where the
// ExecutionValue is created:
// bucket.hasNonZeroStatus = true;
return {
flags: (0, utils_js_1.arrayOfLength)(size, constants_js_1.FLAG_ERROR | constants_js_1.FLAG_STOPPED),
results: (0, utils_js_1.arrayOfLength)(size, error),
};
});
}
else {
return result;
}
}
catch (error) {
// Don't need to do this here, it will be done where the
// ExecutionValue is created:
// bucket.hasNonZeroStatus = true;
return {
flags: (0, utils_js_1.arrayOfLength)(size, constants_js_1.FLAG_ERROR | constants_js_1.FLAG_STOPPED),
results: (0, utils_js_1.arrayOfLength)(size, error),
};
}
}
function executeSamePhaseChildren() {
const result = completeBucketAndExecuteSamePhaseChildren(bucket, requestContext);
if (result != null) {
return result.then(() => {
// PERF: this seems like a bad idea! We're forcing the bucket to be
// retained in this closure? Why?
bucket.isComplete = true;
return;
});
}
else {
bucket.isComplete = true;
return;
}
}
}
function completeBucketAndExecuteSamePhaseChildren(bucket, requestContext) {
const { layerPlan, sharedState } = bucket;
const result = markLayerPlanAsDone(requestContext, sharedState, layerPlan, bucket);
bucket.sharedState.release(bucket);
return result;
}
function markLayerPlanAsDone(requestContext, sharedState, layerPlan, bucket) {
if (sharedState._doneBucketIds.has(layerPlan.id)) {
throw new Error(`${bucket} has already completed, it cannot complete again!`);
}
sharedState._doneBucketIds.add(layerPlan.id);
const childPromises = [];
// This promise should never reject
let mutationQueue = null;
/**
* Ensures that callback is only called once all other enqueued callbacks
* are called.
*/
const enqueue = (callback) => {
const result = mutationQueue ? mutationQueue.then(callback) : callback();
if ((0, utils_js_1.isPromiseLike)(result)) {
mutationQueue = result.then(noop, noop);
}
return result;
};
const { children: childLayerPlans } = layerPlan;
// PERF: create a JIT factory for this at planning time
loop: for (const childLayerPlan of childLayerPlans) {
switch (childLayerPlan.reason.type) {
case "nullableBoundary":
case "listItem":
case "polymorphic":
case "polymorphicPartition": {
const childBucket = bucket == null ? null : childLayerPlan.newBucket(bucket);
// Execute
const result = childBucket !== null
? executeBucket(childBucket, requestContext)
: markLayerPlanAsDone(requestContext, sharedState, childLayerPlan, null);
if ((0, utils_js_1.isPromiseLike)(result)) {
childPromises.push(result);
}
break;
}
case "mutationField": {
const childBucket = bucket == null ? null : childLayerPlan.newBucket(bucket);
// Enqueue for execution (mutations must run in order)
const promise = enqueue(() => childBucket !== null
? executeBucket(childBucket, requestContext)
: markLayerPlanAsDone(requestContext, sharedState, childLayerPlan, null));
if ((0, utils_js_1.isPromiseLike)(promise)) {
childPromises.push(promise);
}
break;
}
case "combined": {
// First, see if _all_ parent layer plans are ready
let allParentLayerPlansAreReady = true;
for (const lp of childLayerPlan.reason.parentLayerPlans) {
if (lp === layerPlan)
continue;
if (!sharedState._doneBucketIds.has(lp.id)) {
allParentLayerPlansAreReady = false;
break;
}
}
if (!allParentLayerPlansAreReady) {
// The last parent layer plan to complete will handle it.
// Make sure we're retained so it can use our data!
// Will be released inside the "combined" branch in childLayerPlan.newBucket
if (bucket != null) {