Skip to content

Commit

Permalink
add helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Jun 18, 2023
1 parent 373870e commit ad7439b
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 53 deletions.
88 changes: 76 additions & 12 deletions src/execution/IncrementalPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import type {
GraphQLFormattedError,
} from '../error/GraphQLError.js';

import type { DeferUsage } from '../type/definition.js';

import type {
DeferUsage,
DeferUsageSet,
GroupedFieldSet,
GroupedFieldSetDetails,
Expand Down Expand Up @@ -303,12 +304,26 @@ export class IncrementalPublisher {
newGroupedFieldSetDeferUsages,
newDeferMap,
);
const deferredGroupedFieldSetRecord = new DeferredGroupedFieldSetRecord({
path,
deferredFragmentRecords,
groupedFieldSet,
shouldInitiateDefer,
});
const deferredGroupedFieldSetRecord =
incrementalDataRecord === undefined
? new DeferredGroupedFieldSetRecord({
path,
deferredFragmentRecords,
groupedFieldSet,
deferPriority: 1,
streamPriority: 0,
shouldInitiateDefer,
})
: new DeferredGroupedFieldSetRecord({
path,
deferredFragmentRecords,
groupedFieldSet,
deferPriority: shouldInitiateDefer
? incrementalDataRecord.deferPriority + 1
: incrementalDataRecord.deferPriority,
streamPriority: incrementalDataRecord.streamPriority,
shouldInitiateDefer,
});
for (const deferredFragmentRecord of deferredFragmentRecords) {
deferredFragmentRecord._pending.add(deferredGroupedFieldSetRecord);
deferredFragmentRecord.deferredGroupedFieldSetRecords.add(
Expand Down Expand Up @@ -342,11 +357,22 @@ export class IncrementalPublisher {
incrementalDataRecord: IncrementalDataRecord | undefined,
): StreamItemsRecord {
const parents = getSubsequentResultRecords(incrementalDataRecord);
const streamItemsRecord = new StreamItemsRecord({
streamRecord,
path,
parents,
});
const streamItemsRecord =
incrementalDataRecord === undefined
? new StreamItemsRecord({
streamRecord,
path,
deferPriority: 0,
streamPriority: 1,
parents,
})
: new StreamItemsRecord({
streamRecord,
path,
deferPriority: incrementalDataRecord.deferPriority,
streamPriority: incrementalDataRecord.streamPriority + 1,
parents,
});

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

if (isStreamItemsRecord(subsequentResultRecord)) {
this._introduce(subsequentResultRecord);
subsequentResultRecord.publish();
return;
}

if (subsequentResultRecord._pending.size === 0) {
subsequentResultRecord.isCompleted = true;
this._push(subsequentResultRecord);
} else {
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
deferredGroupedFieldSetRecord.publish();
}
this._introduce(subsequentResultRecord);
}
}
Expand Down Expand Up @@ -748,24 +778,41 @@ export class IncrementalPublisher {
/** @internal */
export class DeferredGroupedFieldSetRecord {
path: ReadonlyArray<string | number>;
deferPriority: number;
streamPriority: number;
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
groupedFieldSet: GroupedFieldSet;
shouldInitiateDefer: boolean;
errors: Array<GraphQLError>;
data: ObjMap<unknown> | undefined;
published: true | Promise<void>;
publish: () => void;
sent: boolean;

constructor(opts: {
path: Path | undefined;
deferPriority: number;
streamPriority: number;
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
groupedFieldSet: GroupedFieldSet;
shouldInitiateDefer: boolean;
}) {
this.path = pathToArray(opts.path);
this.deferPriority = opts.deferPriority;
this.streamPriority = opts.streamPriority;
this.deferredFragmentRecords = opts.deferredFragmentRecords;
this.groupedFieldSet = opts.groupedFieldSet;
this.shouldInitiateDefer = opts.shouldInitiateDefer;
this.errors = [];
// promiseWithResolvers uses void only as a generic type parameter
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const { promise: published, resolve } = promiseWithResolvers<void>();
this.published = published;
this.publish = () => {
resolve();
this.published = true;
};
this.sent = false;
}
}
Expand Down Expand Up @@ -822,26 +869,43 @@ export class StreamItemsRecord {
errors: Array<GraphQLError>;
streamRecord: StreamRecord;
path: ReadonlyArray<string | number>;
deferPriority: number;
streamPriority: number;
items: Array<unknown>;
parents: ReadonlyArray<SubsequentResultRecord> | undefined;
children: Set<SubsequentResultRecord>;
isFinalRecord?: boolean;
isCompletedAsyncIterator?: boolean;
isCompleted: boolean;
published: true | Promise<void>;
publish: () => void;
sent: boolean;

constructor(opts: {
streamRecord: StreamRecord;
path: Path | undefined;
deferPriority: number;
streamPriority: number;
parents: ReadonlyArray<SubsequentResultRecord> | undefined;
}) {
this.streamRecord = opts.streamRecord;
this.path = pathToArray(opts.path);
this.deferPriority = opts.deferPriority;
this.streamPriority = opts.streamPriority;
this.parents = opts.parents;
this.children = new Set();
this.errors = [];
this.isCompleted = false;
this.items = [];
// promiseWithResolvers uses void only as a generic type parameter
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const { promise: published, resolve } = promiseWithResolvers<void>();
this.published = published;
this.publish = () => {
resolve();
this.published = true;
};
this.sent = false;
}
}
Expand Down
177 changes: 176 additions & 1 deletion src/execution/__tests__/defer-test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { expect } from 'chai';
import { assert, expect } from 'chai';
import { describe, it } from 'mocha';

import { expectJSON } from '../../__testUtils__/expectJSON.js';
import { expectPromise } from '../../__testUtils__/expectPromise.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

import { isPromise } from '../../jsutils/isPromise.js';

import type { DocumentNode } from '../../language/ast.js';
import { Kind } from '../../language/kinds.js';
import { parse } from '../../language/parser.js';

import type { FieldGroup } from '../../type/definition.js';
import {
GraphQLList,
GraphQLNonNull,
Expand Down Expand Up @@ -224,6 +228,177 @@ describe('Execute: defer directive', () => {
},
});
});
it('Can provides correct info about deferred execution state when resolver could defer', async () => {
let fieldGroup: FieldGroup | undefined;
let deferPriority;
let published;
let resumed;

const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
someField: {
type: GraphQLString,
resolve: () => Promise.resolve('someField'),
},
deferredField: {
type: GraphQLString,
resolve: async (_parent, _args, _context, info) => {
fieldGroup = info.fieldGroup;
deferPriority = info.deferPriority;
published = info.published;
await published;
resumed = true;
},
},
},
});

