Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow execution to be cancelled #3791

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
8 changes: 6 additions & 2 deletions src/__testUtils__/resolveOnNextTick.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export function resolveOnNextTick(): Promise<void> {
return Promise.resolve(undefined);
export function resolveOnNextTick(): Promise<void>;
export function resolveOnNextTick<T>(value: T): Promise<T>;
export function resolveOnNextTick(
value: unknown = undefined,
): Promise<unknown> {
return Promise.resolve(value);
}
195 changes: 195 additions & 0 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe, it } from 'mocha';
import { expectJSON } from '../../__testUtils__/expectJSON.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

import type { IAbortSignal, IEvent } from '../../jsutils/AbortController.js';
import { inspect } from '../../jsutils/inspect.js';

import { Kind } from '../../language/kinds.js';
Expand Down Expand Up @@ -222,6 +223,7 @@ describe('Execute: Handles basic execution tasks', () => {
'rootValue',
'operation',
'variableValues',
'signal',
);

const operation = document.definitions[0];
Expand Down Expand Up @@ -1313,4 +1315,197 @@ describe('Execute: Handles basic execution tasks', () => {
expect(result).to.deep.equal({ data: { foo: { bar: 'bar' } } });
expect(possibleTypes).to.deep.equal([fooObject]);
});

/* c8 ignore start */
if (typeof AbortController !== 'undefined') {
it('stops execution and throws an error when signal is aborted', async () => {
// TODO: use real Event once we can finally drop node14 support
class MockAbortEvent implements IEvent {
cancelable = false;
bubbles = false;
composed = false;
currentTarget = null;
cancelBubble = false;
defaultPrevented = false;
isTrusted = true;
returnValue = false;
srcElement = null;
type = 'abort';
eventPhase = 0;
timeStamp = 0;
AT_TARGET = 0;
BUBBLING_PHASE = 0;
CAPTURING_PHASE = 0;
NONE = 0;

target: IAbortSignal;

constructor(abortSignal: IAbortSignal) {
this.target = abortSignal;
}

composedPath = () => {
throw new Error('Not mocked!');
};

initEvent = () => {
throw new Error('Not mocked!');
};

preventDefault = () => {
throw new Error('');
};

stopImmediatePropagation = () => {
throw new Error('');
};

stopPropagation = () => {
throw new Error('');
};
}

class MockAbortSignal implements IAbortSignal {
aborted: boolean = false;
onabort: ((ev: IEvent) => any) | null = null;
reason: unknown;

throwIfAborted() {
if (this.aborted) {
throw this.reason;
}
}

addEventListener(type: string, cb: unknown) {
expect(type).to.equal('abort');
expect(this.onabort).to.equal(null);
expect(cb).to.be.a('function');
this.onabort = cb as any;
}

removeEventListener(type: string, cb: unknown) {
expect(type).to.equal('abort');
expect(cb).to.be.a('function');
this.onabort = null;
}

dispatchEvent(event: IEvent): boolean {
expect(this.onabort).to.be.a('function');
this.onabort?.(event);
return true;
}

dispatchMockAbortEvent(reason?: unknown) {
this.reason = reason;
mockAbortSignal.dispatchEvent(new MockAbortEvent(this));
}
}

const mockAbortSignal = new MockAbortSignal();

const TestType: GraphQLObjectType = new GraphQLObjectType({
name: 'TestType',
fields: () => ({
resolveOnNextTick: {
type: TestType,
resolve: () => resolveOnNextTick({}),
},
string: {
type: GraphQLString,
args: {
value: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: (_, { value }) => value,
},
abortExecution: {
type: GraphQLString,
resolve: () => {
const abortError = new Error('Custom abort error');
mockAbortSignal.dispatchMockAbortEvent(abortError);
return 'aborted';
},
},
shouldNotBeResolved: {
type: GraphQLString,
/* c8 ignore next */
resolve: () => 'This should not be executed!',
},
}),
});

const schema = new GraphQLSchema({
query: TestType,
});

const document = parse(`
query {
value1: string(value: "1")
resolveOnNextTick {
value2: string(value: "2")
resolveOnNextTick {
resolveOnNextTick {
shouldNotBeResolved
}
abortExecution
}
}
alternativeBranch: resolveOnNextTick {
value3: string(value: "3")
resolveOnNextTick {
shouldNotBeResolved
}
}
}
`);

const result = await execute({
schema,
document,
signal: mockAbortSignal,
});

expectJSON(result).toDeepEqual({
data: {
value1: '1',
resolveOnNextTick: {
value2: '2',
resolveOnNextTick: {
resolveOnNextTick: {
shouldNotBeResolved: null,
},
abortExecution: 'aborted',
},
},
alternativeBranch: {
value3: '3',
resolveOnNextTick: {
shouldNotBeResolved: null,
},
},
},
errors: [
{
message: 'Custom abort error',
path: [
'alternativeBranch',
'resolveOnNextTick',
'shouldNotBeResolved',
],
locations: [{ line: 16, column: 13 }],
},
{
message: 'Custom abort error',
path: [
'resolveOnNextTick',
'resolveOnNextTick',
'resolveOnNextTick',
'shouldNotBeResolved',
],
locations: [{ line: 8, column: 15 }],
},
],
});
});
}
/* c8 ignore stop */
});
55 changes: 44 additions & 11 deletions src/execution/__tests__/mapAsyncIterable-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { describe, it } from 'mocha';

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

