Skip to content

Commit 72e65aa

Browse files
committed
add helpers
1 parent b12dcff commit 72e65aa

File tree

6 files changed

+337
-37
lines changed

6 files changed

+337
-37
lines changed

src/execution/IncrementalPublisher.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,12 +663,16 @@ export class IncrementalPublisher {
663663
}
664664

665665
this._introduce(subsequentResultRecord);
666+
subsequentResultRecord.publish();
666667
return;
667668
}
668669

669670
if (subsequentResultRecord._pending.size === 0) {
670671
this._push(subsequentResultRecord);
671672
} else {
673+
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
674+
deferredGroupedFieldSetRecord.publish();
675+
}
672676
this._introduce(subsequentResultRecord);
673677
}
674678
}
@@ -749,33 +753,56 @@ function isStreamItemsRecord(
749753
export class InitialResultRecord {
750754
errors: Array<GraphQLError>;
751755
children: Set<SubsequentResultRecord>;
756+
priority: number;
757+
deferPriority: number;
758+
published: true;
752759
constructor() {
753760
this.errors = [];
754761
this.children = new Set();
762+
this.priority = 0;
763+
this.deferPriority = 0;
764+
this.published = true;
755765
}
756766
}
757767

758768
/** @internal */
759769
export class DeferredGroupedFieldSetRecord {
760770
path: ReadonlyArray<string | number>;
771+
priority: number;
772+
deferPriority: number;
761773
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
762774
groupedFieldSet: GroupedFieldSet;
763775
shouldInitiateDefer: boolean;
764776
errors: Array<GraphQLError>;
765777
data: ObjMap<unknown> | undefined;
778+
published: true | Promise<void>;
779+
publish: () => void;
766780
sent: boolean;
767781

768782
constructor(opts: {
769783
path: Path | undefined;
784+
priority: number;
785+
deferPriority: number;
770786
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
771787
groupedFieldSet: GroupedFieldSet;
772788
shouldInitiateDefer: boolean;
773789
}) {
774790
this.path = pathToArray(opts.path);
791+
this.priority = opts.priority;
792+
this.deferPriority = opts.deferPriority;
775793
this.deferredFragmentRecords = opts.deferredFragmentRecords;
776794
this.groupedFieldSet = opts.groupedFieldSet;
777795
this.shouldInitiateDefer = opts.shouldInitiateDefer;
778796
this.errors = [];
797+
// promiseWithResolvers uses void only as a generic type parameter
798+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
799+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
800+
const { promise: published, resolve } = promiseWithResolvers<void>();
801+
this.published = published;
802+
this.publish = () => {
803+
resolve();
804+
this.published = true;
805+
};
779806
this.sent = false;
780807
}
781808
}
@@ -828,20 +855,41 @@ export class StreamItemsRecord {
828855
errors: Array<GraphQLError>;
829856
streamRecord: StreamRecord;
830857
path: ReadonlyArray<string | number>;
858+
priority: number;
859+
deferPriority: number;
831860
items: Array<unknown> | undefined;
832861
children: Set<SubsequentResultRecord>;
833862
isFinalRecord?: boolean;
834863
isCompletedAsyncIterator?: boolean;
835864
isCompleted: boolean;
836865
filtered: boolean;
866+
published: true | Promise<void>;
867+
publish: () => void;
868+
sent: boolean;
837869

838-
constructor(opts: { streamRecord: StreamRecord; path: Path | undefined }) {
870+
constructor(opts: {
871+
streamRecord: StreamRecord;
872+
path: Path | undefined;
873+
priority: number;
874+
}) {
839875
this.streamRecord = opts.streamRecord;
840876
this.path = pathToArray(opts.path);
877+
this.priority = opts.priority;
878+
this.deferPriority = 0;
841879
this.children = new Set();
842880
this.errors = [];
843881
this.isCompleted = false;
844882
this.filtered = false;
883+
// promiseWithResolvers uses void only as a generic type parameter
884+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
885+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
886+
const { promise: published, resolve } = promiseWithResolvers<void>();
887+
this.published = published;
888+
this.publish = () => {
889+
resolve();
890+
this.published = true;
891+
};
892+
this.sent = false;
845893
}
846894
}
847895

src/execution/__tests__/defer-test.ts

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
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 { isPromise } from '../../jsutils/isPromise.js';
9+
810
import type { DocumentNode } from '../../language/ast.js';
11+
import { Kind } from '../../language/kinds.js';
912
import { parse } from '../../language/parser.js';
1013

14+
import type { FieldDetails } from '../../type/definition.js';
1115
import {
1216
GraphQLList,
1317
GraphQLNonNull,
@@ -216,6 +220,174 @@ describe('Execute: defer directive', () => {
216220
},
217221
});
218222
});
223+
it('Can provides correct info about deferred execution state when resolver could defer', async () => {
224+
let fieldDetails: ReadonlyArray<FieldDetails> | undefined;
225+
let deferPriority;
226+
let published;
227+
let resumed;
228+
229+
const SomeType = new GraphQLObjectType({
230+
name: 'SomeType',
231+
fields: {
232+
someField: {
233+
type: GraphQLString,
234+
resolve: () => Promise.resolve('someField'),
235+
},
236+
deferredField: {
237+
type: GraphQLString,
238+
resolve: async (_parent, _args, _context, info) => {
239+
fieldDetails = info.fieldDetails;
240+
deferPriority = info.deferPriority;
241+
published = info.published;
242+
await published;
243+
resumed = true;
244+
},
245+
},
246+
},
247+
});
248+
249+
const someSchema = new GraphQLSchema({ query: SomeType });
250+
251+
const document = parse(`
252+
query {
253+
someField
254+
... @defer {
255+
deferredField
256+
}
257+
}
258+
`);
259+
260+
const operation = document.definitions[0];
261+
assert(operation.kind === Kind.OPERATION_DEFINITION);
262+
const fragment = operation.selectionSet.selections[1];
263+
assert(fragment.kind === Kind.INLINE_FRAGMENT);
264+
const field = fragment.selectionSet.selections[0];
265+
266+
const result = experimentalExecuteIncrementally({
267+
schema: someSchema,
268+
document,
269+
});
270+
271+
expect(fieldDetails).to.equal(undefined);
272+
expect(deferPriority).to.equal(undefined);
273+
expect(published).to.equal(undefined);
274+
expect(resumed).to.equal(undefined);
275+
276+
const initialPayload = await result;
277+
assert('initialResult' in initialPayload);
278+
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
279+
await iterator.next();
280+
281+
assert(fieldDetails !== undefined);
282+
expect(fieldDetails[0].node).to.equal(field);
283+
expect(fieldDetails[0].target?.deferPriority).to.equal(1);
284+
expect(deferPriority).to.equal(1);
285+
expect(isPromise(published)).to.equal(true);
286+
expect(resumed).to.equal(true);
287+
});
288+
it('Can provides correct info about deferred execution state when deferred field is masked by non-deferred field', async () => {
289+
let fieldDetails: ReadonlyArray<FieldDetails> | undefined;
290+
let deferPriority;
291+
let published;
292+
293+
const SomeType = new GraphQLObjectType({
294+
name: 'SomeType',
295+
fields: {
296+
someField: {
297+
type: GraphQLString,
298+
resolve: (_parent, _args, _context, info) => {
299+
fieldDetails = info.fieldDetails;
300+
deferPriority = info.deferPriority;
301+
published = info.published;
302+
return 'someField';
303+
},
304+
},
305+
},
306+
});
307+
308+
const someSchema = new GraphQLSchema({ query: SomeType });
309+
310+
const document = parse(`
311+
query {
312+
someField
313+
... @defer {
314+
someField
315+
}
316+
}
317+
`);
318+
319+
const operation = document.definitions[0];
320+
assert(operation.kind === Kind.OPERATION_DEFINITION);
321+
const node1 = operation.selectionSet.selections[0];
322+
const fragment = operation.selectionSet.selections[1];
323+
assert(fragment.kind === Kind.INLINE_FRAGMENT);
324+
const node2 = fragment.selectionSet.selections[0];
325+
326+
const result = experimentalExecuteIncrementally({
327+
schema: someSchema,
328+
document,
329+
});
330+
331+
const initialPayload = await result;
332+
assert('initialResult' in initialPayload);
333+
expect(initialPayload.initialResult).to.deep.equal({
334+
data: {
335+
someField: 'someField',
336+
},
337+
pending: [{ id: '0', path: [] }],
338+
hasNext: true,
339+
});
340+
341+
assert(fieldDetails !== undefined);
342+
expect(fieldDetails[0].node).to.equal(node1);
343+
expect(fieldDetails[0].target).to.equal(undefined);
344+
expect(fieldDetails[1].node).to.equal(node2);
345+
expect(fieldDetails[1].target?.deferPriority).to.equal(1);
346+
expect(deferPriority).to.equal(0);
347+
expect(published).to.equal(true);
348+
});
349+
it('Can provides correct info about deferred execution state when resolver need not defer', async () => {
350+
let deferPriority;
351+
let published;
352+
const SomeType = new GraphQLObjectType({
353+
name: 'SomeType',
354+
fields: {
355+
deferredField: {
356+
type: GraphQLString,
357+
resolve: (_parent, _args, _context, info) => {
358+
deferPriority = info.deferPriority;
359+
published = info.published;
360+
},
361+
},
362+
},
363+
});
364+
365+
const someSchema = new GraphQLSchema({ query: SomeType });
366+
367+
const document = parse(`
368+
query {
369+
... @defer {
370+
deferredField
371+
}
372+
}
373+
`);
374+
375+
const result = experimentalExecuteIncrementally({
376+
schema: someSchema,
377+
document,
378+
});
379+
380+
expect(deferPriority).to.equal(undefined);
381+
expect(published).to.equal(undefined);
382+
383+
const initialPayload = await result;
384+
assert('initialResult' in initialPayload);
385+
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
386+
await iterator.next();
387+
388+
expect(deferPriority).to.equal(1);
389+
expect(published).to.equal(true);
390+
});
219391
it('Does not disable defer with null if argument', async () => {
220392
const document = parse(`
221393
query HeroNameQuery($shouldDefer: Boolean) {

src/execution/__tests__/executor-test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { inspect } from '../../jsutils/inspect.js';
99
import { Kind } from '../../language/kinds.js';
1010
import { parse } from '../../language/parser.js';
1111

12+
import type { GraphQLResolveInfo } from '../../type/definition.js';
1213
import {
1314
GraphQLInterfaceType,
1415
GraphQLList,
@@ -191,7 +192,7 @@ describe('Execute: Handles basic execution tasks', () => {
191192
});
192193

193194
it('provides info about current execution state', () => {
194-
let resolvedInfo;
195+
let resolvedInfo: GraphQLResolveInfo | undefined;
195196
const testType = new GraphQLObjectType({
196197
name: 'Test',
197198
fields: {
@@ -213,7 +214,7 @@ describe('Execute: Handles basic execution tasks', () => {
213214

214215
expect(resolvedInfo).to.have.all.keys(
215216
'fieldName',
216-
'fieldNodes',
217+
'fieldDetails',
217218
'returnType',
218219
'parentType',
219220
'path',
@@ -222,6 +223,9 @@ describe('Execute: Handles basic execution tasks', () => {
222223
'rootValue',
223224
'operation',
224225
'variableValues',
226+
'priority',
227+
'deferPriority',
228+
'published',
225229
);
226230

227231
const operation = document.definitions[0];
@@ -234,14 +238,24 @@ describe('Execute: Handles basic execution tasks', () => {
234238
schema,
235239
rootValue,
236240
operation,
241+
priority: 0,
242+
deferPriority: 0,
243+
published: true,
237244
});
238245

239-
const field = operation.selectionSet.selections[0];
240246
expect(resolvedInfo).to.deep.include({
241-
fieldNodes: [field],
242247
path: { prev: undefined, key: 'result', typename: 'Test' },
243248
variableValues: { var: 'abc' },
244249
});
250+
251+
const fieldDetails = resolvedInfo?.fieldDetails;
252+
assert(fieldDetails !== undefined);
253+
254+
const field = operation.selectionSet.selections[0];
255+
expect(fieldDetails[0]).to.deep.include({
256+
node: field,
257+
target: undefined,
258+
});
245259
});
246260

247261
it('populates path correctly with complex types', () => {

0 commit comments

Comments
 (0)