Skip to content

Commit

Permalink
Support incremental delivery with defer/stream (#41)
Browse files Browse the repository at this point in the history
* support async benchmark tests

* Add benchmarks for sync and async list fields

* Support returning async iterables from resolver functions

Support returning async iterables from resolver functions

* add benchmark tests for async iterable list fields

* Add @defer directive to specified directives

# Conflicts:
#	src/index.d.ts
#	src/type/directives.d.ts
#	src/type/directives.ts
#	src/type/index.js

* Implement support for @defer directive

* Add @stream directive to specified directives

# Conflicts:
#	src/index.d.ts
#	src/type/directives.d.ts
#	src/type/directives.ts
#	src/type/index.js

* Implement support for @stream directive

# Conflicts:
#	src/execution/execute.ts
#	src/validation/index.d.ts
#	src/validation/index.ts

* add defer/stream support for subscriptions (#7)

# Conflicts:
#	src/subscription/subscribe.ts

* Return underlying AsyncIterators when execute result is returned (graphql#2843)

# Conflicts:
#	src/execution/execute.ts

* fix(race): concurrent next calls with defer/stream (graphql#2975)

* fix(race): concurrent next calls

* refactor test

* use invariant

* disable eslint error

* fix

* Update executor

* Disable require-atomic-updates

eslint/eslint#11899

* Fix merege

* Further merge fixes

* run prettier

* add changeset

* Update defer/stream to return AsyncGenerator

...instead of AsyncIterable, to match v16

* add optional arguments to disable incremental delivery

* Subscription root field by spec cannot be inside deferred fragment

* Use spread initializers

* fix code coverage

Co-authored-by: Rob Richard <rob@1stdibs.com>
Co-authored-by: Liliana Matos <liliana@1stdibs.com>
  • Loading branch information
3 people committed Oct 29, 2021
1 parent 5dd52c5 commit f6d0b73
Show file tree
Hide file tree
Showing 25 changed files with 4,930 additions and 114 deletions.
8 changes: 8 additions & 0 deletions .changeset/many-olives-eat.md
@@ -0,0 +1,8 @@
---
'graphql-executor': patch
---

Support incremental delivery with defer/stream directives

Port of https://github.com/graphql/graphql-js/pull/2839
defer/stream support is enabled by default, but can be disabled using the `disableIncremental` argument.
300 changes: 300 additions & 0 deletions src/execution/__tests__/defer-test.ts
@@ -0,0 +1,300 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import type { DocumentNode } from 'graphql';
import {
GraphQLID,
GraphQLList,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
parse,
} from 'graphql';

import { isAsyncIterable } from '../../jsutils/isAsyncIterable';

import { execute } from '../execute';
import { expectJSON } from '../../__testUtils__/expectJSON';

const friendType = new GraphQLObjectType({
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
},
name: 'Friend',
});

const friends = [
{ name: 'Han', id: 2 },
{ name: 'Leia', id: 3 },
{ name: 'C-3PO', id: 4 },
];

const heroType = new GraphQLObjectType({
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
errorField: {
type: GraphQLString,
resolve: () => {
throw new Error('bad');
},
},
friends: {
type: new GraphQLList(friendType),
resolve: () => friends,
},
},
name: 'Hero',
});

const hero = { name: 'Luke', id: 1 };

const query = new GraphQLObjectType({
fields: {
hero: {
type: heroType,
resolve: () => hero,
},
},
name: 'Query',
});

async function complete(document: DocumentNode, disableIncremental = false) {
const schema = new GraphQLSchema({ query });

const result = await execute({
schema,
document,
rootValue: {},
disableIncremental,
});

if (isAsyncIterable(result)) {
const results = [];
for await (const patch of result) {
results.push(patch);
}
return results;
}
return result;
}

describe('Execute: defer directive', () => {
it('Can defer fragments containing scalar types', async () => {
const document = parse(`
query HeroNameQuery {
hero {
id
...NameFragment @defer
}
}
fragment NameFragment on Hero {
id
name
}
`);
const result = await complete(document);

expect(result).to.deep.equal([
{
data: {
hero: {
id: '1',
},
},
hasNext: true,
},
{
data: {
id: '1',
name: 'Luke',
},
path: ['hero'],
hasNext: false,
},
]);
});
it('Can disable defer using disableIncremental argument', async () => {
const document = parse(`
query HeroNameQuery {
hero {
id
...NameFragment @defer
}
}
fragment NameFragment on Hero {
name
}
`);
const result = await complete(document, true);

expect(result).to.deep.equal({
data: {
hero: {
id: '1',
name: 'Luke',
},
},
});
});
it('Can disable defer using if argument', async () => {
const document = parse(`
query HeroNameQuery {
hero {
id
...NameFragment @defer(if: false)
}
}
fragment NameFragment on Hero {
name
}
`);
const result = await complete(document);

expect(result).to.deep.equal({
data: {
hero: {
id: '1',
name: 'Luke',
},
},
});
});
it('Can defer fragments containing on the top level Query field', async () => {
const document = parse(`
query HeroNameQuery {
...QueryFragment @defer(label: "DeferQuery")
}
fragment QueryFragment on Query {
hero {
id
}
}
`);
const result = await complete(document);

expect(result).to.deep.equal([
{
data: {},
hasNext: true,
},
{
data: {
hero: {
id: '1',
},
},
path: [],
label: 'DeferQuery',
hasNext: false,
},
]);
});
it('Can defer a fragment within an already deferred fragment', async () => {
const document = parse(`
query HeroNameQuery {
hero {
id
...TopFragment @defer(label: "DeferTop")
}
}
fragment TopFragment on Hero {
name
...NestedFragment @defer(label: "DeferNested")
}
fragment NestedFragment on Hero {
friends {
name
}
}
`);
const result = await complete(document);

expect(result).to.deep.equal([
{
data: {
hero: {
id: '1',
},
},
hasNext: true,
},
{
data: {
friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }],
},
path: ['hero'],
label: 'DeferNested',
hasNext: true,
},
{
data: {
name: 'Luke',
},
path: ['hero'],
label: 'DeferTop',
hasNext: false,
},
]);
});
it('Can defer an inline fragment', async () => {
const document = parse(`
query HeroNameQuery {
hero {
id
... on Hero @defer(label: "InlineDeferred") {
name
}
}
}
`);
const result = await complete(document);

expect(result).to.deep.equal([
{
data: { hero: { id: '1' } },
hasNext: true,
},
{
data: { name: 'Luke' },
path: ['hero'],
label: 'InlineDeferred',
hasNext: false,
},
]);
});
it('Handles errors thrown in deferred fragments', async () => {
const document = parse(`
query HeroNameQuery {
hero {
id
...NameFragment @defer
}
}
fragment NameFragment on Hero {
errorField
}
`);
const result = await complete(document);

expectJSON(result).toDeepEqual([
{
data: { hero: { id: '1' } },
hasNext: true,
},
{
data: { errorField: null },
path: ['hero'],
errors: [
{
message: 'bad',
locations: [{ line: 9, column: 9 }],
path: ['hero', 'errorField'],
},
],
hasNext: false,
},
]);
});
});

0 comments on commit f6d0b73

Please sign in to comment.