UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

962 lines (961 loc) 61 kB
"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) {