Skip to content

Commit ad7439b

Browse files
committed
add helpers
1 parent 373870e commit ad7439b

File tree

7 files changed

+359
-53
lines changed

7 files changed

+359
-53
lines changed

src/execution/IncrementalPublisher.ts

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

11+
import type { DeferUsage } from '../type/definition.js';
12+
1113
import type {
12-
DeferUsage,
1314
DeferUsageSet,
1415
GroupedFieldSet,
1516
GroupedFieldSetDetails,
@@ -303,12 +304,26 @@ export class IncrementalPublisher {
303304
newGroupedFieldSetDeferUsages,
304305
newDeferMap,
305306
);
306-
const deferredGroupedFieldSetRecord = new DeferredGroupedFieldSetRecord({
307-
path,
308-
deferredFragmentRecords,
309-
groupedFieldSet,
310-
shouldInitiateDefer,
311-
});
307+
const deferredGroupedFieldSetRecord =
308+
incrementalDataRecord === undefined
309+
? new DeferredGroupedFieldSetRecord({
310+
path,
311+
deferredFragmentRecords,
312+
groupedFieldSet,
313+
deferPriority: 1,
314+
streamPriority: 0,
315+
shouldInitiateDefer,
316+
})
317+
: new DeferredGroupedFieldSetRecord({
318+
path,
319+
deferredFragmentRecords,
320+
groupedFieldSet,
321+
deferPriority: shouldInitiateDefer
322+
? incrementalDataRecord.deferPriority + 1
323+
: incrementalDataRecord.deferPriority,
324+
streamPriority: incrementalDataRecord.streamPriority,
325+
shouldInitiateDefer,
326+
});
312327
for (const deferredFragmentRecord of deferredFragmentRecords) {
313328
deferredFragmentRecord._pending.add(deferredGroupedFieldSetRecord);
314329
deferredFragmentRecord.deferredGroupedFieldSetRecords.add(
@@ -342,11 +357,22 @@ export class IncrementalPublisher {
342357
incrementalDataRecord: IncrementalDataRecord | undefined,
343358
): StreamItemsRecord {
344359
const parents = getSubsequentResultRecords(incrementalDataRecord);
345-
const streamItemsRecord = new StreamItemsRecord({
346-
streamRecord,
347-
path,
348-
parents,
349-
});
360+
const streamItemsRecord =
361+
incrementalDataRecord === undefined
362+
? new StreamItemsRecord({
363+
streamRecord,
364+
path,
365+
deferPriority: 0,
366+
streamPriority: 1,
367+
parents,
368+
})
369+
: new StreamItemsRecord({
370+
streamRecord,
371+
path,
372+
deferPriority: incrementalDataRecord.deferPriority,
373+
streamPriority: incrementalDataRecord.streamPriority + 1,
374+
parents,
375+
});
350376

351377
if (parents === undefined) {
352378
this._initialResult.children.add(streamItemsRecord);
@@ -672,13 +698,17 @@ export class IncrementalPublisher {
672698

673699
if (isStreamItemsRecord(subsequentResultRecord)) {
674700
this._introduce(subsequentResultRecord);
701+
subsequentResultRecord.publish();
675702
return;
676703
}
677704

678705
if (subsequentResultRecord._pending.size === 0) {
679706
subsequentResultRecord.isCompleted = true;
680707
this._push(subsequentResultRecord);
681708
} else {
709+
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
710+
deferredGroupedFieldSetRecord.publish();
711+
}
682712
this._introduce(subsequentResultRecord);
683713
}
684714
}
@@ -748,24 +778,41 @@ export class IncrementalPublisher {
748778
/** @internal */
749779
export class DeferredGroupedFieldSetRecord {
750780
path: ReadonlyArray<string | number>;
781+
deferPriority: number;
782+
streamPriority: number;
751783
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
752784
groupedFieldSet: GroupedFieldSet;
753785
shouldInitiateDefer: boolean;
754786
errors: Array<GraphQLError>;
755787
data: ObjMap<unknown> | undefined;
788+
published: true | Promise<void>;
789+
publish: () => void;
756790
sent: boolean;
757791

758792
constructor(opts: {
759793
path: Path | undefined;
794+
deferPriority: number;
795+
streamPriority: number;
760796
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
761797
groupedFieldSet: GroupedFieldSet;
762798
shouldInitiateDefer: boolean;
763799
}) {
764800
this.path = pathToArray(opts.path);
801+
this.deferPriority = opts.deferPriority;
802+
this.streamPriority = opts.streamPriority;
765803
this.deferredFragmentRecords = opts.deferredFragmentRecords;
766804
this.groupedFieldSet = opts.groupedFieldSet;
767805
this.shouldInitiateDefer = opts.shouldInitiateDefer;
768806
this.errors = [];
807+
// promiseWithResolvers uses void only as a generic type parameter
808+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
809+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
810+
const { promise: published, resolve } = promiseWithResolvers<void>();
811+
this.published = published;
812+
this.publish = () => {
813+
resolve();
814+
this.published = true;
815+
};
769816
this.sent = false;
770817
}
771818
}
@@ -822,26 +869,43 @@ export class StreamItemsRecord {
822869
errors: Array<GraphQLError>;
823870
streamRecord: StreamRecord;
824871
path: ReadonlyArray<string | number>;
872+
deferPriority: number;
873+
streamPriority: number;
825874
items: Array<unknown>;
826875
parents: ReadonlyArray<SubsequentResultRecord> | undefined;
827876
children: Set<SubsequentResultRecord>;
828877
isFinalRecord?: boolean;
829878
isCompletedAsyncIterator?: boolean;
830879
isCompleted: boolean;
880+
published: true | Promise<void>;
881+
publish: () => void;
831882
sent: boolean;
832883

833884
constructor(opts: {
834885
streamRecord: StreamRecord;
835886
path: Path | undefined;
887+
deferPriority: number;
888+
streamPriority: number;
836889
parents: ReadonlyArray<SubsequentResultRecord> | undefined;
837890
}) {
838891
this.streamRecord = opts.streamRecord;
839892
this.path = pathToArray(opts.path);
893+
this.deferPriority = opts.deferPriority;
894+
this.streamPriority = opts.streamPriority;
840895
this.parents = opts.parents;
841896
this.children = new Set();
842897
this.errors = [];
843898
this.isCompleted = false;
844899
this.items = [];
900+
// promiseWithResolvers uses void only as a generic type parameter
901+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
902+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
903+
const { promise: published, resolve } = promiseWithResolvers<void>();
904+
this.published = published;
905+
this.publish = () => {
906+
resolve();
907+
this.published = true;
908+
};
845909
this.sent = false;
846910
}
847911
}

src/execution/__tests__/defer-test.ts

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

0 commit comments

Comments
 (0)