Skip to content

Commit e62ca02

Browse files
committed
add helpers
1 parent 6b6f344 commit e62ca02

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
@@ -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+
priority: number;
709+
deferPriority: number;
710+
published: true;
704711
constructor() {
705712
this.errors = [];
706713
this.children = new Set();
714+
this.priority = 0;
715+
this.deferPriority = 0;
716+
this.published = true;
707717
}
708718
}
709719

710720
/** @internal */
711721
export class DeferredGroupedFieldSetRecord {
712722
path: ReadonlyArray<string | number>;
723+
priority: number;
724+
deferPriority: 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+
priority: number;
737+
deferPriority: number;
722738
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
723739
groupedFieldSet: GroupedFieldSet;
724740
shouldInitiateDefer: boolean;
725741
}) {
726742
this.path = pathToArray(opts.path);
743+
this.priority = opts.priority;
744+
this.deferPriority = opts.deferPriority;
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,42 @@ export class StreamItemsRecord {
778805
errors: Array<GraphQLError>;
779806
streamRecord: StreamRecord;
780807
path: ReadonlyArray<string | number>;
808+
priority: number;
809+
deferPriority: 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+
priority: number;
824+
}) {
789825
this.streamRecord = opts.streamRecord;
790826
this.path = pathToArray(opts.path);
827+
this.priority = opts.priority;
828+
this.deferPriority = 0;
791829
this.children = new Set();
792830
this.errors = [];
793831
this.isCompleted = false;
794832
this.filtered = false;
795833
this.items = [];
834+
// promiseWithResolvers uses void only as a generic type parameter
835+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
836+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
837+
const { promise: published, resolve } = promiseWithResolvers<void>();
838+
this.published = published;
839+
this.publish = () => {
840+
resolve();
841+
this.published = true;
842+
};
843+
this.sent = false;
796844
}
797845
}
798846

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: [{ 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)