Skip to content

Commit 6c701c7

Browse files
committed
simplify CollectFields for @defer and @stream (#3994)
minimizes the changes to `CollectFields` required for incremental delivery (inspired by #3982) -- but retains a single memoized incremental field plan per list item.
1 parent 688ee2f commit 6c701c7

File tree

6 files changed

+364
-329
lines changed

6 files changed

+364
-329
lines changed

src/execution/IncrementalPublisher.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
GraphQLFormattedError,
99
} from '../error/GraphQLError.js';
1010

11-
import type { GroupedFieldSet } from './collectFields.js';
11+
import type { GroupedFieldSet } from './buildFieldPlan.js';
1212

1313
interface IncrementalUpdate<TData = unknown, TExtensions = ObjMap<unknown>> {
1414
pending: ReadonlyArray<PendingResult>;
@@ -301,25 +301,20 @@ export class IncrementalPublisher {
301301
initialResultRecord: InitialResultRecord,
302302
data: ObjMap<unknown> | null,
303303
): ExecutionResult | ExperimentalIncrementalExecutionResults {
304+
const pendingSources = new Set<DeferredFragmentRecord | StreamRecord>();
304305
for (const child of initialResultRecord.children) {
305306
if (child.filtered) {
306307
continue;
307308
}
308-
this._publish(child);
309+
const maybePendingSource = this._publish(child);
310+
if (maybePendingSource) {
311+
pendingSources.add(maybePendingSource);
312+
}
309313
}
310314

311315
const errors = initialResultRecord.errors;
312316
const initialResult = errors.length === 0 ? { data } : { errors, data };
313-
const pending = this._pending;
314-
if (pending.size > 0) {
315-
const pendingSources = new Set<DeferredFragmentRecord | StreamRecord>();
316-
for (const subsequentResultRecord of pending) {
317-
const pendingSource = isStreamItemsRecord(subsequentResultRecord)
318-
? subsequentResultRecord.streamRecord
319-
: subsequentResultRecord;
320-
pendingSources.add(pendingSource);
321-
}
322-
317+
if (pendingSources.size > 0) {
323318
return {
324319
initialResult: {
325320
...initialResult,
@@ -542,13 +537,10 @@ export class IncrementalPublisher {
542537
if (child.filtered) {
543538
continue;
544539
}
545-
const pendingSource = isStreamItemsRecord(child)
546-
? child.streamRecord
547-
: child;
548-
if (!pendingSource.pendingSent) {
549-
newPendingSources.add(pendingSource);
540+
const maybePendingSource = this._publish(child);
541+
if (maybePendingSource) {
542+
newPendingSources.add(maybePendingSource);
550543
}
551-
this._publish(child);
552544
}
553545
if (isStreamItemsRecord(subsequentResultRecord)) {
554546
if (subsequentResultRecord.isFinalRecord) {
@@ -655,14 +647,20 @@ export class IncrementalPublisher {
655647
return result;
656648
}
657649

658-
private _publish(subsequentResultRecord: SubsequentResultRecord): void {
650+
private _publish(
651+
subsequentResultRecord: SubsequentResultRecord,
652+
): DeferredFragmentRecord | StreamRecord | undefined {
659653
if (isStreamItemsRecord(subsequentResultRecord)) {
660654
if (subsequentResultRecord.isCompleted) {
661655
this._push(subsequentResultRecord);
662-
return;
656+
} else {
657+
this._introduce(subsequentResultRecord);
663658
}
664659

665-
this._introduce(subsequentResultRecord);
660+
const stream = subsequentResultRecord.streamRecord;
661+
if (!stream.pendingSent) {
662+
return stream;
663+
}
666664
return;
667665
}
668666

@@ -673,6 +671,12 @@ export class IncrementalPublisher {
673671
subsequentResultRecord.children.size > 0
674672
) {
675673
this._push(subsequentResultRecord);
674+
} else {
675+
return;
676+
}
677+
678+
if (!subsequentResultRecord.pendingSent) {
679+
return subsequentResultRecord;
676680
}
677681
}
678682

src/execution/__tests__/defer-test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const anotherNestedObject = new GraphQLObjectType({
6464

6565
const hero = {
6666
name: 'Luke',
67+
lastName: 'SkyWalker',
6768
id: 1,
6869
friends,
6970
nestedObject,
@@ -112,6 +113,7 @@ const heroType = new GraphQLObjectType({
112113
fields: {
113114
id: { type: GraphQLID },
114115
name: { type: GraphQLString },
116+
lastName: { type: GraphQLString },
115117
nonNullName: { type: new GraphQLNonNull(GraphQLString) },
116118
friends: {
117119
type: new GraphQLList(friendType),
@@ -566,6 +568,58 @@ describe('Execute: defer directive', () => {
566568
]);
567569
});
568570

571+
it('Separately emits defer fragments with different labels with varying subfields with superimposed masked defer', async () => {
572+
const document = parse(`
573+
query HeroNameQuery {
574+
... @defer(label: "DeferID") {
575+
hero {
576+
id
577+
}
578+
}
579+
... @defer(label: "DeferName") {
580+
hero {
581+
name
582+
lastName
583+
... @defer {
584+
lastName
585+
}
586+
}
587+
}
588+
}
589+
`);
590+
const result = await complete(document);
591+
expectJSON(result).toDeepEqual([
592+
{
593+
data: {},
594+
pending: [
595+
{ id: '0', path: [], label: 'DeferID' },
596+
{ id: '1', path: [], label: 'DeferName' },
597+
],
598+
hasNext: true,
599+
},
600+
{
601+
incremental: [
602+
{
603+
data: { hero: {} },
604+
id: '0',
605+
},
606+
{
607+
data: { id: '1' },
608+
id: '0',
609+
subPath: ['hero'],
610+
},
611+
{
612+
data: { name: 'Luke', lastName: 'SkyWalker' },
613+
id: '1',
614+
subPath: ['hero'],
615+
},
616+
],
617+
completed: [{ id: '0' }, { id: '1' }],
618+
hasNext: false,
619+
},
620+
]);
621+
});
622+
569623
it('Separately emits defer fragments with different labels with varying subfields that return promises', async () => {
570624
const document = parse(`
571625
query HeroNameQuery {

src/execution/buildFieldPlan.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { getBySet } from '../jsutils/getBySet.js';
2+
import { isSameSet } from '../jsutils/isSameSet.js';
3+
4+
import type { DeferUsage, FieldDetails } from './collectFields.js';
5+
6+
export type DeferUsageSet = ReadonlySet<DeferUsage>;
7+
8+
export interface FieldGroup {
9+
fields: ReadonlyArray<FieldDetails>;
10+
deferUsages?: DeferUsageSet | undefined;
11+
knownDeferUsages?: DeferUsageSet | undefined;
12+
}
13+
14+
export type GroupedFieldSet = Map<string, FieldGroup>;
15+
16+
export interface NewGroupedFieldSetDetails {
17+
groupedFieldSet: GroupedFieldSet;
18+
shouldInitiateDefer: boolean;
19+
}
20+
21+
export function buildFieldPlan(
22+
fields: Map<string, ReadonlyArray<FieldDetails>>,
23+
parentDeferUsages: DeferUsageSet = new Set<DeferUsage>(),
24+
knownDeferUsages: DeferUsageSet = new Set<DeferUsage>(),
25+
): {
26+
groupedFieldSet: GroupedFieldSet;
27+
newGroupedFieldSetDetailsMap: Map<DeferUsageSet, NewGroupedFieldSetDetails>;
28+
newDeferUsages: ReadonlyArray<DeferUsage>;
29+
} {
30+
const newDeferUsages: Set<DeferUsage> = new Set<DeferUsage>();
31+
const newKnownDeferUsages = new Set<DeferUsage>(knownDeferUsages);
32+
33+
const groupedFieldSet = new Map<
34+
string,
35+
{
36+
fields: Array<FieldDetails>;
37+
deferUsages: DeferUsageSet;
38+
knownDeferUsages: DeferUsageSet;
39+
}
40+
>();
41+
42+
const newGroupedFieldSetDetailsMap = new Map<
43+
DeferUsageSet,
44+
{
45+
groupedFieldSet: Map<
46+
string,
47+
{
48+
fields: Array<FieldDetails>;
49+
deferUsages: DeferUsageSet;
50+
knownDeferUsages: DeferUsageSet;
51+
}
52+
>;
53+
shouldInitiateDefer: boolean;
54+
}
55+
>();
56+
57+
const map = new Map<
58+
string,
59+
{
60+
deferUsageSet: DeferUsageSet;
61+
fieldDetailsList: ReadonlyArray<FieldDetails>;
62+
}
63+
>();
64+
65+
for (const [responseKey, fieldDetailsList] of fields) {
66+
const deferUsageSet = new Set<DeferUsage>();
67+
let inOriginalResult = false;
68+
for (const fieldDetails of fieldDetailsList) {
69+
const deferUsage = fieldDetails.deferUsage;
70+
if (deferUsage === undefined) {
71+
inOriginalResult = true;
72+
continue;
73+
}
74+
deferUsageSet.add(deferUsage);
75+
if (!knownDeferUsages.has(deferUsage)) {
76+
newDeferUsages.add(deferUsage);
77+
newKnownDeferUsages.add(deferUsage);
78+
}
79+
}
80+
if (inOriginalResult) {
81+
deferUsageSet.clear();
82+
} else {
83+
deferUsageSet.forEach((deferUsage) => {
84+
const ancestors = getAncestors(deferUsage);
85+
for (const ancestor of ancestors) {
86+
if (deferUsageSet.has(ancestor)) {
87+
deferUsageSet.delete(deferUsage);
88+
}
89+
}
90+
});
91+
}
92+
map.set(responseKey, { deferUsageSet, fieldDetailsList });
93+
}
94+
95+
for (const [responseKey, { deferUsageSet, fieldDetailsList }] of map) {
96+
if (isSameSet(deferUsageSet, parentDeferUsages)) {
97+
let fieldGroup = groupedFieldSet.get(responseKey);
98+
if (fieldGroup === undefined) {
99+
fieldGroup = {
100+
fields: [],
101+
deferUsages: deferUsageSet,
102+
knownDeferUsages: newKnownDeferUsages,
103+
};
104+
groupedFieldSet.set(responseKey, fieldGroup);
105+
}
106+
fieldGroup.fields.push(...fieldDetailsList);
107+
continue;
108+
}
109+
110+
let newGroupedFieldSetDetails = getBySet(
111+
newGroupedFieldSetDetailsMap,
112+
deferUsageSet,
113+
);
114+
let newGroupedFieldSet;
115+
if (newGroupedFieldSetDetails === undefined) {
116+
newGroupedFieldSet = new Map<
117+
string,
118+
{
119+
fields: Array<FieldDetails>;
120+
deferUsages: DeferUsageSet;
121+
knownDeferUsages: DeferUsageSet;
122+
}
123+
>();
124+
125+
newGroupedFieldSetDetails = {
126+
groupedFieldSet: newGroupedFieldSet,
127+
shouldInitiateDefer: Array.from(deferUsageSet).some(
128+
(deferUsage) => !parentDeferUsages.has(deferUsage),
129+
),
130+
};
131+
newGroupedFieldSetDetailsMap.set(
132+
deferUsageSet,
133+
newGroupedFieldSetDetails,
134+
);
135+
} else {
136+
newGroupedFieldSet = newGroupedFieldSetDetails.groupedFieldSet;
137+
}
138+
let fieldGroup = newGroupedFieldSet.get(responseKey);
139+
if (fieldGroup === undefined) {
140+
fieldGroup = {
141+
fields: [],
142+
deferUsages: deferUsageSet,
143+
knownDeferUsages: newKnownDeferUsages,
144+
};
145+
newGroupedFieldSet.set(responseKey, fieldGroup);
146+
}
147+
fieldGroup.fields.push(...fieldDetailsList);
148+
}
149+
150+
return {
151+
groupedFieldSet,
152+
newGroupedFieldSetDetailsMap,
153+
newDeferUsages: Array.from(newDeferUsages),
154+
};
155+
}
156+
157+
function getAncestors(deferUsage: DeferUsage): ReadonlyArray<DeferUsage> {
158+
const ancestors: Array<DeferUsage> = [];
159+
let parentDeferUsage: DeferUsage | undefined = deferUsage.parentDeferUsage;
160+
while (parentDeferUsage !== undefined) {
161+
ancestors.unshift(parentDeferUsage);
162+
parentDeferUsage = parentDeferUsage.parentDeferUsage;
163+
}
164+
return ancestors;
165+
}

0 commit comments

Comments
 (0)