diff --git a/src/execution/IncrementalGraph.ts b/src/execution/IncrementalGraph.ts index cc31c21207..36f1898931 100644 --- a/src/execution/IncrementalGraph.ts +++ b/src/execution/IncrementalGraph.ts @@ -1,12 +1,14 @@ import { BoxedPromiseOrValue } from '../jsutils/BoxedPromiseOrValue.js'; import { invariant } from '../jsutils/invariant.js'; import { isPromise } from '../jsutils/isPromise.js'; +import type { Path } from '../jsutils/Path.js'; +import { pathAtFieldDepth } from '../jsutils/Path.js'; import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js'; import type { GraphQLError } from '../error/GraphQLError.js'; +import type { DeferUsage } from './collectFields.js'; import type { - DeferredFragmentRecord, DeferredGroupedFieldSetRecord, IncrementalDataRecord, IncrementalDataRecordResult, @@ -16,22 +18,69 @@ import type { SubsequentResultRecord, } from './types.js'; import { + DeferredFragmentRecord, isDeferredFragmentRecord, isDeferredGroupedFieldSetRecord, } from './types.js'; +/** + * @internal + */ +class DeferredFragmentFactory { + private _rootDeferredFragments = new Map< + DeferUsage, + DeferredFragmentRecord + >(); + + get(deferUsage: DeferUsage, path: Path | undefined): DeferredFragmentRecord { + const deferUsagePath = pathAtFieldDepth(path, deferUsage.fieldDepth); + let deferredFragmentRecords: + | Map + | undefined; + if (deferUsagePath === undefined) { + deferredFragmentRecords = this._rootDeferredFragments; + } else { + deferredFragmentRecords = ( + deferUsagePath as unknown as { + deferredFragmentRecords: Map; + } + ).deferredFragmentRecords; + if (deferredFragmentRecords === undefined) { + deferredFragmentRecords = new Map(); + ( + deferUsagePath as unknown as { + deferredFragmentRecords: Map; + } + ).deferredFragmentRecords = deferredFragmentRecords; + } + } + let deferredFragmentRecord = deferredFragmentRecords.get(deferUsage); + if (deferredFragmentRecord === undefined) { + const { label, parentDeferUsage } = deferUsage; + deferredFragmentRecord = new DeferredFragmentRecord( + deferUsagePath, + label, + parentDeferUsage, + ); + deferredFragmentRecords.set(deferUsage, deferredFragmentRecord); + } + return deferredFragmentRecord; + } +} + /** * @internal */ export class IncrementalGraph { private _rootNodes: Set; - + private _deferredFragmentFactory: DeferredFragmentFactory; private _completedQueue: Array; private _nextQueue: Array< (iterable: Iterable | undefined) => void >; constructor() { + this._deferredFragmentFactory = new DeferredFragmentFactory(); this._rootNodes = new Set(); this._completedQueue = []; this._nextQueue = []; @@ -52,8 +101,15 @@ export class IncrementalGraph { addCompletedReconcilableDeferredGroupedFieldSet( reconcilableResult: ReconcilableDeferredGroupedFieldSetResult, ): void { - for (const deferredFragmentRecord of reconcilableResult - .deferredGroupedFieldSetRecord.deferredFragmentRecords) { + const { deferUsages, path } = + reconcilableResult.deferredGroupedFieldSetRecord; + const deferredFragmentRecords: Array = []; + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); + deferredFragmentRecords.push(deferredFragmentRecord); deferredFragmentRecord.deferredGroupedFieldSetRecords.delete( reconcilableResult.deferredGroupedFieldSetRecord, ); @@ -64,12 +120,31 @@ export class IncrementalGraph { if (incrementalDataRecords !== undefined) { this._addIncrementalDataRecords( incrementalDataRecords, - reconcilableResult.deferredGroupedFieldSetRecord - .deferredFragmentRecords, + deferredFragmentRecords, ); } } + getDeepestDeferredFragmentRecord( + initialDeferUsage: DeferUsage, + deferUsages: ReadonlySet, + path: Path | undefined, + ): DeferredFragmentRecord { + let bestDeferUsage = initialDeferUsage; + let maxFieldDepth = initialDeferUsage.fieldDepth; + for (const deferUsage of deferUsages) { + if (deferUsage === initialDeferUsage) { + continue; + } + const fieldDepth = deferUsage.fieldDepth; + if (fieldDepth > maxFieldDepth) { + maxFieldDepth = fieldDepth; + bestDeferUsage = deferUsage; + } + } + return this._deferredFragmentFactory.get(bestDeferUsage, path); + } + *currentCompletedBatch(): Generator { let completed; while ((completed = this._completedQueue.shift()) !== undefined) { @@ -102,12 +177,20 @@ export class IncrementalGraph { return this._rootNodes.size > 0; } - completeDeferredFragment(deferredFragmentRecord: DeferredFragmentRecord): + completeDeferredFragment( + deferUsage: DeferUsage, + path: Path | undefined, + ): | { + deferredFragmentRecord: DeferredFragmentRecord; newRootNodes: ReadonlyArray; reconcilableResults: ReadonlyArray; } | undefined { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); if ( !this._rootNodes.has(deferredFragmentRecord) || deferredFragmentRecord.deferredGroupedFieldSetRecords.size > 0 @@ -119,8 +202,13 @@ export class IncrementalGraph { ); this._removeRootNode(deferredFragmentRecord); for (const reconcilableResult of reconcilableResults) { - for (const otherDeferredFragmentRecord of reconcilableResult - .deferredGroupedFieldSetRecord.deferredFragmentRecords) { + const { deferUsages, path: resultPath } = + reconcilableResult.deferredGroupedFieldSetRecord; + for (const otherDeferUsage of deferUsages) { + const otherDeferredFragmentRecord = this._deferredFragmentFactory.get( + otherDeferUsage, + resultPath, + ); otherDeferredFragmentRecord.reconcilableResults.delete( reconcilableResult, ); @@ -129,17 +217,22 @@ export class IncrementalGraph { const newRootNodes = this._promoteNonEmptyToRoot( deferredFragmentRecord.children, ); - return { newRootNodes, reconcilableResults }; + return { deferredFragmentRecord, newRootNodes, reconcilableResults }; } removeDeferredFragment( - deferredFragmentRecord: DeferredFragmentRecord, - ): boolean { + deferUsage: DeferUsage, + path: Path | undefined, + ): DeferredFragmentRecord | undefined { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); if (!this._rootNodes.has(deferredFragmentRecord)) { - return false; + return; } this._removeRootNode(deferredFragmentRecord); - return true; + return deferredFragmentRecord; } removeStream(streamRecord: StreamRecord): void { @@ -159,7 +252,12 @@ export class IncrementalGraph { ): void { for (const incrementalDataRecord of incrementalDataRecords) { if (isDeferredGroupedFieldSetRecord(incrementalDataRecord)) { - for (const deferredFragmentRecord of incrementalDataRecord.deferredFragmentRecords) { + const { deferUsages, path } = incrementalDataRecord; + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); this._addDeferredFragment( deferredFragmentRecord, initialResultChildren, @@ -216,9 +314,17 @@ export class IncrementalGraph { private _completesRootNode( deferredGroupedFieldSetRecord: DeferredGroupedFieldSetRecord, ): boolean { - return deferredGroupedFieldSetRecord.deferredFragmentRecords.some( - (deferredFragmentRecord) => this._rootNodes.has(deferredFragmentRecord), - ); + const { deferUsages, path } = deferredGroupedFieldSetRecord; + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); + if (this._rootNodes.has(deferredFragmentRecord)) { + return true; + } + } + return false; } private _addDeferredFragment( @@ -228,12 +334,16 @@ export class IncrementalGraph { if (this._rootNodes.has(deferredFragmentRecord)) { return; } - const parent = deferredFragmentRecord.parent; - if (parent === undefined) { + const parentDeferUsage = deferredFragmentRecord.parentDeferUsage; + if (parentDeferUsage === undefined) { invariant(initialResultChildren !== undefined); initialResultChildren.add(deferredFragmentRecord); return; } + const parent = this._deferredFragmentFactory.get( + parentDeferUsage, + deferredFragmentRecord.path, + ); parent.children.add(deferredFragmentRecord); this._addDeferredFragment(parent, initialResultChildren); } diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index dd27033ed8..d49c07426a 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -8,7 +8,6 @@ import { IncrementalGraph } from './IncrementalGraph.js'; import type { CancellableStreamRecord, CompletedResult, - DeferredFragmentRecord, DeferredGroupedFieldSetResult, ExperimentalIncrementalExecutionResults, IncrementalDataRecord, @@ -221,20 +220,21 @@ class IncrementalPublisher { deferredGroupedFieldSetResult: DeferredGroupedFieldSetResult, context: SubsequentIncrementalExecutionResultContext, ): void { + const { deferUsages, path } = + deferredGroupedFieldSetResult.deferredGroupedFieldSetRecord; if ( isNonReconcilableDeferredGroupedFieldSetResult( deferredGroupedFieldSetResult, ) ) { - for (const deferredFragmentRecord of deferredGroupedFieldSetResult - .deferredGroupedFieldSetRecord.deferredFragmentRecords) { - const id = deferredFragmentRecord.id; - if ( - !this._incrementalGraph.removeDeferredFragment(deferredFragmentRecord) - ) { + 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; } + const id = deferredFragmentRecord.id; invariant(id !== undefined); context.completed.push({ id, @@ -248,34 +248,43 @@ class IncrementalPublisher { deferredGroupedFieldSetResult, ); - for (const deferredFragmentRecord of deferredGroupedFieldSetResult - .deferredGroupedFieldSetRecord.deferredFragmentRecords) { + for (const deferUsage of deferUsages) { const completion = this._incrementalGraph.completeDeferredFragment( - deferredFragmentRecord, + deferUsage, + path, ); if (completion === undefined) { continue; } - const id = deferredFragmentRecord.id; - invariant(id !== undefined); - const incremental = context.incremental; - const { newRootNodes, reconcilableResults } = completion; + const { deferredFragmentRecord, newRootNodes, reconcilableResults } = + completion; context.pending.push(...this._toPendingResults(newRootNodes)); + const incremental = context.incremental; for (const reconcilableResult of reconcilableResults) { - const { bestId, subPath } = this._getBestIdAndSubPath( - id, - deferredFragmentRecord, - reconcilableResult, - ); + const { deferUsages: resultDeferUsages, path: resultPath } = + reconcilableResult.deferredGroupedFieldSetRecord; + const bestDeferredFragmentRecord = + this._incrementalGraph.getDeepestDeferredFragmentRecord( + deferUsage, + resultDeferUsages, + resultPath, + ); + const bestId = bestDeferredFragmentRecord.id; + invariant(bestId !== undefined); const incrementalEntry: IncrementalDeferResult = { ...reconcilableResult.result, id: bestId, }; - if (subPath !== undefined) { + const subPath = pathToArray(resultPath).slice( + pathToArray(bestDeferredFragmentRecord.path).length, + ); + if (subPath.length > 0) { incrementalEntry.subPath = subPath; } incremental.push(incrementalEntry); } + const id = deferredFragmentRecord.id; + invariant(id !== undefined); context.completed.push({ id }); } } @@ -326,39 +335,6 @@ class IncrementalPublisher { } } - private _getBestIdAndSubPath( - initialId: string, - initialDeferredFragmentRecord: DeferredFragmentRecord, - deferredGroupedFieldSetResult: DeferredGroupedFieldSetResult, - ): { bestId: string; subPath: ReadonlyArray | undefined } { - let maxLength = pathToArray(initialDeferredFragmentRecord.path).length; - let bestId = initialId; - - for (const deferredFragmentRecord of deferredGroupedFieldSetResult - .deferredGroupedFieldSetRecord.deferredFragmentRecords) { - if (deferredFragmentRecord === initialDeferredFragmentRecord) { - continue; - } - const id = deferredFragmentRecord.id; - // TODO: add test case for when an fragment has not been released, but might be processed for the shortest path. - /* c8 ignore next 3 */ - if (id === undefined) { - continue; - } - const fragmentPath = pathToArray(deferredFragmentRecord.path); - const length = fragmentPath.length; - if (length > maxLength) { - maxLength = length; - bestId = id; - } - } - const subPath = deferredGroupedFieldSetResult.path.slice(maxLength); - return { - bestId, - subPath: subPath.length > 0 ? subPath : undefined, - }; - } - private async _returnAsyncIterators(): Promise { const cancellableStreams = this._context.cancellableStreams; if (cancellableStreams === undefined) { diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index de33f8c91b..97ae573241 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -239,7 +239,7 @@ describe('Execute: Handles basic execution tasks', () => { const field = operation.selectionSet.selections[0]; expect(resolvedInfo).to.deep.include({ fieldNodes: [field], - path: { prev: undefined, key: 'result', typename: 'Test' }, + path: { prev: undefined, key: 'result', typename: 'Test', fieldDepth: 1 }, variableValues: { var: 'abc' }, }); }); @@ -291,12 +291,15 @@ describe('Execute: Handles basic execution tasks', () => { expect(path).to.deep.equal({ key: 'l2', typename: 'SomeObject', + fieldDepth: 2, prev: { key: 0, typename: undefined, + fieldDepth: 1, prev: { key: 'l1', typename: 'SomeQuery', + fieldDepth: 1, prev: undefined, }, }, diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index d411ff3f77..4171cfbfac 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -28,17 +28,21 @@ import { getDirectiveValues } from './values.js'; export interface DeferUsage { label: string | undefined; + fieldDepth: number; parentDeferUsage: DeferUsage | undefined; } export interface FieldDetails { node: FieldNode; + fieldDepth: number; deferUsage: DeferUsage | undefined; } export type FieldGroup = ReadonlyArray; -export type GroupedFieldSet = ReadonlyMap; +export type GroupedFieldSet = ReadonlyMap & { + encounteredDefer?: boolean; +}; interface CollectFieldsContext { schema: GraphQLSchema; @@ -64,12 +68,8 @@ export function collectFields( variableValues: { [variable: string]: unknown }, runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, -): { - groupedFieldSet: GroupedFieldSet; - newDeferUsages: ReadonlyArray; -} { +): GroupedFieldSet { const groupedFieldSet = new AccumulatorMap(); - const newDeferUsages: Array = []; const context: CollectFieldsContext = { schema, fragments, @@ -79,13 +79,8 @@ export function collectFields( visitedFragmentNames: new Set(), }; - collectFieldsImpl( - context, - operation.selectionSet, - groupedFieldSet, - newDeferUsages, - ); - return { groupedFieldSet, newDeferUsages }; + collectFieldsImpl(context, operation.selectionSet, groupedFieldSet); + return groupedFieldSet; } /** @@ -106,10 +101,7 @@ export function collectSubfields( operation: OperationDefinitionNode, returnType: GraphQLObjectType, fieldGroup: FieldGroup, -): { - groupedFieldSet: GroupedFieldSet; - newDeferUsages: ReadonlyArray; -} { +): GroupedFieldSet { const context: CollectFieldsContext = { schema, fragments, @@ -119,32 +111,30 @@ export function collectSubfields( visitedFragmentNames: new Set(), }; const subGroupedFieldSet = new AccumulatorMap(); - const newDeferUsages: Array = []; for (const fieldDetail of fieldGroup) { - const node = fieldDetail.node; + const { node, fieldDepth, deferUsage } = fieldDetail; if (node.selectionSet) { collectFieldsImpl( context, node.selectionSet, subGroupedFieldSet, - newDeferUsages, - fieldDetail.deferUsage, + fieldDepth + 1, + deferUsage, ); } } - return { - groupedFieldSet: subGroupedFieldSet, - newDeferUsages, - }; + return subGroupedFieldSet; } function collectFieldsImpl( context: CollectFieldsContext, selectionSet: SelectionSetNode, - groupedFieldSet: AccumulatorMap, - newDeferUsages: Array, + groupedFieldSet: AccumulatorMap & { + encounteredDefer?: boolean; + }, + fieldDepth = 0, deferUsage?: DeferUsage, ): void { const { @@ -164,6 +154,7 @@ function collectFieldsImpl( } groupedFieldSet.add(getFieldEntryKey(selection), { node: selection, + fieldDepth, deferUsage, }); break; @@ -180,6 +171,7 @@ function collectFieldsImpl( operation, variableValues, selection, + fieldDepth, deferUsage, ); @@ -188,16 +180,16 @@ function collectFieldsImpl( context, selection.selectionSet, groupedFieldSet, - newDeferUsages, + fieldDepth, deferUsage, ); } else { - newDeferUsages.push(newDeferUsage); + groupedFieldSet.encounteredDefer = true; collectFieldsImpl( context, selection.selectionSet, groupedFieldSet, - newDeferUsages, + fieldDepth, newDeferUsage, ); } @@ -211,6 +203,7 @@ function collectFieldsImpl( operation, variableValues, selection, + fieldDepth, deferUsage, ); @@ -235,16 +228,16 @@ function collectFieldsImpl( context, fragment.selectionSet, groupedFieldSet, - newDeferUsages, + fieldDepth, deferUsage, ); } else { - newDeferUsages.push(newDeferUsage); + groupedFieldSet.encounteredDefer = true; collectFieldsImpl( context, fragment.selectionSet, groupedFieldSet, - newDeferUsages, + fieldDepth, newDeferUsage, ); } @@ -263,6 +256,7 @@ function getDeferUsage( operation: OperationDefinitionNode, variableValues: { [variable: string]: unknown }, node: FragmentSpreadNode | InlineFragmentNode, + fieldDepth: number, parentDeferUsage: DeferUsage | undefined, ): DeferUsage | undefined { const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues); @@ -283,6 +277,7 @@ function getDeferUsage( return { label: typeof defer.label === 'string' ? defer.label : undefined, parentDeferUsage, + fieldDepth, }; } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 68a901f4b7..adc12e936f 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -50,11 +50,7 @@ import { assertValidSchema } from '../type/validate.js'; import type { DeferUsageSet, FieldPlan } from './buildFieldPlan.js'; import { buildFieldPlan } from './buildFieldPlan.js'; -import type { - DeferUsage, - FieldGroup, - GroupedFieldSet, -} from './collectFields.js'; +import type { FieldGroup, GroupedFieldSet } from './collectFields.js'; import { collectFields, collectSubfields as _collectSubfields, @@ -72,7 +68,6 @@ import type { StreamItemResult, StreamRecord, } from './types.js'; -import { DeferredFragmentRecord } from './types.js'; import { getArgumentValues, getDirectiveValues, @@ -142,6 +137,7 @@ export interface ExecutionContext { subscribeFieldResolver: GraphQLFieldResolver; enableEarlyExecution: boolean; errors: Array | undefined; + encounteredDefer: boolean; cancellableStreams: Set | undefined; } @@ -274,32 +270,32 @@ function executeOperation( ); } - const collectedFields = collectFields( + const originalGroupedFieldSet = collectFields( schema, fragments, variableValues, rootType, operation, ); - let groupedFieldSet = collectedFields.groupedFieldSet; - const newDeferUsages = collectedFields.newDeferUsages; let graphqlWrappedResult: PromiseOrValue< GraphQLWrappedResult> >; - if (newDeferUsages.length === 0) { + if ( + !exeContext.encounteredDefer && + originalGroupedFieldSet.encounteredDefer !== true + ) { graphqlWrappedResult = executeRootGroupedFieldSet( exeContext, operation.operation, rootType, rootValue, - groupedFieldSet, - undefined, + originalGroupedFieldSet, ); } else { - const fieldPLan = buildFieldPlan(groupedFieldSet); - groupedFieldSet = fieldPLan.groupedFieldSet; - const newGroupedFieldSets = fieldPLan.newGroupedFieldSets; - const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); + exeContext.encounteredDefer = true; + const { groupedFieldSet, newGroupedFieldSets } = buildFieldPlan( + originalGroupedFieldSet, + ); graphqlWrappedResult = executeRootGroupedFieldSet( exeContext, @@ -307,7 +303,6 @@ function executeOperation( rootType, rootValue, groupedFieldSet, - newDeferMap, ); if (newGroupedFieldSets.size > 0) { @@ -319,7 +314,6 @@ function executeOperation( undefined, undefined, newGroupedFieldSets, - newDeferMap, ); graphqlWrappedResult = withNewDeferredGroupedFieldSets( @@ -505,6 +499,7 @@ export function buildExecutionContext( subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, enableEarlyExecution: enableEarlyExecution === true, errors: undefined, + encounteredDefer: false, cancellableStreams: undefined, }; } @@ -526,7 +521,6 @@ function executeRootGroupedFieldSet( rootType: GraphQLObjectType, rootValue: unknown, groupedFieldSet: GroupedFieldSet, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { switch (operation) { case OperationTypeNode.QUERY: @@ -537,7 +531,6 @@ function executeRootGroupedFieldSet( undefined, groupedFieldSet, undefined, - deferMap, ); case OperationTypeNode.MUTATION: return executeFieldsSerially( @@ -547,7 +540,6 @@ function executeRootGroupedFieldSet( undefined, groupedFieldSet, undefined, - deferMap, ); case OperationTypeNode.SUBSCRIPTION: // TODO: deprecate `subscribe` and move all logic here @@ -559,7 +551,6 @@ function executeRootGroupedFieldSet( undefined, groupedFieldSet, undefined, - deferMap, ); } } @@ -575,7 +566,6 @@ function executeFieldsSerially( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { return promiseReduce( groupedFieldSet, @@ -588,7 +578,6 @@ function executeFieldsSerially( fieldGroup, fieldPath, incrementalContext, - deferMap, ); if (result === undefined) { return graphqlWrappedResult; @@ -619,7 +608,6 @@ function executeFields( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const results = Object.create(null); const graphqlWrappedResult: GraphQLWrappedResult> = [ @@ -638,7 +626,6 @@ function executeFields( fieldGroup, fieldPath, incrementalContext, - deferMap, ); if (result !== undefined) { @@ -697,7 +684,6 @@ function executeField( fieldGroup: FieldGroup, path: Path, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { const fieldName = fieldGroup[0].node.name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); @@ -743,7 +729,6 @@ function executeField( path, result, incrementalContext, - deferMap, ); } @@ -755,7 +740,6 @@ function executeField( path, result, incrementalContext, - deferMap, ); if (isPromise(completed)) { @@ -870,7 +854,6 @@ function completeValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue> { // If result is an Error, throw a located error. if (result instanceof Error) { @@ -888,7 +871,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); if ((completed as GraphQLWrappedResult)[0] === null) { throw new Error( @@ -913,7 +895,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); } @@ -934,7 +915,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); } @@ -948,7 +928,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); } /* c8 ignore next 6 */ @@ -967,7 +946,6 @@ async function completePromisedValue( path: Path, result: Promise, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): Promise> { try { const resolved = await result; @@ -979,7 +957,6 @@ async function completePromisedValue( path, resolved, incrementalContext, - deferMap, ); if (isPromise(completed)) { @@ -1057,6 +1034,7 @@ function getStreamUsage( const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({ node: fieldDetails.node, + fieldDepth: 0, deferUsage: undefined, })); @@ -1084,7 +1062,6 @@ async function completeAsyncIteratorValue( path: Path, asyncIterator: AsyncIterator, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): Promise>> { let containsPromise = false; const completedResults: Array = []; @@ -1165,7 +1142,6 @@ async function completeAsyncIteratorValue( info, itemPath, incrementalContext, - deferMap, ), ); containsPromise = true; @@ -1181,7 +1157,6 @@ async function completeAsyncIteratorValue( info, itemPath, incrementalContext, - deferMap, ) // TODO: add tests for stream backed by asyncIterator that completes to a promise /* c8 ignore start */ @@ -1221,7 +1196,6 @@ function completeListValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const itemType = returnType.ofType; @@ -1236,7 +1210,6 @@ function completeListValue( path, asyncIterator, incrementalContext, - deferMap, ); } @@ -1254,7 +1227,6 @@ function completeListValue( path, result, incrementalContext, - deferMap, ); } @@ -1266,7 +1238,6 @@ function completeIterableValue( path: Path, items: Iterable, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. @@ -1318,7 +1289,6 @@ function completeIterableValue( info, itemPath, incrementalContext, - deferMap, ), ); containsPromise = true; @@ -1333,7 +1303,6 @@ function completeIterableValue( info, itemPath, incrementalContext, - deferMap, ) ) { containsPromise = true; @@ -1366,7 +1335,6 @@ function completeListItemValue( info: GraphQLResolveInfo, itemPath: Path, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): boolean { try { const completedItem = completeValue( @@ -1377,7 +1345,6 @@ function completeListItemValue( itemPath, item, incrementalContext, - deferMap, ); if (isPromise(completedItem)) { @@ -1430,7 +1397,6 @@ async function completePromisedListItemValue( info: GraphQLResolveInfo, itemPath: Path, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): Promise { try { const resolved = await item; @@ -1442,7 +1408,6 @@ async function completePromisedListItemValue( itemPath, resolved, incrementalContext, - deferMap, ); if (isPromise(completed)) { completed = await completed; @@ -1492,7 +1457,6 @@ function completeAbstractValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; const contextValue = exeContext.contextValue; @@ -1515,7 +1479,6 @@ function completeAbstractValue( path, result, incrementalContext, - deferMap, ), ); } @@ -1535,7 +1498,6 @@ function completeAbstractValue( path, result, incrementalContext, - deferMap, ); } @@ -1605,7 +1567,6 @@ function completeObjectValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather @@ -1625,7 +1586,6 @@ function completeObjectValue( path, result, incrementalContext, - deferMap, ); }); } @@ -1642,7 +1602,6 @@ function completeObjectValue( path, result, incrementalContext, - deferMap, ); } @@ -1657,59 +1616,6 @@ function invalidReturnTypeError( ); } -/** - * Instantiates new DeferredFragmentRecords for the given path within an - * incremental data record, returning an updated map of DeferUsage - * objects to DeferredFragmentRecords. - * - * Note: As defer directives may be used with operations returning lists, - * a DeferUsage object may correspond to many DeferredFragmentRecords. - * - * DeferredFragmentRecord creation includes the following steps: - * 1. The new DeferredFragmentRecord is instantiated at the given path. - * 2. The parent result record is calculated from the given incremental data - * record. - * 3. The IncrementalPublisher is notified that a new DeferredFragmentRecord - * with the calculated parent has been added; the record will be released only - * after the parent has completed. - * - */ -function addNewDeferredFragments( - newDeferUsages: ReadonlyArray, - newDeferMap: Map, - path?: Path | undefined, -): ReadonlyMap { - // For each new deferUsage object: - for (const newDeferUsage of newDeferUsages) { - const parentDeferUsage = newDeferUsage.parentDeferUsage; - - const parent = - parentDeferUsage === undefined - ? undefined - : deferredFragmentRecordFromDeferUsage(parentDeferUsage, newDeferMap); - - // Instantiate the new record. - const deferredFragmentRecord = new DeferredFragmentRecord( - path, - newDeferUsage.label, - parent, - ); - - // Update the map. - newDeferMap.set(newDeferUsage, deferredFragmentRecord); - } - - return newDeferMap; -} - -function deferredFragmentRecordFromDeferUsage( - deferUsage: DeferUsage, - deferMap: ReadonlyMap, -): DeferredFragmentRecord { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return deferMap.get(deferUsage)!; -} - function collectAndExecuteSubfields( exeContext: ExecutionContext, returnType: GraphQLObjectType, @@ -1717,40 +1623,32 @@ function collectAndExecuteSubfields( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // Collect sub-fields to execute to complete this value. - const collectedSubfields = collectSubfields( + const originalGroupedFieldSet = collectSubfields( exeContext, returnType, fieldGroup, ); - let groupedFieldSet = collectedSubfields.groupedFieldSet; - const newDeferUsages = collectedSubfields.newDeferUsages; - if (deferMap === undefined && newDeferUsages.length === 0) { + if ( + !exeContext.encounteredDefer && + originalGroupedFieldSet.encounteredDefer !== true + ) { return executeFields( exeContext, returnType, result, path, - groupedFieldSet, + originalGroupedFieldSet, incrementalContext, - undefined, ); } - const subFieldPlan = buildSubFieldPlan( - groupedFieldSet, + exeContext.encounteredDefer = true; + const { groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( + originalGroupedFieldSet, incrementalContext?.deferUsageSet, ); - groupedFieldSet = subFieldPlan.groupedFieldSet; - const newGroupedFieldSets = subFieldPlan.newGroupedFieldSets; - const newDeferMap = addNewDeferredFragments( - newDeferUsages, - new Map(deferMap), - path, - ); - const subFields = executeFields( exeContext, returnType, @@ -1758,7 +1656,6 @@ function collectAndExecuteSubfields( path, groupedFieldSet, incrementalContext, - newDeferMap, ); if (newGroupedFieldSets.size > 0) { @@ -1769,7 +1666,6 @@ function collectAndExecuteSubfields( path, incrementalContext?.deferUsageSet, newGroupedFieldSets, - newDeferMap, ); return withNewDeferredGroupedFieldSets( @@ -2009,7 +1905,7 @@ function executeSubscription( ); } - const { groupedFieldSet } = collectFields( + const groupedFieldSet = collectFields( schema, fragments, variableValues, @@ -2095,19 +1991,14 @@ function executeDeferredGroupedFieldSets( path: Path | undefined, parentDeferUsages: DeferUsageSet | undefined, newGroupedFieldSets: Map, - deferMap: ReadonlyMap, ): ReadonlyArray { const newDeferredGroupedFieldSetRecords: Array = []; for (const [deferUsageSet, groupedFieldSet] of newGroupedFieldSets) { - const deferredFragmentRecords = getDeferredFragmentRecords( - deferUsageSet, - deferMap, - ); - const deferredGroupedFieldSetRecord: DeferredGroupedFieldSetRecord = { - deferredFragmentRecords, + deferUsages: deferUsageSet, + path, result: undefined as unknown as BoxedPromiseOrValue, }; @@ -2124,7 +2015,6 @@ function executeDeferredGroupedFieldSets( errors: undefined, deferUsageSet, }, - deferMap, ); if (exeContext.enableEarlyExecution) { @@ -2168,7 +2058,6 @@ function executeDeferredGroupedFieldSet( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext, - deferMap: ReadonlyMap, ): PromiseOrValue { let result; try { @@ -2179,7 +2068,6 @@ function executeDeferredGroupedFieldSet( path, groupedFieldSet, incrementalContext, - deferMap, ); } catch (error) { return { @@ -2229,15 +2117,6 @@ function buildDeferredGroupedFieldSetResult( }; } -function getDeferredFragmentRecords( - deferUsages: DeferUsageSet, - deferMap: ReadonlyMap, -): ReadonlyArray { - return Array.from(deferUsages).map((deferUsage) => - deferredFragmentRecordFromDeferUsage(deferUsage, deferMap), - ); -} - function buildSyncStreamItemQueue( initialItem: PromiseOrValue, initialIndex: number, @@ -2427,7 +2306,6 @@ function completeStreamItem( itemPath, item, incrementalContext, - new Map(), ).then( (resolvedItem) => buildStreamItemResult(incrementalContext.errors, resolvedItem), @@ -2448,7 +2326,6 @@ function completeStreamItem( itemPath, item, incrementalContext, - new Map(), ); } catch (rawError) { handleFieldError( diff --git a/src/execution/types.ts b/src/execution/types.ts index c88ae9986e..18ef93c747 100644 --- a/src/execution/types.ts +++ b/src/execution/types.ts @@ -7,6 +7,8 @@ import type { GraphQLFormattedError, } from '../error/GraphQLError.js'; +import type { DeferUsage } from './collectFields.js'; + /** * The result of GraphQL execution. * @@ -169,7 +171,7 @@ export interface FormattedCompletedResult { export function isDeferredGroupedFieldSetRecord( incrementalDataRecord: IncrementalDataRecord, ): incrementalDataRecord is DeferredGroupedFieldSetRecord { - return 'deferredFragmentRecords' in incrementalDataRecord; + return 'deferUsages' in incrementalDataRecord; } export type DeferredGroupedFieldSetResult = @@ -208,7 +210,8 @@ type ThunkIncrementalResult = | (() => BoxedPromiseOrValue); export interface DeferredGroupedFieldSetRecord { - deferredFragmentRecords: ReadonlyArray; + deferUsages: ReadonlySet; + path: Path | undefined; result: ThunkIncrementalResult; } @@ -218,8 +221,8 @@ export type SubsequentResultRecord = DeferredFragmentRecord | StreamRecord; export class DeferredFragmentRecord { path: Path | undefined; label: string | undefined; + parentDeferUsage: DeferUsage | undefined; id?: string | undefined; - parent: DeferredFragmentRecord | undefined; deferredGroupedFieldSetRecords: Set; reconcilableResults: Set; children: Set; @@ -227,11 +230,11 @@ export class DeferredFragmentRecord { constructor( path: Path | undefined, label: string | undefined, - parent: DeferredFragmentRecord | undefined, + parentDeferUsage: DeferUsage | undefined, ) { this.path = path; this.label = label; - this.parent = parent; + this.parentDeferUsage = parentDeferUsage; this.deferredGroupedFieldSetRecords = new Set(); this.reconcilableResults = new Set(); this.children = new Set(); diff --git a/src/jsutils/Path.ts b/src/jsutils/Path.ts index d223b6e752..f8200c6359 100644 --- a/src/jsutils/Path.ts +++ b/src/jsutils/Path.ts @@ -4,6 +4,7 @@ export interface Path { readonly prev: Path | undefined; readonly key: string | number; readonly typename: string | undefined; + readonly fieldDepth: number; } /** @@ -14,7 +15,12 @@ export function addPath( key: string | number, typename: string | undefined, ): Path { - return { prev, key, typename }; + const fieldDepth = prev + ? typeof key === 'number' + ? prev.fieldDepth + : prev.fieldDepth + 1 + : 1; + return { prev, key, typename, fieldDepth }; } /** @@ -31,3 +37,25 @@ export function pathToArray( } return flattened.reverse(); } + +export function pathAtFieldDepth( + path: Path | undefined, + fieldDepth: number, +): Path | undefined { + if (fieldDepth === 0) { + return undefined; + } + let currentPath = path; + while (currentPath !== undefined) { + if (currentPath.fieldDepth === fieldDepth) { + return currentPath; + } + currentPath = currentPath.prev; + } + /* c8 ignore next 5 */ + throw new Error( + `Path is of fieldDepth ${ + path === undefined ? 0 : path.fieldDepth + }, but fieldDepth ${fieldDepth} requested.`, + ); +} diff --git a/src/jsutils/__tests__/Path-test.ts b/src/jsutils/__tests__/Path-test.ts index 0484377db9..906e1b4f67 100644 --- a/src/jsutils/__tests__/Path-test.ts +++ b/src/jsutils/__tests__/Path-test.ts @@ -11,6 +11,7 @@ describe('Path', () => { prev: undefined, key: 1, typename: 'First', + fieldDepth: 1, }); }); @@ -22,6 +23,7 @@ describe('Path', () => { prev: first, key: 'two', typename: 'Second', + fieldDepth: 2, }); }); diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 700bc0bda7..9707bfe8be 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -47,7 +47,7 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - const { groupedFieldSet } = collectFields( + const groupedFieldSet = collectFields( schema, fragments, variableValues,