Skip to content

Commit

Permalink
Introduce possibility of passing in an AbortSignal
Browse files Browse the repository at this point in the history
Co-Authored-By: yaacovCR <[email protected]>
  • Loading branch information
JoviDeCroock and yaacovCR committed Oct 22, 2024
1 parent f531737 commit ae108b2
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 7 deletions.
8 changes: 7 additions & 1 deletion integrationTests/ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"],
"lib": [
"es2019",
"es2020.promise",
"es2020.bigint",
"es2020.string",
"DOM"
],
"noEmit": true,
"types": [],
"strict": true,
Expand Down
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@svgr/webpack": "8.1.0",
"@types/chai": "4.3.19",
"@types/mocha": "10.0.7",
"@types/node": "22.5.4",
"@types/node": "22.7.7",
"@typescript-eslint/eslint-plugin": "8.4.0",
"@typescript-eslint/parser": "8.4.0",
"c8": "10.1.2",
Expand Down
347 changes: 347 additions & 0 deletions src/execution/__tests__/abort-signal-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

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

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

import { buildSchema } from '../../utilities/buildASTSchema.js';

import { execute, experimentalExecuteIncrementally } from '../execute.js';
import type {
InitialIncrementalExecutionResult,
SubsequentIncrementalExecutionResult,
} from '../types.js';

async function complete(
document: DocumentNode,
rootValue: unknown,
abortSignal: AbortSignal,
) {
const result = await experimentalExecuteIncrementally({
schema,
document,
rootValue,
abortSignal,
});

if ('initialResult' in result) {
const results: Array<
InitialIncrementalExecutionResult | SubsequentIncrementalExecutionResult
> = [result.initialResult];
for await (const patch of result.subsequentResults) {
results.push(patch);
}
return results;
}
}

const schema = buildSchema(`
type Todo {
id: ID
text: String
author: User
}
type User {
id: ID
name: String
}
type Query {
todo: Todo
}
type Mutation {
foo: String
bar: String
}
`);

describe('Execute: Cancellation', () => {
it('should stop the execution when aborted', async () => {
const abortController = new AbortController();
const document = parse(`
query {
todo {
id
author {
id
}
}
}
`);

const resultPromise = execute({
document,
schema,
abortSignal: abortController.signal,
rootValue: {
todo: async () =>
Promise.resolve({
id: '1',
text: 'Hello, World!',
/* c8 ignore next */
author: () => expect.fail('Should not be called'),
}),
},
});

abortController.abort('Aborted');

const result = await resultPromise;

expectJSON(result).toDeepEqual({
data: {
todo: null,
},
errors: [
{
message: 'Aborted',
path: ['todo'],
locations: [{ line: 3, column: 9 }],
},
],
});
});

it('should stop the execution when aborted during nested object field completion', async () => {
const abortController = new AbortController();
const document = parse(`
query {
todo {
id
author {
id
}
}
}
`);

const resultPromise = execute({
document,
schema,
abortSignal: abortController.signal,
rootValue: {
todo: {
id: '1',
text: 'Hello, World!',
/* c8 ignore next 3 */
author: async () =>
Promise.resolve(() => expect.fail('Should not be called')),
},
},
});

abortController.abort('Aborted');

const result = await resultPromise;

expectJSON(result).toDeepEqual({
data: {
todo: {
id: '1',
author: null,
},
},
errors: [
{
message: 'Aborted',
path: ['todo', 'author'],
locations: [{ line: 5, column: 11 }],
},
],
});
});

it('should stop deferred execution when aborted', async () => {
const abortController = new AbortController();
const document = parse(`
query {
todo {
id
... on Todo @defer {
text
author {
... on Author @defer {
id
}
}
}
}
}
`);

const resultPromise = execute({
document,
schema,
rootValue: {
todo: async () =>
Promise.resolve({
id: '1',
text: 'hello world',
/* c8 ignore next */
author: () => expect.fail('Should not be called'),
}),
},
abortSignal: abortController.signal,
});

abortController.abort('Aborted');

const result = await resultPromise;

expectJSON(result).toDeepEqual({
data: {
todo: null,
},
errors: [
{
message: 'Aborted',
path: ['todo'],
locations: [{ line: 3, column: 9 }],
},
],
});
});

it('should stop deferred execution when aborted mid-execution', async () => {
const abortController = new AbortController();
const document = parse(`
query {
todo {
id
... on Todo @defer {
text
author {
... on Author @defer {
id
}
}
}
}
}
`);

const resultPromise = complete(
document,
{
todo: async () =>
Promise.resolve({
id: '1',
text: 'hello world',
/* c8 ignore next 2 */
author: async () =>
Promise.resolve(() => expect.fail('Should not be called')),
}),
},
abortController.signal,
);

await resolveOnNextTick();
await resolveOnNextTick();
await resolveOnNextTick();

abortController.abort('Aborted');

const result = await resultPromise;

expectJSON(result).toDeepEqual([
{
data: {
todo: {
id: '1',
},
},
pending: [{ id: '0', path: ['todo'] }],
hasNext: true,
},
{
completed: [
{
errors: [
{
message: 'Aborted',
},
],
id: '0',
},
],
hasNext: false,
},
]);
});

it('should stop the execution when aborted mid-mutation', async () => {
const abortController = new AbortController();
const document = parse(`
mutation {
foo
bar
}
`);

const resultPromise = execute({
document,
schema,
abortSignal: abortController.signal,
rootValue: {
foo: async () => Promise.resolve('baz'),
/* c8 ignore next */
bar: () => expect.fail('Should not be called'),
},
});

abortController.abort('Aborted');

const result = await resultPromise;

expectJSON(result).toDeepEqual({
data: {
foo: 'baz',
bar: null,
},
errors: [
{
message: 'Aborted',
path: ['bar'],
locations: [{ line: 4, column: 9 }],
},
],
});
});

it('should stop the execution when aborted pre-execute', async () => {
const abortController = new AbortController();
const document = parse(`
query {
todo {
id
author {
id
}
}
}
`);
abortController.abort('Aborted');
const result = await execute({
document,
schema,
abortSignal: abortController.signal,
rootValue: {
/* c8 ignore next */
todo: () => expect.fail('Should not be called'),
},
});

expectJSON(result).toDeepEqual({
errors: [
{
message: 'Aborted',
},
],
});
});
});
Loading

0 comments on commit ae108b2

Please sign in to comment.