@graphql-tools/executor
Version:
Fork of GraphQL.js' execute function
583 lines (582 loc) • 18.5 kB
JavaScript
import { pathToArray } from '@graphql-tools/utils';
import { DisposableSymbols } from '@whatwg-node/disposablestack';
export function createIncrementalPublisherState() {
return {
nextId: 0,
ids: new Map(),
announced: new Set(),
};
}
export class StreamGroup {
path;
label;
parentContext;
hasIterator;
iteratorCompleted;
openCount;
constructor(opts) {
this.path = pathToArray(opts.path);
this.label = opts.label;
this.parentContext = opts.parentContext;
this.hasIterator = opts.hasIterator === true;
this.iteratorCompleted = false;
this.openCount = 0;
}
retain() {
this.openCount++;
}
release() {
this.openCount--;
}
isStillPending() {
return this.openCount > 0 || (this.hasIterator && !this.iteratorCompleted);
}
}
export function createStreamGroupIfV02(exeContext, opts) {
if (exeContext.incrementalSpec !== 'v0.2') {
return undefined;
}
return new StreamGroup(opts);
}
export function deleteAsyncPayload(exeContext, record) {
exeContext.subsequentPayloads.delete(record);
if (record.type === 'stream') {
record.streamGroup?.release();
}
}
export function pathStartsWith(path, prefix) {
if (prefix.length > path.length) {
return false;
}
for (let i = 0; i < prefix.length; i++) {
if (path[i] !== prefix[i]) {
return false;
}
}
return true;
}
export function isAtOrUnderNulledPath(path, nulledPaths) {
if (nulledPaths == null || nulledPaths.length === 0) {
return false;
}
for (const nulledPath of nulledPaths) {
if (pathStartsWith(path, nulledPath)) {
return true;
}
}
return false;
}
function isStrictlyUnderNulledPath(path, nulledPaths) {
for (const nulledPath of nulledPaths) {
if (path.length > nulledPath.length && pathStartsWith(path, nulledPath)) {
return true;
}
}
return false;
}
function isExactNulledPath(path, nulledPaths) {
for (const nulledPath of nulledPaths) {
if (path.length === nulledPath.length && pathStartsWith(path, nulledPath)) {
return true;
}
}
return false;
}
function isEmptyIncrementalData(data) {
if (data == null) {
return true;
}
return (typeof data === 'object' &&
!Array.isArray(data) &&
Object.keys(data).length === 0);
}
/**
* Drop keys that would write into a subtree already nulled by error bubbling.
* Ancestor deferred records can still deliver sibling fields (e.g. `someField`
* next to a nulled `b.c`).
*/
export function pruneUnmergeableIncrementalData(data, basePath, nulledPaths) {
if (nulledPaths == null || nulledPaths.length === 0 || data == null || typeof data !== 'object') {
return data;
}
if (Array.isArray(data)) {
return data.map((item, index) => {
const itemPath = [...basePath, index];
if (isAtOrUnderNulledPath(itemPath, nulledPaths)) {
return null;
}
return pruneUnmergeableIncrementalData(item, itemPath, nulledPaths);
});
}
const result = {};
for (const [key, value] of Object.entries(data)) {
const childPath = [...basePath, key];
if (isStrictlyUnderNulledPath(childPath, nulledPaths)) {
continue;
}
if (isExactNulledPath(childPath, nulledPaths)) {
// Keep a null delivered by this payload; drop a non-null write into an
// already-nulled object (Yaacov: unmergeable incremental).
if (value != null) {
continue;
}
result[key] = null;
continue;
}
result[key] = pruneUnmergeableIncrementalData(value, childPath, nulledPaths);
}
return result;
}
function isAnnounced(exeContext, record) {
const state = exeContext.publisher;
if (state == null) {
return false;
}
return state.announced.has(getPublishNode(record));
}
/**
* Cancel a subsequent record whose path is at or under a nulled position
* (graphql-js 17 `hasNulledPosition`). v0.2 records that were already
* announced stay in the set so the publisher can emit `completed` without
* incremental data.
*/
export function cancelAsyncPayload(exeContext, record) {
record.cancelled = true;
if (isStreamPayload(record) && record.iterator?.return) {
record.iterator.return().catch(() => {
// ignore error
});
}
if (!record.isCompleted) {
if (isStreamPayload(record)) {
record.addItems(null);
}
else {
record.addData(null);
}
}
if (exeContext.incrementalSpec === 'v0.2' && isAnnounced(exeContext, record)) {
return;
}
deleteAsyncPayload(exeContext, record);
}
export class DeferredFragmentRecord {
type;
errors;
label;
path;
promise;
data;
parentContext;
deliveryParent;
deferUsage;
isCompleted;
cancelled;
_exeContext;
_resolve;
constructor(opts) {
this.type = 'defer';
this.label = opts.label;
this.path = pathToArray(opts.path);
this.parentContext = opts.parentContext;
this.deliveryParent = opts.deliveryParent ?? opts.parentContext;
this.deferUsage = opts.deferUsage;
this.errors = [];
this._exeContext = opts.exeContext;
this._exeContext.subsequentPayloads.add(this);
this.isCompleted = false;
this.cancelled = false;
this.data = null;
this.promise = new Promise(resolve => {
this._resolve = MaybePromise => {
resolve(MaybePromise);
};
}).then(data => {
this.data = data;
this.isCompleted = true;
});
}
addData(data) {
const parentData = this.parentContext?.promise;
if (parentData) {
this._resolve?.(parentData.then(() => data));
return;
}
this._resolve?.(data);
}
}
export class StreamRecord {
type;
errors;
label;
path;
items;
promise;
parentContext;
iterator;
isCompletedIterator;
isCompleted;
cancelled;
streamGroup;
_exeContext;
_resolve;
constructor(opts) {
this.type = 'stream';
this.items = null;
this.label = opts.label;
this.path = pathToArray(opts.path);
this.parentContext = opts.parentContext;
this.iterator = opts.iterator;
this.streamGroup = opts.streamGroup;
this.errors = [];
this._exeContext = opts.exeContext;
this._exeContext.subsequentPayloads.add(this);
this.streamGroup?.retain();
this.isCompleted = false;
this.cancelled = false;
this.items = null;
this.promise = new Promise(resolve => {
this._resolve = MaybePromise => {
resolve(MaybePromise);
};
}).then(items => {
this.items = items;
this.isCompleted = true;
});
}
addItems(items) {
const parentData = this.parentContext?.promise;
if (parentData) {
this._resolve?.(parentData.then(() => items));
return;
}
this._resolve?.(items);
}
setIsCompletedIterator() {
this.isCompletedIterator = true;
if (this.streamGroup) {
this.streamGroup.iteratorCompleted = true;
}
}
}
export function isStreamPayload(asyncPayload) {
return asyncPayload.type === 'stream';
}
function ensureId(state, node) {
let id = state.ids.get(node);
if (id !== undefined) {
return id;
}
id = String(state.nextId++);
state.ids.set(node, id);
return id;
}
function getPublishNode(record) {
return record.type === 'stream' ? (record.streamGroup ?? record) : record;
}
function getPublishParent(record) {
if (record.type === 'stream') {
return record.streamGroup?.parentContext;
}
return record.deliveryParent ?? record.parentContext;
}
function isParentAnnouncedOrRoot(record, state) {
const parent = getPublishParent(record);
if (parent == null) {
return true;
}
return state.announced.has(getPublishNode(parent));
}
function toPendingResult(state, record) {
const node = getPublishNode(record);
const pending = {
id: ensureId(state, node),
path: record.type === 'stream' ? (record.streamGroup?.path ?? record.path) : record.path,
};
const label = record.type === 'stream' ? record.streamGroup?.label : record.label;
if (label !== undefined) {
pending.label = label;
}
return pending;
}
function announceIfNeeded(record, state, pending) {
if (record.cancelled) {
return;
}
const node = getPublishNode(record);
if (state.announced.has(node)) {
return;
}
if (!isParentAnnouncedOrRoot(record, state)) {
return;
}
pending.push(toPendingResult(state, record));
state.announced.add(node);
}
export function collectInitialPending(exeContext) {
const state = exeContext.publisher;
if (state == null) {
return [];
}
const pending = [];
for (const record of exeContext.subsequentPayloads) {
if (getPublishParent(record) != null) {
continue;
}
announceIfNeeded(record, state, pending);
}
return pending;
}
function getCompletedIncrementalResults(exeContext) {
const incrementalResults = [];
for (const asyncPayloadRecord of exeContext.subsequentPayloads) {
const incrementalResult = {};
if (!asyncPayloadRecord.isCompleted) {
continue;
}
deleteAsyncPayload(exeContext, asyncPayloadRecord);
if (asyncPayloadRecord.cancelled) {
continue;
}
if (isStreamPayload(asyncPayloadRecord)) {
const items = asyncPayloadRecord.items;
if (asyncPayloadRecord.isCompletedIterator) {
// async iterable resolver just finished but there may be pending payloads
continue;
}
incrementalResult.items = items;
}
else {
const data = pruneUnmergeableIncrementalData(asyncPayloadRecord.data, asyncPayloadRecord.path, exeContext.nulledPaths);
if (isEmptyIncrementalData(data) && asyncPayloadRecord.errors.length === 0) {
continue;
}
incrementalResult.data =
data ?? null;
}
incrementalResult.path = asyncPayloadRecord.path;
if (asyncPayloadRecord.label) {
incrementalResult.label = asyncPayloadRecord.label;
}
if (asyncPayloadRecord.errors.length > 0) {
incrementalResult.errors = asyncPayloadRecord.errors;
}
incrementalResults.push(incrementalResult);
}
return incrementalResults;
}
function buildSubsequentV02(exeContext, completedRecords) {
const state = exeContext.publisher;
if (state == null) {
return { hasNext: exeContext.subsequentPayloads.size > 0 };
}
const pending = [];
const incremental = [];
const completed = [];
let announcedCount;
do {
announcedCount = pending.length;
for (const record of completedRecords) {
announceIfNeeded(record, state, pending);
}
for (const record of exeContext.subsequentPayloads) {
announceIfNeeded(record, state, pending);
}
} while (pending.length !== announcedCount);
const streamBatches = new Map();
const completedGroups = new Set();
const iteratorDoneGroups = new Set();
for (const record of completedRecords) {
if (record.type === 'defer') {
if (!state.announced.has(record)) {
continue;
}
const id = ensureId(state, record);
if (record.cancelled) {
completed.push({ id });
continue;
}
const data = pruneUnmergeableIncrementalData(record.data, record.path, exeContext.nulledPaths);
if ((record.data == null && record.errors.length > 0) ||
(isEmptyIncrementalData(data) && record.errors.length > 0)) {
completed.push({ id, errors: record.errors });
}
else if (isEmptyIncrementalData(data) && record.errors.length === 0) {
completed.push({ id });
}
else {
const incrementalResult = {
id,
data: data ?? null,
};
if (record.errors.length > 0) {
incrementalResult.errors = record.errors;
}
incremental.push(incrementalResult);
completed.push({ id });
}
continue;
}
if (record.isCompletedIterator) {
if (record.streamGroup) {
iteratorDoneGroups.add(record.streamGroup);
}
continue;
}
const group = record.streamGroup;
if (group == null || !state.announced.has(group)) {
continue;
}
let batch = streamBatches.get(group);
if (batch == null) {
batch = { items: [], errors: [], failed: false };
streamBatches.set(group, batch);
}
if (record.items == null) {
batch.failed = true;
batch.errors.push(...record.errors);
}
else {
batch.items.push(...record.items);
if (record.errors.length > 0) {
batch.errors.push(...record.errors);
}
}
}
for (const [group, batch] of streamBatches) {
const id = ensureId(state, group);
if (batch.failed) {
const completedResult = { id };
if (batch.errors.length > 0) {
completedResult.errors = batch.errors;
}
completed.push(completedResult);
continue;
}
const incrementalResult = { id, items: batch.items };
if (batch.errors.length > 0) {
incrementalResult.errors = batch.errors;
}
incremental.push(incrementalResult);
if (!group.isStillPending()) {
completedGroups.add(group);
}
}
for (const group of iteratorDoneGroups) {
if (state.announced.has(group) && !group.isStillPending() && !streamBatches.has(group)) {
completedGroups.add(group);
}
}
for (const group of completedGroups) {
completed.push({ id: ensureId(state, group) });
}
let hasNext = exeContext.subsequentPayloads.size > 0;
if (!hasNext) {
for (const group of streamBatches.keys()) {
if (group.isStillPending()) {
hasNext = true;
break;
}
}
}
const result = { hasNext };
if (pending.length > 0) {
result.pending = pending;
}
if (incremental.length > 0) {
result.incremental = incremental;
}
if (completed.length > 0) {
result.completed = completed;
}
return result;
}
export function yieldSubsequentPayloads(exeContext) {
let isDone = false;
const isV02 = exeContext.incrementalSpec === 'v0.2';
async function next() {
if (isDone) {
return { value: undefined, done: true };
}
const subSequentPayloadPromises = Array.from(exeContext.subsequentPayloads).map(record => record.promise);
if (subSequentPayloadPromises.length === 0) {
isDone = true;
return { value: { hasNext: false }, done: false };
}
if (exeContext.signalPromise) {
await Promise.race([exeContext.signalPromise, ...subSequentPayloadPromises]);
}
else {
await Promise.race(subSequentPayloadPromises);
}
if (isDone) {
// a different call to next has exhausted all payloads
return { value: undefined, done: true };
}
if (isV02) {
const completedRecords = [];
for (const record of exeContext.subsequentPayloads) {
if (record.isCompleted) {
completedRecords.push(record);
deleteAsyncPayload(exeContext, record);
}
}
const result = buildSubsequentV02(exeContext, completedRecords);
const hasWork = result.hasNext ||
result.incremental != null ||
result.pending != null ||
result.completed != null;
if (!hasWork && result.hasNext) {
return next();
}
if (!result.incremental && !result.pending && !result.completed && result.hasNext) {
return next();
}
if (!result.hasNext) {
isDone = true;
}
return { value: result, done: false };
}
const incremental = getCompletedIncrementalResults(exeContext);
const hasNext = exeContext.subsequentPayloads.size > 0;
if (!incremental.length && hasNext) {
return next();
}
if (!hasNext) {
isDone = true;
}
return {
value: incremental.length ? { incremental, hasNext } : { hasNext },
done: false,
};
}
function returnStreamIterators() {
const promises = [];
exeContext.subsequentPayloads.forEach(asyncPayloadRecord => {
if (isStreamPayload(asyncPayloadRecord) && asyncPayloadRecord.iterator?.return) {
promises.push(asyncPayloadRecord.iterator.return());
}
});
return Promise.all(promises);
}
return {
[Symbol.asyncIterator]() {
return this;
},
next,
async return() {
await returnStreamIterators();
isDone = true;
return { value: undefined, done: true };
},
async throw(error) {
await returnStreamIterators();
isDone = true;
throw error;
},
async [DisposableSymbols.asyncDispose]() {
await returnStreamIterators();
isDone = true;
},
};
}