@graphql-tools/executor
Version:
Fork of GraphQL.js' execute function
330 lines (329 loc) • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildIncrementalResponse = void 0;
const utils_1 = require("@graphql-tools/utils");
const DeferredFragments_js_1 = require("./DeferredFragments.js");
const IncrementalGraph_js_1 = require("./IncrementalGraph.js");
const invariant_js_1 = require("./invariant.js");
const types_js_1 = require("./types.js");
function buildIncrementalResponse(context, result, errors, incrementalDataRecords) {
const incrementalPublisher = new IncrementalPublisher(context);
return incrementalPublisher.buildResponse(result, errors, incrementalDataRecords);
}
exports.buildIncrementalResponse = buildIncrementalResponse;
/**
* The IncrementalPublisherState Enum tracks the state of the IncrementalPublisher, which is initialized to
* "Started". When there are no more incremental results to publish, the state is set to "Completed". On the
* next call to next, clean-up is potentially performed and the state is set to "Finished".
*
* If the IncrementalPublisher is ended early, it may be advanced directly from "Started" to "Finished".
*/
var IncrementalPublisherState;
(function (IncrementalPublisherState) {
IncrementalPublisherState[IncrementalPublisherState["Started"] = 1] = "Started";
IncrementalPublisherState[IncrementalPublisherState["Completed"] = 2] = "Completed";
IncrementalPublisherState[IncrementalPublisherState["Finished"] = 3] = "Finished";
})(IncrementalPublisherState || (IncrementalPublisherState = {}));
/**
* This class is used to publish incremental results to the client, enabling semi-concurrent
* execution while preserving result order.
*
* @internal
*/
class IncrementalPublisher {
_context;
_nextId;
_incrementalGraph;
constructor(context) {
this._context = context;
this._nextId = 0;
let deferredFragmentFactory = context.deferredFragmentFactory;
if (deferredFragmentFactory === undefined) {
context.deferredFragmentFactory = deferredFragmentFactory = new DeferredFragments_js_1.DeferredFragmentFactory();
}
this._incrementalGraph = new IncrementalGraph_js_1.IncrementalGraph(deferredFragmentFactory);
}
buildResponse(data, errors, incrementalDataRecords) {
const newRootNodes = this._incrementalGraph.getNewRootNodes(incrementalDataRecords);
const initialResult = this._context
.useIncrementalNotifications
? errors === undefined
? { data, pending: this._toPendingResults(newRootNodes), hasNext: true }
: { errors, data, pending: this._toPendingResults(newRootNodes), hasNext: true }
: errors === undefined
? { data, hasNext: true }
: { errors, data, hasNext: true };
return {
initialResult,
subsequentResults: this._subscribe(),
};
}
_toPendingResults(newRootNodes) {
const pendingResults = [];
for (const node of newRootNodes) {
const id = String(this._getNextId());
node.id = id;
const pendingResult = {
id,
path: (0, utils_1.pathToArray)(node.path),
};
if (node.label !== undefined) {
pendingResult.label = node.label;
}
pendingResults.push(pendingResult);
}
return pendingResults;
}
_getNextId() {
return String(this._nextId++);
}
_subscribe() {
let incrementalPublisherState = IncrementalPublisherState.Started;
const _finish = async () => {
incrementalPublisherState = IncrementalPublisherState.Finished;
this._incrementalGraph.abort();
await this._returnAsyncIterators();
};
this._context.signal?.addEventListener('abort', () => {
this._incrementalGraph.abort();
});
const _next = async () => {
switch (incrementalPublisherState) {
case IncrementalPublisherState.Finished: {
return { value: undefined, done: true };
}
case IncrementalPublisherState.Completed: {
await _finish();
return { value: undefined, done: true };
}
case IncrementalPublisherState.Started: {
// continue
}
}
const context = {
pending: [],
incremental: [],
completed: [],
};
let batch = this._incrementalGraph.currentCompletedBatch();
do {
for (const completedResult of batch) {
this._handleCompletedIncrementalData(completedResult, context);
}
const { incremental, completed } = context;
if (incremental.length > 0 || completed.length > 0) {
const hasNext = this._incrementalGraph.hasNext();
if (!hasNext) {
incrementalPublisherState = IncrementalPublisherState.Completed;
}
const subsequentIncrementalExecutionResult = {
hasNext,
};
const pending = context.pending;
if (pending.length > 0) {
subsequentIncrementalExecutionResult.pending = pending;
}
if (incremental.length > 0) {
subsequentIncrementalExecutionResult.incremental = incremental;
}
if (completed.length > 0) {
subsequentIncrementalExecutionResult.completed = completed;
}
return { value: subsequentIncrementalExecutionResult, done: false };
}
batch = await this._incrementalGraph.nextCompletedBatch();
} while (batch !== undefined);
if (this._context.signal?.aborted) {
throw this._context.signal.reason;
}
return { value: undefined, done: true };
};
const _return = async () => {
await _finish();
return { value: undefined, done: true };
};
const _throw = async (error) => {
await _finish();
return Promise.reject(error);
};
return {
[Symbol.asyncIterator]() {
return this;
},
next: _next,
return: _return,
throw: _throw,
};
}
_handleCompletedIncrementalData(completedIncrementalData, context) {
if ((0, types_js_1.isCompletedExecutionGroup)(completedIncrementalData)) {
this._handleCompletedExecutionGroup(completedIncrementalData, context);
}
else {
this._handleCompletedStreamItems(completedIncrementalData, context);
}
}
_handleCompletedExecutionGroup(completedExecutionGroup, context) {
const { deferUsages, path } = completedExecutionGroup.pendingExecutionGroup;
if ((0, types_js_1.isFailedExecutionGroup)(completedExecutionGroup)) {
for (const deferUsage of deferUsages) {
const deferredFragmentRecord = this._incrementalGraph.removeDeferredFragment(deferUsage, path);
if (deferredFragmentRecord === undefined) {
// This can occur if multiple deferred grouped field sets error for a fragment.
continue;
}
if (this._context.useIncrementalNotifications) {
const id = deferredFragmentRecord.id;
(0, invariant_js_1.invariant)(id !== undefined);
context.completed.push({
id,
errors: completedExecutionGroup.errors,
});
}
else {
const incrementalEntry = {
errors: completedExecutionGroup.errors,
data: null,
};
const { path, label } = deferredFragmentRecord;
incrementalEntry.path = (0, utils_1.pathToArray)(path);
if (label !== undefined) {
incrementalEntry.label = label;
}
context.incremental.push(incrementalEntry);
}
}
return;
}
this._incrementalGraph.addCompletedSuccessfulExecutionGroup(completedExecutionGroup);
for (const deferUsage of deferUsages) {
const completion = this._incrementalGraph.completeDeferredFragment(deferUsage, path);
if (completion === undefined) {
continue;
}
const incremental = context.incremental;
const { deferredFragmentRecord, newRootNodes, successfulExecutionGroups } = completion;
if (this._context.useIncrementalNotifications) {
context.pending.push(...this._toPendingResults(newRootNodes));
for (const successfulExecutionGroup of successfulExecutionGroups) {
const { deferUsages: resultDeferUsages, path: resultPath } = successfulExecutionGroup.pendingExecutionGroup;
const bestDeferredFragmentRecord = this._incrementalGraph.getDeepestDeferredFragmentAtRoot(deferUsage, resultDeferUsages, resultPath);
const bestId = bestDeferredFragmentRecord.id;
(0, invariant_js_1.invariant)(bestId !== undefined);
const incrementalEntry = {
...successfulExecutionGroup.result,
id: bestId,
};
const subPath = (0, utils_1.pathToArray)(resultPath).slice((0, utils_1.pathToArray)(bestDeferredFragmentRecord.path).length);
if (subPath.length > 0) {
incrementalEntry.subPath = subPath;
}
incremental.push(incrementalEntry);
}
const id = deferredFragmentRecord.id;
(0, invariant_js_1.invariant)(id !== undefined);
context.completed.push({ id });
}
else {
for (const successfulExecutionGroup of successfulExecutionGroups) {
const incrementalEntry = {
...successfulExecutionGroup.result,
};
const { path, label } = deferredFragmentRecord;
incrementalEntry.path = (0, utils_1.pathToArray)(path);
if (label !== undefined) {
incrementalEntry.label = label;
}
incremental.push(incrementalEntry);
}
}
}
}
_handleCompletedStreamItems(streamItemsResult, context) {
const streamRecord = streamItemsResult.streamRecord;
if (streamItemsResult.errors !== undefined) {
if (this._context.useIncrementalNotifications) {
const id = streamRecord.id;
(0, invariant_js_1.invariant)(id !== undefined);
context.completed.push({
id,
errors: streamItemsResult.errors,
});
}
else {
const incrementalEntry = {
errors: streamItemsResult.errors,
items: null,
};
const { path, label, index } = streamRecord;
incrementalEntry.path = (0, utils_1.pathToArray)((0, utils_1.addPath)(path, index, undefined));
if (label !== undefined) {
incrementalEntry.label = label;
}
context.incremental.push(incrementalEntry);
}
this._incrementalGraph.removeStream(streamRecord);
if ((0, types_js_1.isCancellableStreamRecord)(streamRecord)) {
(0, invariant_js_1.invariant)(this._context.cancellableStreams !== undefined);
this._context.cancellableStreams.delete(streamRecord);
streamRecord.earlyReturn().catch(() => {
/* c8 ignore next 1 */
// ignore error
});
}
}
else if (streamItemsResult.result === undefined) {
if (this._context.useIncrementalNotifications) {
const id = streamRecord.id;
(0, invariant_js_1.invariant)(id !== undefined);
context.completed.push({ id });
}
this._incrementalGraph.removeStream(streamRecord);
if ((0, types_js_1.isCancellableStreamRecord)(streamRecord)) {
(0, invariant_js_1.invariant)(this._context.cancellableStreams !== undefined);
this._context.cancellableStreams.delete(streamRecord);
}
}
else {
const bareResult = streamItemsResult.result;
const incrementalEntry = {
...bareResult,
};
if (this._context.useIncrementalNotifications) {
const id = streamRecord.id;
(0, invariant_js_1.invariant)(id !== undefined);
incrementalEntry.id = id;
}
else {
const { path, label, index } = streamRecord;
incrementalEntry.path = (0, utils_1.pathToArray)((0, utils_1.addPath)(path, index, undefined));
streamRecord.index += bareResult.items.length;
if (label !== undefined) {
incrementalEntry.label = label;
}
}
context.incremental.push(incrementalEntry);
const incrementalDataRecords = streamItemsResult.incrementalDataRecords;
if (incrementalDataRecords !== undefined) {
const newPending = this._incrementalGraph.getNewRootNodes(incrementalDataRecords);
if (this._context.useIncrementalNotifications) {
context.pending.push(...this._toPendingResults(newPending));
}
}
}
}
async _returnAsyncIterators() {
await this._incrementalGraph.abort();
const cancellableStreams = this._context.cancellableStreams;
if (cancellableStreams === undefined) {
return;
}
const promises = [];
for (const streamRecord of cancellableStreams) {
if (streamRecord.earlyReturn !== undefined) {
promises.push(streamRecord.earlyReturn());
}
}
await Promise.all(promises);
}
}