Skip to content

Commit

Permalink
incremental: allow nested defers at the same level (#4002)
Browse files Browse the repository at this point in the history
Implements Option A from
graphql/defer-stream-wg#80
  • Loading branch information
yaacovCR committed Mar 20, 2024
1 parent ef478a2 commit 51f41eb
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 51f41eb

Please sign in to comment.