UNPKG

@graphql-tools/executor

Version:

Fork of GraphQL.js' execute function

583 lines (582 loc) • 18.5 kB
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; }, }; }