Skip to content

Commit 7c6e300

Browse files
committed
add helpers
1 parent 02d58f8 commit 7c6e300

File tree

6 files changed

+322
-36
lines changed

6 files changed

+322
-36
lines changed

src/execution/IncrementalPublisher.ts

+55-2
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,
@@ -255,8 +256,11 @@ export class IncrementalPublisher {
255256
path,
256257
deferredFragmentRecords,
257258
groupedFieldSet,
259+
deferPriority: incrementalDataRecord.deferPriority + 1,
260+
streamPriority: incrementalDataRecord.streamPriority,
258261
shouldInitiateDefer,
259262
});
263+
260264
for (const deferredFragmentRecord of deferredFragmentRecords) {
261265
deferredFragmentRecord._pending.add(deferredGroupedFieldSetRecord);
262266
deferredFragmentRecord.deferredGroupedFieldSetRecords.add(
@@ -292,6 +296,8 @@ export class IncrementalPublisher {
292296
const streamItemsRecord = new StreamItemsRecord({
293297
streamRecord,
294298
path,
299+
deferPriority: 0,
300+
streamPriority: incrementalDataRecord.streamPriority + 1,
295301
});
296302

297303
if (isDeferredGroupedFieldSetRecord(incrementalDataRecord)) {
@@ -714,12 +720,16 @@ export class IncrementalPublisher {
714720
}
715721

716722
this._introduce(subsequentResultRecord);
723+
subsequentResultRecord.publish();
717724
return;
718725
}
719726

720727
if (subsequentResultRecord._pending.size === 0) {
721728
this._push(subsequentResultRecord);
722729
} else {
730+
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
731+
deferredGroupedFieldSetRecord.publish();
732+
}
723733
this._introduce(subsequentResultRecord);
724734
}
725735
}
@@ -788,33 +798,56 @@ export class IncrementalPublisher {
788798
export class InitialResultRecord {
789799
errors: Array<GraphQLError>;
790800
children: Set<SubsequentResultRecord>;
801+
deferPriority: number;
802+
streamPriority: number;
803+
published: true;
791804
constructor() {
792805
this.errors = [];
793806
this.children = new Set();
807+
this.deferPriority = 0;
808+
this.streamPriority = 0;
809+
this.published = true;
794810
}
795811
}
796812

797813
/** @internal */
798814
export class DeferredGroupedFieldSetRecord {
799815
path: ReadonlyArray<string | number>;
816+
deferPriority: number;
817+
streamPriority: number;
800818
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
801819
groupedFieldSet: GroupedFieldSet;
802820
shouldInitiateDefer: boolean;
803821
errors: Array<GraphQLError>;
804822
data: ObjMap<unknown> | undefined;
823+
published: true | Promise<void>;
824+
publish: () => void;
805825
sent: boolean;
806826

807827
constructor(opts: {
808828
path: Path | undefined;
829+
deferPriority: number;
830+
streamPriority: number;
809831
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
810832
groupedFieldSet: GroupedFieldSet;
811833
shouldInitiateDefer: boolean;
812834
}) {
813835
this.path = pathToArray(opts.path);
836+
this.deferPriority = opts.deferPriority;
837+
this.streamPriority = opts.streamPriority;
814838
this.deferredFragmentRecords = opts.deferredFragmentRecords;
815839
this.groupedFieldSet = opts.groupedFieldSet;
816840
this.shouldInitiateDefer = opts.shouldInitiateDefer;
817841
this.errors = [];
842+
// promiseWithResolvers uses void only as a generic type parameter
843+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
844+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
845+
const { promise: published, resolve } = promiseWithResolvers<void>();
846+
this.published = published;
847+
this.publish = () => {
848+
resolve();
849+
this.published = true;
850+
};
818851
this.sent = false;
819852
}
820853
}
@@ -865,22 +898,42 @@ export class StreamItemsRecord {
865898
errors: Array<GraphQLError>;
866899
streamRecord: StreamRecord;
867900
path: ReadonlyArray<string | number>;
901+
deferPriority: number;
902+
streamPriority: number;
868903
items: Array<unknown>;
869904
children: Set<SubsequentResultRecord>;
870905
isFinalRecord?: boolean;
871906
isCompletedAsyncIterator?: boolean;
872907
isCompleted: boolean;
873908
filtered: boolean;
909+
published: true | Promise<void>;
910+
publish: () => void;
874911
sent: boolean;
875912

876-
constructor(opts: { streamRecord: StreamRecord; path: Path | undefined }) {
913+
constructor(opts: {
914+
streamRecord: StreamRecord;
915+
path: Path | undefined;
916+
deferPriority: number;
917+
streamPriority: number;
918+
}) {
877919
this.streamRecord = opts.streamRecord;
878920
this.path = pathToArray(opts.path);
921+
this.deferPriority = opts.deferPriority;
922+
this.streamPriority = opts.streamPriority;
879923
this.children = new Set();
880924
this.errors = [];
881925
this.isCompleted = false;
882926
this.filtered = false;
883927
this.items = [];
928+
// promiseWithResolvers uses void only as a generic type parameter
929+
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
930+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
931+
const { promise: published, resolve } = promiseWithResolvers<void>();
932+
this.published = published;
933+
this.publish = () => {
934+
resolve();
935+
this.published = true;
936+
};
884937
this.sent = false;
885938
}
886939
}

src/execution/__tests__/defer-test.ts

+173-1
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) {

0 commit comments

Comments
 (0)