import { noop } from '../../jsutils/AbortController.js';

import { mapAsyncIterable } from '../mapAsyncIterable.js';

/* eslint-disable @typescript-eslint/require-await */
Expand All @@ -14,15 +16,24 @@ describe('mapAsyncIterable', () => {
yield 3;
}

const doubles = mapAsyncIterable(source(), (x) => x + x);
let calledFinishedCallback = false;
const doubles = mapAsyncIterable(
source(),
(x) => x + x,
() => {
calledFinishedCallback = true;
},
);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
expect(await doubles.next()).to.deep.equal({ value: 6, done: false });
expect(calledFinishedCallback).to.equal(false);
expect(await doubles.next()).to.deep.equal({
value: undefined,
done: true,
});
expect(calledFinishedCallback).to.equal(true);
});

it('maps over async iterable', async () => {
Expand All @@ -44,15 +55,24 @@ describe('mapAsyncIterable', () => {
},
};

const doubles = mapAsyncIterable(iterable, (x) => x + x);
let calledFinishedCallback = false;
const doubles = mapAsyncIterable(
iterable,
(x) => x + x,
() => {
calledFinishedCallback = true;
},
);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
expect(await doubles.next()).to.deep.equal({ value: 6, done: false });
expect(calledFinishedCallback).to.equal(false);
expect(await doubles.next()).to.deep.equal({
value: undefined,
done: true,
});
expect(calledFinishedCallback).to.equal(true);
});

it('compatible with for-await-of', async () => {
Expand All @@ -62,12 +82,21 @@ describe('mapAsyncIterable', () => {
yield 3;
}

const doubles = mapAsyncIterable(source(), (x) => x + x);
let calledFinishedCallback = false;
const doubles = mapAsyncIterable(
source(),
(x) => x + x,
() => {
calledFinishedCallback = true;
},
);

const result = [];
for await (const x of doubles) {
result.push(x);
expect(calledFinishedCallback).to.equal(false);
}
expect(calledFinishedCallback).to.equal(true);
expect(result).to.deep.equal([2, 4, 6]);
});

Expand All @@ -78,7 +107,11 @@ describe('mapAsyncIterable', () => {
yield 3;
}

const doubles = mapAsyncIterable(source(), (x) => Promise.resolve(x + x));
const doubles = mapAsyncIterable(
source(),
(x) => Promise.resolve(x + x),
noop,
);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
Expand All @@ -102,7 +135,7 @@ describe('mapAsyncIterable', () => {
}
}

const doubles = mapAsyncIterable(source(), (x) => x + x);
const doubles = mapAsyncIterable(source(), (x) => x + x, noop);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
Expand Down Expand Up @@ -141,7 +174,7 @@ describe('mapAsyncIterable', () => {
},
};

const doubles = mapAsyncIterable(iterable, (x) => x + x);
const doubles = mapAsyncIterable(iterable, (x) => x + x, noop);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
Expand All @@ -166,7 +199,7 @@ describe('mapAsyncIterable', () => {
}
}

const doubles = mapAsyncIterable(source(), (x) => x + x);
const doubles = mapAsyncIterable(source(), (x) => x + x, noop);

expect(await doubles.next()).to.deep.equal({ value: 'aa', done: false });
expect(await doubles.next()).to.deep.equal({ value: 'bb', done: false });
Expand Down Expand Up @@ -205,7 +238,7 @@ describe('mapAsyncIterable', () => {
},
};

const doubles = mapAsyncIterable(iterable, (x) => x + x);
const doubles = mapAsyncIterable(iterable, (x) => x + x, noop);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
Expand All @@ -228,7 +261,7 @@ describe('mapAsyncIterable', () => {
}
}

const doubles = mapAsyncIterable(source(), (x) => x + x);
const doubles = mapAsyncIterable(source(), (x) => x + x, noop);

expect(await doubles.next()).to.deep.equal({ value: 2, done: false });
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
Expand All @@ -255,7 +288,7 @@ describe('mapAsyncIterable', () => {
throw new Error('Goodbye');
}

const doubles = mapAsyncIterable(source(), (x) => x + x);
const doubles = mapAsyncIterable(source(), (x) => x + x, noop);

expect(await doubles.next()).to.deep.equal({
value: 'HelloHello',
Expand All @@ -280,7 +313,7 @@ describe('mapAsyncIterable', () => {
}
}

const throwOver1 = mapAsyncIterable(source(), mapper);
const throwOver1 = mapAsyncIterable(source(), mapper, noop);

expect(await throwOver1.next()).to.deep.equal({ value: 1, done: false });

Expand Down
Loading