Skip to content

Commit 1a4d602

Browse files
committed
add helpers
1 parent 1e6e4b0 commit 1a4d602

File tree

6 files changed

+325
-31
lines changed

6 files changed

+325
-31
lines changed

src/execution/IncrementalPublisher.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,12 +615,16 @@ export class IncrementalPublisher {
615615
}
616616

617617
this._introduce(subsequentResultRecord);
618+
subsequentResultRecord.publish();
618619
return;
619620
}
620621

621622
if (subsequentResultRecord._pending.size === 0) {
622623
this._push(subsequentResultRecord);
623624
} else {
625+
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
626+
deferredGroupedFieldSetRecord.publish();
627+
}
624628
this._introduce(subsequentResultRecord);
625629
}
626630
}
@@ -701,33 +705,56 @@ function isStreamItemsRecord(
701705
export class InitialResultRecord {
702706
errors: Array<GraphQLError>;
703707
children: Set<SubsequentResultRecord>;
708+
deferPriority: number;
709+
streamPriority: number;
710+
published: true;
704711
constructor() {
705712
this.errors = [];
706713
this.children = new Set();
714+
this.deferPriority = 0;
715+
this.streamPriority = 0;
716+
this.published = true;
707717
}
708718
}
709719

710720
/** @internal */
711721
export class DeferredGroupedFieldSetRecord {
712722
path: ReadonlyArray<string | number>;
723+
deferPriority: number;
724+
streamPriority: number;
713725
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
714726
groupedFieldSet: GroupedFieldSet;
715727
shouldInitiateDefer: boolean;
716728
errors: Array<GraphQLError>;
717729
data: ObjMap<unknown> | undefined;
730+
published: true | Promise<void>;
731+
publish: () => void;
718732
sent: boolean;
719733

720734
constructor(opts: {
721735
path: Path | undefined;
736+
deferPriority: number;
737+
streamPriority: number;
722738
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
723739
groupedFieldSet: GroupedFieldSet;
724740
shouldInitiateDefer: boolean;
725741
}) {
726742
this.path = pathToArray(opts.path);
743+
this.deferPriority = opts.deferPriority;
744+
this.streamPriority = opts.streamPriority;
727745
this.deferredFragmentRecords = opts.deferredFragmentRecords;
728746
this.groupedFieldSet = opts.groupedFieldSet;
729747
this.shouldInitiateDefer = opts.shouldInitiateDefer;
730748
this.errors = [];
749+
// promiseWithResolvers uses void only as a generic type parameter
750+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
751+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
752+
const { promise: published, resolve } = promiseWithResolvers<void>();
753+
this.published = published;
754+
this.publish = () => {
755+
resolve();
756+
this.published = true;
757+
};
731758
this.sent = false;
732759
}
733760
}
@@ -778,21 +805,43 @@ export class StreamItemsRecord {
778805
errors: Array<GraphQLError>;
779806
streamRecord: StreamRecord;
780807
path: ReadonlyArray<string | number>;
808+
deferPriority: number;
809+
streamPriority: number;
781810
items: Array<unknown>;
782811
children: Set<SubsequentResultRecord>;
783812
isFinalRecord?: boolean;
784813
isCompletedAsyncIterator?: boolean;
785814
isCompleted: boolean;
786815
filtered: boolean;
816+
published: true | Promise<void>;
817+
publish: () => void;
818+
sent: boolean;
787819

788-
constructor(opts: { streamRecord: StreamRecord; path: Path | undefined }) {
820+
constructor(opts: {
821+
streamRecord: StreamRecord;
822+
path: Path | undefined;
823+
deferPriority: number;
824+
streamPriority: number;
825+
}) {
789826
this.streamRecord = opts.streamRecord;
790827
this.path = pathToArray(opts.path);
828+
this.deferPriority = opts.deferPriority;
829+
this.streamPriority = opts.streamPriority;
791830
this.children = new Set();
792831
this.errors = [];
793832
this.isCompleted = false;
794833
this.filtered = false;
795834
this.items = [];
835+
// promiseWithResolvers uses void only as a generic type parameter
836+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
837+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
838+
const { promise: published, resolve } = promiseWithResolvers<void>();
839+
this.published = published;
840+
this.publish = () => {
841+
resolve();
842+
this.published = true;
843+
};
844+
this.sent = false;
796845
}
797846
}
798847

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