Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/execution/IncrementalPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,12 +663,16 @@ export class IncrementalPublisher {
}

this._introduce(subsequentResultRecord);
subsequentResultRecord.publish();
return;
}

if (subsequentResultRecord._pending.size === 0) {
this._push(subsequentResultRecord);
} else {
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
deferredGroupedFieldSetRecord.publish();
}
this._introduce(subsequentResultRecord);
}
}
Expand Down Expand Up @@ -749,33 +753,56 @@ function isStreamItemsRecord(
export class InitialResultRecord {
errors: Array<GraphQLError>;
children: Set<SubsequentResultRecord>;
priority: number;
deferPriority: number;
published: true;
constructor() {
this.errors = [];
this.children = new Set();
this.priority = 0;
this.deferPriority = 0;
this.published = true;
}
}

/** @internal */
export class DeferredGroupedFieldSetRecord {
path: ReadonlyArray<string | number>;
priority: number;
deferPriority: 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;
priority: number;
deferPriority: number;
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
groupedFieldSet: GroupedFieldSet;
shouldInitiateDefer: boolean;
}) {
this.path = pathToArray(opts.path);
this.priority = opts.priority;
this.deferPriority = opts.deferPriority;
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 @@ -828,20 +855,41 @@ export class StreamItemsRecord {
errors: Array<GraphQLError>;
streamRecord: StreamRecord;
path: ReadonlyArray<string | number>;
priority: number;
deferPriority: number;
items: Array<unknown> | undefined;
children: Set<SubsequentResultRecord>;
isFinalRecord?: boolean;
isCompletedAsyncIterator?: boolean;
isCompleted: boolean;
filtered: boolean;
published: true | Promise<void>;
publish: () => void;
sent: boolean;

constructor(opts: { streamRecord: StreamRecord; path: Path | undefined }) {
constructor(opts: {
streamRecord: StreamRecord;
path: Path | undefined;
priority: number;
}) {
this.streamRecord = opts.streamRecord;
this.path = pathToArray(opts.path);
this.priority = opts.priority;
this.deferPriority = 0;
this.children = new Set();
this.errors = [];
this.isCompleted = false;
this.filtered = false;
// 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
174 changes: 173 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 { FieldDetails } from '../../type/definition.js';
import {
GraphQLList,
GraphQLNonNull,
Expand Down Expand Up @@ -216,6 +220,174 @@ describe('Execute: defer directive', () => {
},
});
});
it('Can provides correct info about deferred execution state when resolver could defer', async () => {
let fieldDetails: ReadonlyArray<FieldDetails> | 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) => {
fieldDetails = info.fieldDetails;
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(fieldDetails).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(fieldDetails !== undefined);
expect(fieldDetails[0].node).to.equal(field);
expect(fieldDetails[0].target?.deferPriority).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 fieldDetails: ReadonlyArray<FieldDetails> | undefined;
let deferPriority;
let published;

const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
someField: {
type: GraphQLString,
resolve: (_parent, _args, _context, info) => {
fieldDetails = info.fieldDetails;
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: [{ id: '0', path: [] }],
hasNext: true,
});

assert(fieldDetails !== undefined);
expect(fieldDetails[0].node).to.equal(node1);
expect(fieldDetails[0].target).to.equal(undefined);
expect(fieldDetails[1].node).to.equal(node2);
expect(fieldDetails[1].target?.deferPriority).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
22 changes: 18 additions & 4 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { inspect } from '../../jsutils/inspect.js';
import { Kind } from '../../language/kinds.js';
import { parse } from '../../language/parser.js';

import type { GraphQLResolveInfo } from '../../type/definition.js';
import {
GraphQLInterfaceType,
GraphQLList,
Expand Down Expand Up @@ -191,7 +192,7 @@ describe('Execute: Handles basic execution tasks', () => {
});

it('provides info about current execution state', () => {
let resolvedInfo;
let resolvedInfo: GraphQLResolveInfo | undefined;
const testType = new GraphQLObjectType({
name: 'Test',
fields: {
Expand All @@ -213,7 +214,7 @@ describe('Execute: Handles basic execution tasks', () => {

expect(resolvedInfo).to.have.all.keys(
'fieldName',
'fieldNodes',
'fieldDetails',
'returnType',
'parentType',
'path',
Expand All @@ -222,6 +223,9 @@ describe('Execute: Handles basic execution tasks', () => {
'rootValue',
'operation',
'variableValues',
'priority',
'deferPriority',
'published',
);

const operation = document.definitions[0];
Expand All @@ -234,14 +238,24 @@ describe('Execute: Handles basic execution tasks', () => {
schema,
rootValue,
operation,
priority: 0,
deferPriority: 0,
published: true,
});

const field = operation.selectionSet.selections[0];
expect(resolvedInfo).to.deep.include({
fieldNodes: [field],
path: { prev: undefined, key: 'result', typename: 'Test' },
variableValues: { var: 'abc' },
});

const fieldDetails = resolvedInfo?.fieldDetails;
assert(fieldDetails !== undefined);

const field = operation.selectionSet.selections[0];
expect(fieldDetails[0]).to.deep.include({
node: field,
target: undefined,
});
});

it('populates path correctly with complex types', () => {
Expand Down
Loading