Skip to content

Commit 74f2af7

Browse files
committed
fix(incremental): properly initiate nested deferred grouped field sets
when early execution is disabled, deferred grouped field sets should start immediately if and only if one of their deferred fragments is released as pending see: graphql/defer-stream-wg#84 (reply in thread)
1 parent 605bd1f commit 74f2af7

File tree

4 files changed

+194
-22
lines changed

4 files changed

+194
-22
lines changed

src/execution/IncrementalGraph.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { GraphQLError } from '../error/GraphQLError.js';
88
import type {
99
DeferredFragmentRecord,
1010
DeferredGroupedFieldSetRecord,
11+
DeferredGroupedFieldSetResult,
1112
IncrementalDataRecord,
1213
IncrementalDataRecordResult,
1314
ReconcilableDeferredGroupedFieldSetResult,
@@ -237,6 +238,7 @@ export class IncrementalGraph {
237238
for (const node of newPendingNodes) {
238239
if (isDeferredFragmentNode(node)) {
239240
if (node.deferredGroupedFieldSetRecords.size > 0) {
241+
node.deferredFragmentRecord.setAsPending();
240242
for (const deferredGroupedFieldSetRecord of node.deferredGroupedFieldSetRecords) {
241243
if (!this._hasPendingFragment(deferredGroupedFieldSetRecord)) {
242244
this._onDeferredGroupedFieldSet(deferredGroupedFieldSetRecord);
@@ -313,12 +315,9 @@ export class IncrementalGraph {
313315
private _onDeferredGroupedFieldSet(
314316
deferredGroupedFieldSetRecord: DeferredGroupedFieldSetRecord,
315317
): void {
316-
const deferredGroupedFieldSetResult = deferredGroupedFieldSetRecord.result;
317-
const result =
318-
deferredGroupedFieldSetResult instanceof BoxedPromiseOrValue
319-
? deferredGroupedFieldSetResult.value
320-
: deferredGroupedFieldSetResult().value;
321-
318+
const result = (
319+
deferredGroupedFieldSetRecord.result as BoxedPromiseOrValue<DeferredGroupedFieldSetResult>
320+
).value;
322321
if (isPromise(result)) {
323322
// eslint-disable-next-line @typescript-eslint/no-floating-promises
324323
result.then((resolved) => this._enqueue(resolved));

src/execution/__tests__/defer-test.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { expect } from 'chai';
1+
import { assert, expect } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON.js';
55
import { expectPromise } from '../../__testUtils__/expectPromise.js';
66
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
77

8+
import { promiseWithResolvers } from '../../jsutils/promiseWithResolvers.js';
9+
810
import type { DocumentNode } from '../../language/ast.js';
911
import { parse } from '../../language/parser.js';
1012

@@ -856,6 +858,134 @@ describe('Execute: defer directive', () => {
856858
]);
857859
});
858860

861+
it('Initiates all deferred grouped field sets immediately if and only if they have been released as pending', async () => {
862+
const document = parse(`
863+
query {
864+
... @defer {
865+
a {
866+
... @defer {
867+
b {
868+
c { d }
869+
}
870+
}
871+
}
872+
}
873+
... @defer {
874+
a {
875+
someField
876+
... @defer {
877+
b {
878+
e { f }
879+
}
880+
}
881+
}
882+
}
883+
}
884+
`);
885+
886+
const { promise: slowFieldPromise, resolve: resolveSlowField } =
887+
promiseWithResolvers();
888+
let cResolverCalled = false;
889+
let eResolverCalled = false;
890+
const executeResult = experimentalExecuteIncrementally({
891+
schema,
892+
document,
893+
rootValue: {
894+
a: {
895+
someField: slowFieldPromise,
896+
b: {
897+
c: () => {
898+
cResolverCalled = true;
899+
return { d: 'd' };
900+
},
901+
e: () => {
902+
eResolverCalled = true;
903+
return { f: 'f' };
904+
},
905+
},
906+
},
907+
},
908+
enableEarlyExecution: false,
909+
});
910+
911+
assert('initialResult' in executeResult);
912+
913+
const result1 = executeResult.initialResult;
914+
expectJSON(result1).toDeepEqual({
915+
data: {},
916+
pending: [
917+
{ id: '0', path: [] },
918+
{ id: '1', path: [] },
919+
],
920+
hasNext: true,
921+
});
922+
923+
const iterator = executeResult.subsequentResults[Symbol.asyncIterator]();
924+
925+
expect(cResolverCalled).to.equal(false);
926+
expect(eResolverCalled).to.equal(false);
927+
928+
const result2 = await iterator.next();
929+
expectJSON(result2).toDeepEqual({
930+
value: {
931+
pending: [{ id: '2', path: ['a'] }],
932+
incremental: [
933+
{
934+
data: { a: {} },
935+
id: '0',
936+
},
937+
{
938+
data: { b: {} },
939+
id: '2',
940+
},
941+
{
942+
data: { c: { d: 'd' } },
943+
id: '2',
944+
subPath: ['b'],
945+
},
946+
],
947+
completed: [{ id: '0' }, { id: '2' }],
948+
hasNext: true,
949+
},
950+
done: false,
951+
});
952+
953+
expect(cResolverCalled).to.equal(true);
954+
expect(eResolverCalled).to.equal(false);
955+
956+
resolveSlowField('someField');
957+
958+
const result3 = await iterator.next();
959+
expectJSON(result3).toDeepEqual({
960+
value: {
961+
pending: [{ id: '3', path: ['a'] }],
962+
incremental: [
963+
{
964+
data: { someField: 'someField' },
965+
id: '1',
966+
subPath: ['a'],
967+
},
968+
{
969+
data: { e: { f: 'f' } },
970+
id: '3',
971+
subPath: ['b'],
972+
},
973+
],
974+
completed: [{ id: '1' }, { id: '3' }],
975+
hasNext: false,
976+
},
977+
done: false,
978+
});
979+
980+
expect(eResolverCalled).to.equal(true);
981+
982+
const result4 = await iterator.next();
983+
expectJSON(result4).toDeepEqual({
984+
value: undefined,
985+
done: true,
986+
});
987+
});
988+
859989
it('Can deduplicate multiple defers on the same object', async () => {
860990
const document = parse(`
861991
query {

src/execution/execute.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import { buildIncrementalResponse } from './IncrementalPublisher.js';
6363
import { mapAsyncIterable } from './mapAsyncIterable.js';
6464
import type {
6565
CancellableStreamRecord,
66-
DeferredFragmentRecord,
6766
DeferredGroupedFieldSetRecord,
6867
DeferredGroupedFieldSetResult,
6968
ExecutionResult,
@@ -73,6 +72,7 @@ import type {
7372
StreamItemResult,
7473
StreamRecord,
7574
} from './types.js';
75+
import { DeferredFragmentRecord } from './types.js';
7676
import {
7777
getArgumentValues,
7878
getDirectiveValues,
@@ -1676,11 +1676,11 @@ function addNewDeferredFragments(
16761676
: deferredFragmentRecordFromDeferUsage(parentDeferUsage, newDeferMap);
16771677

16781678
// Instantiate the new record.
1679-
const deferredFragmentRecord: DeferredFragmentRecord = {
1679+
const deferredFragmentRecord = new DeferredFragmentRecord(
16801680
path,
1681-
label: newDeferUsage.label,
1681+
newDeferUsage.label,
16821682
parent,
1683-
};
1683+
);
16841684

16851685
// Update the map.
16861686
newDeferMap.set(newDeferUsage, deferredFragmentRecord);
@@ -2114,16 +2114,33 @@ function executeDeferredGroupedFieldSets(
21142114
deferMap,
21152115
);
21162116

2117-
const shouldDeferThisDeferUsageSet = shouldDefer(
2118-
parentDeferUsages,
2119-
deferUsageSet,
2120-
);
2121-
2122-
deferredGroupedFieldSetRecord.result = shouldDeferThisDeferUsageSet
2123-
? exeContext.enableEarlyExecution
2124-
? new BoxedPromiseOrValue(Promise.resolve().then(executor))
2125-
: () => new BoxedPromiseOrValue(executor())
2126-
: new BoxedPromiseOrValue(executor());
2117+
if (exeContext.enableEarlyExecution) {
2118+
deferredGroupedFieldSetRecord.result = new BoxedPromiseOrValue(
2119+
shouldDefer(parentDeferUsages, deferUsageSet)
2120+
? Promise.resolve().then(executor)
2121+
: executor(),
2122+
);
2123+
} else if (
2124+
deferredFragmentRecords.some(
2125+
(deferredFragmentRecord) => deferredFragmentRecord.pending,
2126+
)
2127+
) {
2128+
deferredGroupedFieldSetRecord.result = new BoxedPromiseOrValue(
2129+
executor(),
2130+
);
2131+
} else {
2132+
deferredGroupedFieldSetRecord.result = () =>
2133+
new BoxedPromiseOrValue(executor());
2134+
const resolveThunk = () => {
2135+
const maybeThunk = deferredGroupedFieldSetRecord.result;
2136+
if (!(maybeThunk instanceof BoxedPromiseOrValue)) {
2137+
deferredGroupedFieldSetRecord.result = maybeThunk();
2138+
}
2139+
};
2140+
for (const deferredFragmentRecord of deferredFragmentRecords) {
2141+
deferredFragmentRecord.onPending(resolveThunk);
2142+
}
2143+
}
21272144

21282145
newDeferredGroupedFieldSetRecords.push(deferredGroupedFieldSetRecord);
21292146
}

src/execution/types.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,36 @@ export interface DeferredGroupedFieldSetRecord {
214214

215215
export type SubsequentResultRecord = DeferredFragmentRecord | StreamRecord;
216216

217-
export interface DeferredFragmentRecord {
217+
/** @internal */
218+
export class DeferredFragmentRecord {
218219
path: Path | undefined;
219220
label: string | undefined;
220221
parent: DeferredFragmentRecord | undefined;
222+
fns: Array<() => void>;
223+
pending: boolean;
224+
225+
constructor(
226+
path: Path | undefined,
227+
label: string | undefined,
228+
parent: DeferredFragmentRecord | undefined,
229+
) {
230+
this.path = path;
231+
this.label = label;
232+
this.parent = parent;
233+
this.pending = false;
234+
this.fns = [];
235+
}
236+
237+
onPending(fn: () => void): void {
238+
this.fns.push(fn);
239+
}
240+
241+
setAsPending(): void {
242+
this.pending = true;
243+
for (const fn of this.fns) {
244+
fn();
245+
}
246+
}
221247
}
222248

223249
export interface StreamItemResult {

0 commit comments

Comments
 (0)