Skip to content

Commit a73202a

Browse files
committed
add helpers
1 parent 373870e commit a73202a

File tree

6 files changed

+343
-46
lines changed

6 files changed

+343
-46
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: 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,
@@ -224,6 +228,174 @@ describe('Execute: defer directive', () => {
224228
},
225229
});
226230
});
231+
it('Can provides correct info about deferred execution state when resolver could defer', async () => {
232+
let fieldDetails: ReadonlyArray<FieldDetails> | 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+
fieldDetails = info.fieldDetails;
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(fieldDetails).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(fieldDetails !== undefined);
290+
expect(fieldDetails[0].node).to.equal(field);
291+
expect(fieldDetails[0].target?.priority).to.equal(1);
292+
expect(deferPriority).to.equal(1);
293+
expect(isPromise(published)).to.equal(true);
294+
expect(resumed).to.equal(true);
295+
});
296+
it('Can provides correct info about deferred execution state when deferred field is masked by non-deferred field', async () => {
297+
let fieldDetails: ReadonlyArray<FieldDetails> | undefined;
298+
let deferPriority;
299+
let published;
300+
301+
const SomeType = new GraphQLObjectType({
302+
name: 'SomeType',
303+
fields: {
304+
someField: {
305+
type: GraphQLString,
306+
resolve: (_parent, _args, _context, info) => {
307+
fieldDetails = info.fieldDetails;
308+
deferPriority = info.deferPriority;
309+
published = info.published;
310+
return 'someField';
311+
},
312+
},
313+
},
314+
});
315+
316+
const someSchema = new GraphQLSchema({ query: SomeType });
317+
318+
const document = parse(`
319+
query {
320+
someField
321+
... @defer {
322+
someField
323+
}
324+
}
325+
`);
326+
327+
const operation = document.definitions[0];
328+
assert(operation.kind === Kind.OPERATION_DEFINITION);
329+
const node1 = operation.selectionSet.selections[0];
330+
const fragment = operation.selectionSet.selections[1];
331+
assert(fragment.kind === Kind.INLINE_FRAGMENT);
332+
const node2 = fragment.selectionSet.selections[0];
333+
334+
const result = experimentalExecuteIncrementally({
335+
schema: someSchema,
336+
document,
337+
});
338+
339+
const initialPayload = await result;
340+
assert('initialResult' in initialPayload);
341+
expect(initialPayload.initialResult).to.deep.equal({
342+
data: {
343+
someField: 'someField',
344+
},
345+
pending: [{ path: [] }],
346+
hasNext: true,
347+
});
348+
349+
assert(fieldDetails !== undefined);
350+
expect(fieldDetails[0].node).to.equal(node1);
351+
expect(fieldDetails[0].target).to.equal(undefined);
352+
expect(fieldDetails[1].node).to.equal(node2);
353+
expect(fieldDetails[1].target?.priority).to.equal(1);
354+
expect(deferPriority).to.equal(0);
355+
expect(published).to.equal(true);
356+
});
357+
it('Can provides correct info about deferred execution state when resolver need not defer', async () => {
358+
let deferPriority;
359+
let published;
360+
const SomeType = new GraphQLObjectType({
361+
name: 'SomeType',
362+
fields: {
363+
deferredField: {
364+
type: GraphQLString,
365+
resolve: (_parent, _args, _context, info) => {
366+
deferPriority = info.deferPriority;
367+
published = info.published;
368+
},
369+
},
370+
},
371+
});
372+
373+
const someSchema = new GraphQLSchema({ query: SomeType });
374+
375+
const document = parse(`
376+
query {
377+
... @defer {
378+
deferredField
379+
}
380+
}
381+
`);
382+
383+
const result = experimentalExecuteIncrementally({
384+
schema: someSchema,
385+
document,
386+
});
387+
388+
expect(deferPriority).to.equal(undefined);
389+
expect(published).to.equal(undefined);
390+
391+
const initialPayload = await result;
392+
assert('initialResult' in initialPayload);
393+
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
394+
await iterator.next();
395+
396+
expect(deferPriority).to.equal(1);
397+
expect(published).to.equal(true);
398+
});
227399
it('Does not disable defer with null if argument', async () => {
228400
const document = parse(`
229401
query HeroNameQuery($shouldDefer: Boolean) {

0 commit comments

Comments
 (0)