Skip to content

Commit b6b15c6

Browse files
committed
Cancels deferred fields when overlapping deferred results exhibits null bubbling
1 parent 7a6d055 commit b6b15c6

File tree

4 files changed

+174
-65
lines changed

4 files changed

+174
-65
lines changed

src/execution/IncrementalPublisher.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -350,13 +350,20 @@ export class IncrementalPublisher {
350350
const streams = new Set<StreamRecord>();
351351

352352
const children = this._getChildren(erroringIncrementalDataRecord);
353-
const descendants = this._getDescendants(children);
353+
const descendants = new Set<SubsequentResultRecord>();
354+
this._getDescendants(children, descendants, (child) =>
355+
this._nullsChildSubsequentResultRecord(child, nullPathArray),
356+
);
354357

355-
for (const child of descendants) {
356-
if (!this._nullsChildSubsequentResultRecord(child, nullPathArray)) {
357-
continue;
358-
}
358+
if (isDeferredGroupedFieldSetRecord(erroringIncrementalDataRecord)) {
359+
this.filterSiblings(
360+
nullPathArray,
361+
erroringIncrementalDataRecord,
362+
descendants,
363+
);
364+
}
359365

366+
for (const child of descendants) {
360367
child.filtered = true;
361368

362369
if (isStreamItemsRecord(child)) {
@@ -371,6 +378,30 @@ export class IncrementalPublisher {
371378
});
372379
}
373380

381+
private filterSiblings(
382+
nullPath: Array<string | number>,
383+
erroringIncrementalDataRecord: DeferredGroupedFieldSetRecord,
384+
descendants: Set<SubsequentResultRecord>,
385+
): Set<SubsequentResultRecord> {
386+
for (const deferredFragmentRecord of erroringIncrementalDataRecord.deferredFragmentRecords) {
387+
for (const deferredGroupedFieldSetRecord of deferredFragmentRecord.deferredGroupedFieldSetRecords) {
388+
if (deferredGroupedFieldSetRecord === erroringIncrementalDataRecord) {
389+
continue;
390+
}
391+
if (this._matchesPath(deferredGroupedFieldSetRecord.path, nullPath)) {
392+
deferredFragmentRecord.deferredGroupedFieldSetRecords.delete(
393+
deferredGroupedFieldSetRecord,
394+
);
395+
deferredFragmentRecord._pending.delete(deferredGroupedFieldSetRecord);
396+
397+
const children = this._getChildren(deferredGroupedFieldSetRecord);
398+
this._getDescendants(children, descendants);
399+
}
400+
}
401+
}
402+
return descendants;
403+
}
404+
374405
private _pendingSourcesToResults(
375406
pendingSources: ReadonlySet<DeferredFragmentRecord | StreamRecord>,
376407
): Array<PendingResult> {
@@ -694,10 +725,13 @@ export class IncrementalPublisher {
694725
private _getDescendants(
695726
children: ReadonlySet<SubsequentResultRecord>,
696727
descendants = new Set<SubsequentResultRecord>(),
697-
): ReadonlySet<SubsequentResultRecord> {
728+
predicate = (_child: SubsequentResultRecord) => true,
729+
): Set<SubsequentResultRecord> {
698730
for (const child of children) {
699-
descendants.add(child);
700-
this._getDescendants(child.children, descendants);
731+
if (predicate(child)) {
732+
descendants.add(child);
733+
this._getDescendants(child.children, descendants, () => true);
734+
}
701735
}
702736
return descendants;
703737
}
@@ -760,7 +794,6 @@ export class DeferredGroupedFieldSetRecord {
760794
path: ReadonlyArray<string | number>;
761795
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
762796
groupedFieldSet: GroupedFieldSet;
763-
shouldInitiateDefer: boolean;
764797
errors: Array<GraphQLError>;
765798
data: ObjMap<unknown> | undefined;
766799
sent: boolean;
@@ -769,12 +802,10 @@ export class DeferredGroupedFieldSetRecord {
769802
path: Path | undefined;
770803
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
771804
groupedFieldSet: GroupedFieldSet;
772-
shouldInitiateDefer: boolean;
773805
}) {
774806
this.path = pathToArray(opts.path);
775807
this.deferredFragmentRecords = opts.deferredFragmentRecords;
776808
this.groupedFieldSet = opts.groupedFieldSet;
777-
this.shouldInitiateDefer = opts.shouldInitiateDefer;
778809
this.errors = [];
779810
this.sent = false;
780811
}

src/execution/__tests__/defer-test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,64 @@ describe('Execute: defer directive', () => {
14781478
]);
14791479
});
14801480

1481+
it('Cancels deferred fields when overlapping deferred results exhibits null bubbling', async () => {
1482+
const document = parse(`
1483+
query {
1484+
... @defer {
1485+
hero {
1486+
nonNullName
1487+
id
1488+
}
1489+
}
1490+
... @defer {
1491+
hero {
1492+
nonNullName
1493+
name
1494+
}
1495+
}
1496+
}
1497+
`);
1498+
const result = await complete(document, {
1499+
hero: {
1500+
...hero,
1501+
nonNullName: Promise.resolve(null),
1502+
},
1503+
});
1504+
expectJSON(result).toDeepEqual([
1505+
{
1506+
data: {},
1507+
pending: [
1508+
{ id: '0', path: [] },
1509+
{ id: '1', path: [] },
1510+
],
1511+
hasNext: true,
1512+
},
1513+
{
1514+
incremental: [
1515+
{
1516+
data: {
1517+
hero: null,
1518+
},
1519+
errors: [
1520+
{
1521+
message:
1522+
'Cannot return null for non-nullable field Hero.nonNullName.',
1523+
locations: [
1524+
{ line: 5, column: 13 },
1525+
{ line: 11, column: 13 },
1526+
],
1527+
path: ['hero', 'nonNullName'],
1528+
},
1529+
],
1530+
id: '0',
1531+
},
1532+
],
1533+
completed: [{ id: '0' }, { id: '1' }],
1534+
hasNext: false,
1535+
},
1536+
]);
1537+
});
1538+
14811539
it('Deduplicates list fields', async () => {
14821540
const document = parse(`
14831541
query {

src/execution/collectFields.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,10 @@ export interface FieldGroup {
5151

5252
export type GroupedFieldSet = Map<string, FieldGroup>;
5353

54-
export interface GroupedFieldSetDetails {
55-
groupedFieldSet: GroupedFieldSet;
56-
shouldInitiateDefer: boolean;
57-
}
58-
5954
export interface CollectFieldsResult {
6055
groupedFieldSet: GroupedFieldSet;
61-
newGroupedFieldSetDetails: Map<DeferUsageSet, GroupedFieldSetDetails>;
56+
supplementalGroupedFieldSets: Map<DeferUsageSet, GroupedFieldSet>;
57+
newDeferredGroupedFieldSets: Map<DeferUsageSet, GroupedFieldSet>;
6258
newDeferUsages: ReadonlyArray<DeferUsage>;
6359
}
6460

@@ -358,7 +354,8 @@ function buildGroupedFieldSets(
358354
parentTargets = NON_DEFERRED_TARGET_SET,
359355
): {
360356
groupedFieldSet: GroupedFieldSet;
361-
newGroupedFieldSetDetails: Map<DeferUsageSet, GroupedFieldSetDetails>;
357+
supplementalGroupedFieldSets: Map<DeferUsageSet, GroupedFieldSet>;
358+
newDeferredGroupedFieldSets: Map<DeferUsageSet, GroupedFieldSet>;
362359
} {
363360
const { parentTargetKeys, targetSetDetailsMap } = getTargetSetDetails(
364361
targetsByKey,
@@ -375,10 +372,11 @@ function buildGroupedFieldSets(
375372
)
376373
: new Map();
377374

378-
const newGroupedFieldSetDetails = new Map<
375+
const supplementalGroupedFieldSets = new Map<
379376
DeferUsageSet,
380-
GroupedFieldSetDetails
377+
GroupedFieldSet
381378
>();
379+
const newDeferredGroupedFieldSets = new Map<DeferUsageSet, GroupedFieldSet>();
382380

383381
for (const [maskingTargets, targetSetDetails] of targetSetDetailsMap) {
384382
const { keys, shouldInitiateDefer } = targetSetDetails;
@@ -391,16 +389,16 @@ function buildGroupedFieldSets(
391389
);
392390

393391
// All TargetSets that causes new grouped field sets consist only of DeferUsages
394-
// and have shouldInitiateDefer defined
395-
newGroupedFieldSetDetails.set(maskingTargets as DeferUsageSet, {
396-
groupedFieldSet: newGroupedFieldSet,
397-
shouldInitiateDefer,
398-
});
392+
(shouldInitiateDefer
393+
? newDeferredGroupedFieldSets
394+
: supplementalGroupedFieldSets
395+
).set(maskingTargets as DeferUsageSet, newGroupedFieldSet);
399396
}
400397

401398
return {
402399
groupedFieldSet,
403-
newGroupedFieldSetDetails,
400+
supplementalGroupedFieldSets,
401+
newDeferredGroupedFieldSets,
404402
};
405403
}
406404

0 commit comments

Comments
 (0)