Skip to content

Commit

Permalink
allow nested defers at the same level
Browse files Browse the repository at this point in the history
if we allow nesting of defers at the same level then we have to handle the case where a defer is completely empty -- not just empty because its fields are also contained within a parent.

this means that collectFields must directly return the new defer usages so that even empty defer usages can be used to create fragments so that we can track their children
  • Loading branch information
yaacovCR committed Mar 19, 2024
1 parent ef478a2 commit bf20902
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 54 deletions.
43 changes: 38 additions & 5 deletions src/execution/__tests__/defer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,28 +355,32 @@ 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: {
id: '1',
},
id: '0',
},
],
completed: [{ id: '0' }],
hasNext: true,
},
{
incremental: [
{
data: {
friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }],
},
id: '1',
},
],
completed: [{ id: '0' }, { id: '1' }],
completed: [{ id: '1' }],
hasNext: false,
},
]);
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 0 additions & 15 deletions src/execution/buildFieldPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export type DeferUsageSet = ReadonlySet<DeferUsage>;
export interface FieldGroup {
fields: ReadonlyArray<FieldDetails>;
deferUsages?: DeferUsageSet | undefined;
knownDeferUsages?: DeferUsageSet | undefined;
}

export type GroupedFieldSet = Map<string, FieldGroup>;
Expand All @@ -21,21 +20,15 @@ export interface NewGroupedFieldSetDetails {
export function buildFieldPlan(
fields: Map<string, ReadonlyArray<FieldDetails>>,
parentDeferUsages: DeferUsageSet = new Set<DeferUsage>(),
knownDeferUsages: DeferUsageSet = new Set<DeferUsage>(),
): {
groupedFieldSet: GroupedFieldSet;
newGroupedFieldSetDetailsMap: Map<DeferUsageSet, NewGroupedFieldSetDetails>;
newDeferUsages: ReadonlyArray<DeferUsage>;
} {
const newDeferUsages: Set<DeferUsage> = new Set<DeferUsage>();
const newKnownDeferUsages = new Set<DeferUsage>(knownDeferUsages);

const groupedFieldSet = new Map<
string,
{
fields: Array<FieldDetails>;
deferUsages: DeferUsageSet;
knownDeferUsages: DeferUsageSet;
}
>();

Expand All @@ -47,7 +40,6 @@ export function buildFieldPlan(
{
fields: Array<FieldDetails>;
deferUsages: DeferUsageSet;
knownDeferUsages: DeferUsageSet;
}
>;
shouldInitiateDefer: boolean;
Expand All @@ -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();
Expand All @@ -99,7 +87,6 @@ export function buildFieldPlan(
fieldGroup = {
fields: [],
deferUsages: deferUsageSet,
knownDeferUsages: newKnownDeferUsages,
};
groupedFieldSet.set(responseKey, fieldGroup);
}
Expand Down Expand Up @@ -140,7 +127,6 @@ export function buildFieldPlan(
fieldGroup = {
fields: [],
deferUsages: deferUsageSet,
knownDeferUsages: newKnownDeferUsages,
};
newGroupedFieldSet.set(responseKey, fieldGroup);
}
Expand All @@ -150,7 +136,6 @@ export function buildFieldPlan(
return {
groupedFieldSet,
newGroupedFieldSetDetailsMap,
newDeferUsages: Array.from(newDeferUsages),
};
}

Expand Down
84 changes: 60 additions & 24 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ export function collectFields(
variableValues: { [variable: string]: unknown },
runtimeType: GraphQLObjectType,
operation: OperationDefinitionNode,
): Map<string, ReadonlyArray<FieldDetails>> {
): {
fields: Map<string, ReadonlyArray<FieldDetails>>;
newDeferUsages: ReadonlyArray<DeferUsage>;
} {
const groupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];
const context: CollectFieldsContext = {
schema,
fragments,
Expand All @@ -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 };
}

/**
Expand All @@ -93,7 +102,10 @@ export function collectSubfields(
operation: OperationDefinitionNode,
returnType: GraphQLObjectType,
fieldDetails: ReadonlyArray<FieldDetails>,
): Map<string, ReadonlyArray<FieldDetails>> {
): {
fields: Map<string, ReadonlyArray<FieldDetails>>;
newDeferUsages: ReadonlyArray<DeferUsage>;
} {
const context: CollectFieldsContext = {
schema,
fragments,
Expand All @@ -103,6 +115,7 @@ export function collectSubfields(
visitedFragmentNames: new Set(),
};
const subGroupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];

for (const fieldDetail of fieldDetails) {
const node = fieldDetail.node;
Expand All @@ -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<string, FieldDetails>,
parentDeferUsage?: DeferUsage,
newDeferUsages: Array<DeferUsage>,
deferUsage?: DeferUsage,
): void {
const {
Expand All @@ -143,7 +160,7 @@ function collectFieldsImpl(
}
groupedFieldSet.add(getFieldEntryKey(selection), {
node: selection,
deferUsage: deferUsage ?? parentDeferUsage,
deferUsage,
});
break;
}
Expand All @@ -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;
}
Expand All @@ -179,7 +207,7 @@ function collectFieldsImpl(
operation,
variableValues,
selection,
parentDeferUsage,
deferUsage,
);

if (
Expand All @@ -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;
}
}
Expand Down
17 changes: 8 additions & 9 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,18 @@ const buildSubFieldPlan = memoize3(
returnType: GraphQLObjectType,
fieldGroup: FieldGroup,
) => {
const subFields = collectSubfields(
const { fields: subFields, newDeferUsages } = collectSubfields(
exeContext.schema,
exeContext.fragments,
exeContext.variableValues,
exeContext.operation,
returnType,
fieldGroup.fields,
);
return buildFieldPlan(
subFields,
fieldGroup.deferUsages,
fieldGroup.knownDeferUsages,
);
return {
...buildFieldPlan(subFields, fieldGroup.deferUsages),
newDeferUsages,
};
},
);

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1807,7 +1806,7 @@ function executeSubscription(
);
}

const fields = collectFields(
const { fields } = collectFields(
schema,
fragments,
variableValues,
Expand Down
2 changes: 1 addition & 1 deletion src/validation/rules/SingleFieldSubscriptionsRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function SingleFieldSubscriptionsRule(
fragments[definition.name.value] = definition;
}
}
const fields = collectFields(
const { fields } = collectFields(
schema,
fragments,
variableValues,
Expand Down

0 comments on commit bf20902

Please sign in to comment.