Skip to content

Commit 9eb30c2

Browse files
committed
deduplicate leaf fields present in an initial or parent payload
1 parent 11447b5 commit 9eb30c2

File tree

3 files changed

+106
-18
lines changed

3 files changed

+106
-18
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ describe('Execute: defer directive', () => {
186186
}
187187
}
188188
fragment NameFragment on Hero {
189-
id
190189
name
191190
}
192191
`);
@@ -205,7 +204,6 @@ describe('Execute: defer directive', () => {
205204
incremental: [
206205
{
207206
data: {
208-
id: '1',
209207
name: 'Luke',
210208
},
211209
path: ['hero'],
@@ -410,9 +408,7 @@ describe('Execute: defer directive', () => {
410408
{
411409
incremental: [
412410
{
413-
data: {
414-
name: 'Luke',
415-
},
411+
data: {},
416412
path: ['hero'],
417413
},
418414
],
@@ -447,9 +443,7 @@ describe('Execute: defer directive', () => {
447443
{
448444
incremental: [
449445
{
450-
data: {
451-
name: 'Luke',
452-
},
446+
data: {},
453447
path: ['hero'],
454448
},
455449
],
@@ -527,7 +521,7 @@ describe('Execute: defer directive', () => {
527521
]);
528522
});
529523

530-
it('Does not deduplicate leaf fields present in the initial payload', async () => {
524+
it('Can deduplicate leaf fields present in the initial payload', async () => {
531525
const document = parse(`
532526
query {
533527
hero {
@@ -585,6 +579,48 @@ describe('Execute: defer directive', () => {
585579
},
586580
},
587581
anotherNestedObject: {
582+
deeperObject: {},
583+
},
584+
},
585+
path: ['hero'],
586+
},
587+
],
588+
hasNext: false,
589+
},
590+
]);
591+
});
592+
593+
it('Can deduplicate leaf fields present in a parent defer payload', async () => {
594+
const document = parse(`
595+
query {
596+
hero {
597+
... @defer {
598+
nestedObject {
599+
deeperObject {
600+
foo
601+
... @defer {
602+
foo
603+
bar
604+
}
605+
}
606+
}
607+
}
608+
}
609+
}
610+
`);
611+
const result = await complete(document);
612+
expectJSON(result).toDeepEqual([
613+
{
614+
data: {
615+
hero: {},
616+
},
617+
hasNext: true,
618+
},
619+
{
620+
incremental: [
621+
{
622+
data: {
623+
nestedObject: {
588624
deeperObject: {
589625
foo: 'foo',
590626
},
@@ -593,12 +629,23 @@ describe('Execute: defer directive', () => {
593629
path: ['hero'],
594630
},
595631
],
632+
hasNext: true,
633+
},
634+
{
635+
incremental: [
636+
{
637+
data: {
638+
bar: 'bar',
639+
},
640+
path: ['hero', 'nestedObject', 'deeperObject'],
641+
},
642+
],
596643
hasNext: false,
597644
},
598645
]);
599646
});
600647

601-
it('Does not deduplicate fields with deferred fragments at multiple levels', async () => {
648+
it('Does not completely deduplicate fields with deferred fragments at multiple levels', async () => {
602649
const document = parse(`
603650
query {
604651
hero {
@@ -649,7 +696,6 @@ describe('Execute: defer directive', () => {
649696
incremental: [
650697
{
651698
data: {
652-
foo: 'foo',
653699
bar: 'bar',
654700
baz: 'baz',
655701
bak: 'bak',
@@ -659,7 +705,6 @@ describe('Execute: defer directive', () => {
659705
{
660706
data: {
661707
deeperObject: {
662-
foo: 'foo',
663708
bar: 'bar',
664709
baz: 'baz',
665710
},
@@ -670,7 +715,6 @@ describe('Execute: defer directive', () => {
670715
data: {
671716
nestedObject: {
672717
deeperObject: {
673-
foo: 'foo',
674718
bar: 'bar',
675719
},
676720
},

src/execution/collectFields.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { OperationTypeNode } from '../language/ast.js';
1414
import { Kind } from '../language/kinds.js';
1515

1616
import type { GraphQLObjectType } from '../type/definition.js';
17-
import { isAbstractType } from '../type/definition.js';
17+
import { isAbstractType, isLeafType } from '../type/definition.js';
1818
import {
1919
GraphQLDeferDirective,
2020
GraphQLIncludeDirective,
@@ -42,13 +42,15 @@ import { getDirectiveValues } from './values.js';
4242
export interface FieldGroup {
4343
depth: number;
4444
fields: Map<number | undefined, ReadonlyArray<FieldNode>>;
45+
isLeaf: boolean;
4546
}
4647

4748
export type GroupedFieldSet = Map<string, FieldGroup>;
4849

4950
interface MutableFieldGroup {
5051
depth: number;
5152
fields: AccumulatorMap<number | undefined, FieldNode>;
53+
isLeaf: boolean;
5254
}
5355

5456
/**
@@ -166,9 +168,12 @@ function collectFieldsImpl(
166168
const key = getFieldEntryKey(selection);
167169
let fieldGroup = groupedFieldSet.get(key);
168170
if (!fieldGroup) {
171+
const fieldDef = schema.getField(runtimeType, selection.name.value);
172+
const isLeaf = isLeafType(fieldDef?.type);
169173
fieldGroup = {
170174
depth,
171175
fields: new AccumulatorMap<number | undefined, FieldNode>(),
176+
isLeaf,
172177
};
173178
groupedFieldSet.set(key, fieldGroup);
174179
}

src/execution/execute.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,10 +658,49 @@ function executeFieldsSerially(
658658

659659
function shouldExecute(
660660
fieldGroup: FieldGroup,
661-
deferDepth?: number | undefined,
661+
asyncPayload?: AsyncPayloadRecord | undefined,
662662
): boolean {
663-
const fieldGroupForDeferDepth = fieldGroup.fields.get(deferDepth);
664-
return fieldGroupForDeferDepth !== undefined;
663+
let deferDepth: number | undefined;
664+
if (
665+
asyncPayload === undefined ||
666+
(deferDepth = asyncPayload.deferDepth) === undefined ||
667+
!fieldGroup.isLeaf
668+
) {
669+
for (const fieldDeferDepth of fieldGroup.fields.keys()) {
670+
if (fieldDeferDepth === deferDepth) {
671+
return true;
672+
}
673+
}
674+
return false;
675+
}
676+
677+
let hasDepth = false;
678+
for (const fieldDeferDepth of fieldGroup.fields.keys()) {
679+
if (inDeferTree(asyncPayload, fieldDeferDepth, deferDepth)) {
680+
return false;
681+
}
682+
if (fieldDeferDepth === deferDepth) {
683+
hasDepth = true;
684+
}
685+
}
686+
return hasDepth;
687+
}
688+
689+
function inDeferTree(
690+
asyncPayload: AsyncPayloadRecord,
691+
fieldDeferDepth: number | undefined,
692+
deferDepth: number | undefined,
693+
): boolean {
694+
const parentContext = asyncPayload.parentContext;
695+
if (parentContext === undefined) {
696+
return fieldDeferDepth === undefined;
697+
}
698+
const parentDeferDepth = parentContext.deferDepth;
699+
// Stream payloads have the same deferDepth as their parent, and so should be skipped.
700+
if (parentDeferDepth !== deferDepth && fieldDeferDepth === parentDeferDepth) {
701+
return true;
702+
}
703+
return inDeferTree(parentContext, fieldDeferDepth, parentDeferDepth);
665704
}
666705

667706
/**
@@ -683,7 +722,7 @@ function executeFields(
683722
for (const [responseName, fieldGroup] of groupedFieldSet) {
684723
const fieldPath = exeContext.addPath(path, responseName, parentType.name);
685724

686-
if (shouldExecute(fieldGroup, asyncPayloadRecord?.deferDepth)) {
725+
if (shouldExecute(fieldGroup, asyncPayloadRecord)) {
687726
const result = executeField(
688727
exeContext,
689728
parentType,

0 commit comments

Comments
 (0)