diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index 8a94478960..d03570270a 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -355,13 +355,11 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, - pending: [ - { id: '0', path: ['hero'], label: 'DeferTop' }, - { id: '1', path: ['hero'], label: 'DeferNested' }, - ], + pending: [{ id: '0', path: ['hero'], label: 'DeferTop' }], hasNext: true, }, { + pending: [{ id: '1', path: ['hero'], label: 'DeferNested' }], incremental: [ { data: { @@ -369,6 +367,12 @@ describe('Execute: defer directive', () => { }, id: '0', }, + ], + completed: [{ id: '0' }], + hasNext: true, + }, + { + incremental: [ { data: { friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }], @@ -376,7 +380,7 @@ describe('Execute: defer directive', () => { id: '1', }, ], - completed: [{ id: '0' }, { id: '1' }], + completed: [{ id: '1' }], hasNext: false, }, ]); @@ -472,6 +476,35 @@ describe('Execute: defer directive', () => { }); }); + it('Emits children of empty defer fragments', async () => { + const document = parse(` + query HeroNameQuery { + hero { + ... @defer { + ... @defer { + name + } + } + } + } + `); + const result = await complete(document); + expectJSON(result).toDeepEqual([ + { + data: { + hero: {}, + }, + pending: [{ id: '0', path: ['hero'] }], + hasNext: true, + }, + { + incremental: [{ data: { name: 'Luke' }, id: '0' }], + completed: [{ id: '0' }], + hasNext: false, + }, + ]); + }); + it('Can separately emit defer fragments with different labels with varying fields', async () => { const document = parse(` query HeroNameQuery { diff --git a/src/execution/buildFieldPlan.ts b/src/execution/buildFieldPlan.ts index 7f9f6bc98e..390e2cf813 100644 --- a/src/execution/buildFieldPlan.ts +++ b/src/execution/buildFieldPlan.ts @@ -8,7 +8,6 @@ export type DeferUsageSet = ReadonlySet; export interface FieldGroup { fields: ReadonlyArray; deferUsages?: DeferUsageSet | undefined; - knownDeferUsages?: DeferUsageSet | undefined; } export type GroupedFieldSet = Map; @@ -21,21 +20,15 @@ export interface NewGroupedFieldSetDetails { export function buildFieldPlan( fields: Map>, parentDeferUsages: DeferUsageSet = new Set(), - knownDeferUsages: DeferUsageSet = new Set(), ): { groupedFieldSet: GroupedFieldSet; newGroupedFieldSetDetailsMap: Map; - newDeferUsages: ReadonlyArray; } { - const newDeferUsages: Set = new Set(); - const newKnownDeferUsages = new Set(knownDeferUsages); - const groupedFieldSet = new Map< string, { fields: Array; deferUsages: DeferUsageSet; - knownDeferUsages: DeferUsageSet; } >(); @@ -47,7 +40,6 @@ export function buildFieldPlan( { fields: Array; deferUsages: DeferUsageSet; - knownDeferUsages: DeferUsageSet; } >; shouldInitiateDefer: boolean; @@ -72,10 +64,6 @@ export function buildFieldPlan( continue; } deferUsageSet.add(deferUsage); - if (!knownDeferUsages.has(deferUsage)) { - newDeferUsages.add(deferUsage); - newKnownDeferUsages.add(deferUsage); - } } if (inOriginalResult) { deferUsageSet.clear(); @@ -99,7 +87,6 @@ export function buildFieldPlan( fieldGroup = { fields: [], deferUsages: deferUsageSet, - knownDeferUsages: newKnownDeferUsages, }; groupedFieldSet.set(responseKey, fieldGroup); } @@ -140,7 +127,6 @@ export function buildFieldPlan( fieldGroup = { fields: [], deferUsages: deferUsageSet, - knownDeferUsages: newKnownDeferUsages, }; newGroupedFieldSet.set(responseKey, fieldGroup); } @@ -150,7 +136,6 @@ export function buildFieldPlan( return { groupedFieldSet, newGroupedFieldSetDetailsMap, - newDeferUsages: Array.from(newDeferUsages), }; } diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 7625e1af18..03ba5efde6 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -60,8 +60,12 @@ export function collectFields( variableValues: { [variable: string]: unknown }, runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, -): Map> { +): { + fields: Map>; + newDeferUsages: ReadonlyArray; +} { const groupedFieldSet = new AccumulatorMap(); + const newDeferUsages: Array = []; const context: CollectFieldsContext = { schema, fragments, @@ -71,8 +75,13 @@ export function collectFields( visitedFragmentNames: new Set(), }; - collectFieldsImpl(context, operation.selectionSet, groupedFieldSet); - return groupedFieldSet; + collectFieldsImpl( + context, + operation.selectionSet, + groupedFieldSet, + newDeferUsages, + ); + return { fields: groupedFieldSet, newDeferUsages }; } /** @@ -93,7 +102,10 @@ export function collectSubfields( operation: OperationDefinitionNode, returnType: GraphQLObjectType, fieldDetails: ReadonlyArray, -): Map> { +): { + fields: Map>; + newDeferUsages: ReadonlyArray; +} { const context: CollectFieldsContext = { schema, fragments, @@ -103,6 +115,7 @@ export function collectSubfields( visitedFragmentNames: new Set(), }; const subGroupedFieldSet = new AccumulatorMap(); + const newDeferUsages: Array = []; for (const fieldDetail of fieldDetails) { const node = fieldDetail.node; @@ -111,19 +124,23 @@ export function collectSubfields( context, node.selectionSet, subGroupedFieldSet, + newDeferUsages, fieldDetail.deferUsage, ); } } - return subGroupedFieldSet; + return { + fields: subGroupedFieldSet, + newDeferUsages, + }; } function collectFieldsImpl( context: CollectFieldsContext, selectionSet: SelectionSetNode, groupedFieldSet: AccumulatorMap, - parentDeferUsage?: DeferUsage, + newDeferUsages: Array, deferUsage?: DeferUsage, ): void { const { @@ -143,7 +160,7 @@ function collectFieldsImpl( } groupedFieldSet.add(getFieldEntryKey(selection), { node: selection, - deferUsage: deferUsage ?? parentDeferUsage, + deferUsage, }); break; } @@ -159,16 +176,27 @@ function collectFieldsImpl( operation, variableValues, selection, - parentDeferUsage, + deferUsage, ); - collectFieldsImpl( - context, - selection.selectionSet, - groupedFieldSet, - parentDeferUsage, - newDeferUsage ?? deferUsage, - ); + if (!newDeferUsage) { + collectFieldsImpl( + context, + selection.selectionSet, + groupedFieldSet, + newDeferUsages, + deferUsage, + ); + } else { + newDeferUsages.push(newDeferUsage); + collectFieldsImpl( + context, + selection.selectionSet, + groupedFieldSet, + newDeferUsages, + newDeferUsage, + ); + } break; } @@ -179,7 +207,7 @@ function collectFieldsImpl( operation, variableValues, selection, - parentDeferUsage, + deferUsage, ); if ( @@ -199,15 +227,23 @@ function collectFieldsImpl( } if (!newDeferUsage) { visitedFragmentNames.add(fragName); + collectFieldsImpl( + context, + fragment.selectionSet, + groupedFieldSet, + newDeferUsages, + deferUsage, + ); + } else { + newDeferUsages.push(newDeferUsage); + collectFieldsImpl( + context, + fragment.selectionSet, + groupedFieldSet, + newDeferUsages, + newDeferUsage, + ); } - - collectFieldsImpl( - context, - fragment.selectionSet, - groupedFieldSet, - parentDeferUsage, - newDeferUsage ?? deferUsage, - ); break; } } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index baf6b41ea1..370186f552 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -91,7 +91,7 @@ const buildSubFieldPlan = memoize3( returnType: GraphQLObjectType, fieldGroup: FieldGroup, ) => { - const subFields = collectSubfields( + const { fields: subFields, newDeferUsages } = collectSubfields( exeContext.schema, exeContext.fragments, exeContext.variableValues, @@ -99,11 +99,10 @@ const buildSubFieldPlan = memoize3( returnType, fieldGroup.fields, ); - return buildFieldPlan( - subFields, - fieldGroup.deferUsages, - fieldGroup.knownDeferUsages, - ); + return { + ...buildFieldPlan(subFields, fieldGroup.deferUsages), + newDeferUsages, + }; }, ); @@ -408,14 +407,14 @@ function executeOperation( ); } - const fields = collectFields( + const { fields, newDeferUsages } = collectFields( schema, fragments, variableValues, rootType, operation, ); - const { groupedFieldSet, newGroupedFieldSetDetailsMap, newDeferUsages } = + const { groupedFieldSet, newGroupedFieldSetDetailsMap } = buildFieldPlan(fields); const newDeferMap = addNewDeferredFragments( @@ -1807,7 +1806,7 @@ function executeSubscription( ); } - const fields = collectFields( + const { fields } = collectFields( schema, fragments, variableValues, diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 15aee1ece4..06d9545fbc 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -49,7 +49,7 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - const fields = collectFields( + const { fields } = collectFields( schema, fragments, variableValues,