const someSchema = new GraphQLSchema({ query: SomeType });

const document = parse(`
query {
someField
... @defer {
deferredField
}
}
`);

const operation = document.definitions[0];
assert(operation.kind === Kind.OPERATION_DEFINITION);
const fragment = operation.selectionSet.selections[1];
assert(fragment.kind === Kind.INLINE_FRAGMENT);
const field = fragment.selectionSet.selections[0];

const result = experimentalExecuteIncrementally({
schema: someSchema,
document,
});

expect(fieldGroup).to.equal(undefined);
expect(deferPriority).to.equal(undefined);
expect(published).to.equal(undefined);
expect(resumed).to.equal(undefined);

const initialPayload = await result;
assert('initialResult' in initialPayload);
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
await iterator.next();

assert(fieldGroup !== undefined);
const fieldDetails = fieldGroup.fields[0];
expect(fieldDetails.node).to.equal(field);
expect(fieldDetails.target?.priority).to.equal(1);
expect(deferPriority).to.equal(1);
expect(isPromise(published)).to.equal(true);
expect(resumed).to.equal(true);
});
it('Can provides correct info about deferred execution state when deferred field is masked by non-deferred field', async () => {
let fieldGroup: FieldGroup | undefined;
let deferPriority;
let published;

const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
someField: {
type: GraphQLString,
resolve: (_parent, _args, _context, info) => {
fieldGroup = info.fieldGroup;
deferPriority = info.deferPriority;
published = info.published;
return 'someField';
},
},
},
});

const someSchema = new GraphQLSchema({ query: SomeType });

const document = parse(`
query {
someField
... @defer {
someField
}
}
`);

const operation = document.definitions[0];
assert(operation.kind === Kind.OPERATION_DEFINITION);
const node1 = operation.selectionSet.selections[0];
const fragment = operation.selectionSet.selections[1];
assert(fragment.kind === Kind.INLINE_FRAGMENT);
const node2 = fragment.selectionSet.selections[0];

const result = experimentalExecuteIncrementally({
schema: someSchema,
document,
});

const initialPayload = await result;
assert('initialResult' in initialPayload);
expect(initialPayload.initialResult).to.deep.equal({
data: {
someField: 'someField',
},
pending: [{ path: [] }],
hasNext: true,
});

assert(fieldGroup !== undefined);
const fieldDetails1 = fieldGroup.fields[0];
expect(fieldDetails1.node).to.equal(node1);
expect(fieldDetails1.target).to.equal(undefined);
const fieldDetails2 = fieldGroup.fields[1];
expect(fieldDetails2.node).to.equal(node2);
expect(fieldDetails2.target?.priority).to.equal(1);
expect(deferPriority).to.equal(0);
expect(published).to.equal(true);
});
it('Can provides correct info about deferred execution state when resolver need not defer', async () => {
let deferPriority;
let published;
const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
deferredField: {
type: GraphQLString,
resolve: (_parent, _args, _context, info) => {
deferPriority = info.deferPriority;
published = info.published;
},
},
},
});

const someSchema = new GraphQLSchema({ query: SomeType });

const document = parse(`
query {
... @defer {
deferredField
}
}
`);

const result = experimentalExecuteIncrementally({
schema: someSchema,
document,
});

expect(deferPriority).to.equal(undefined);
expect(published).to.equal(undefined);

const initialPayload = await result;
assert('initialResult' in initialPayload);
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
await iterator.next();

expect(deferPriority).to.equal(1);
expect(published).to.equal(true);
});
it('Does not disable defer with null if argument', async () => {
const document = parse(`
query HeroNameQuery($shouldDefer: Boolean) {
Expand Down
Loading

0 comments on commit ad7439b

Please sign in to comment.