Skip to content

Commit

Permalink
stop resolvers after execution ends
Browse files Browse the repository at this point in the history
TODO: add support and tests for stopping within incremental delivery

addresses: #3792
  • Loading branch information
yaacovCR committed Oct 28, 2024
1 parent 12a5ec9 commit 1387e36
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 33 deletions.
84 changes: 80 additions & 4 deletions src/execution/__tests__/abort-signal-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ describe('Execute: Cancellation', () => {
});
});

it('should stop deferred execution when aborted mid-execution', async () => {
it('should stop deferred execution when aborted prior to initiation of deferred execution', async () => {
const abortController = new AbortController();
const document = parse(`
query {
Expand All @@ -358,9 +358,7 @@ describe('Execute: Cancellation', () => {
... on Todo @defer {
text
author {
... on Author @defer {
id
}
id
}
}
}
Expand Down Expand Up @@ -428,6 +426,84 @@ describe('Execute: Cancellation', () => {
]);
});

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

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

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

abortController.abort();

const result = await resultPromise;

expectJSON(result).toDeepEqual([
{
data: {},
pending: [{ id: '0', path: [] }],
hasNext: true,
},
{
incremental: [
{
data: {
todo: {
id: '1',
text: 'hello world',
author: null,
},
},
errors: [
{
locations: [
{
column: 13,
line: 7,
},
],
message: 'This operation was aborted',
path: ['todo', 'author'],
},
],
id: '0',
},
],
completed: [{ id: '0' }],
hasNext: false,
},
]);
});

it('should stop the execution when aborted mid-mutation', async () => {
const abortController = new AbortController();
const document = parse(`
Expand Down
88 changes: 88 additions & 0 deletions src/execution/__tests__/nonnull-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import { describe, it } from 'mocha';

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

import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js';

Expand Down Expand Up @@ -526,6 +527,93 @@ describe('Execute: handles non-nullable types', () => {
});
});

describe('cancellation with null bubbling', () => {
function nestedPromise(n: number): string {
return n > 0 ? `promiseNest { ${nestedPromise(n - 1)} }` : 'promise';
}
it('returns both errors if insufficiently nested', async () => {
const query = `
{
promiseNonNull,
${nestedPromise(3)}
}
`;

const result = await executeQuery(query, throwingData);
expectJSON(result).toDeepEqual({
data: null,
errors: [
{
message: 'promise',
path: ['promiseNest', 'promiseNest', 'promiseNest', 'promise'],
locations: [{ line: 4, column: 51 }],
},
{
message: 'promiseNonNull',
path: ['promiseNonNull'],
locations: [{ line: 3, column: 9 }],
},
],
});
});

it('returns only a single error if sufficiently nested', async () => {
const query = `
{
promiseNonNull,
${nestedPromise(4)}
}
`;

const result = await executeQuery(query, throwingData);
expectJSON(result).toDeepEqual({
data: null,
errors: [
// does not include syncNullError because result returns prior to it being added
{
message: 'promiseNonNull',
path: ['promiseNonNull'],
locations: [{ line: 3, column: 11 }],
},
],
});
});

it('keeps running despite error', async () => {
const query = `
{
promiseNonNull,
${nestedPromise(10)}
}
`;

let counter = 0;
const rootValue = {
...throwingData,
promiseNest() {
return new Promise((resolve) => {
counter++;
resolve(rootValue);
});
},
};
const result = await executeQuery(query, rootValue);
expectJSON(result).toDeepEqual({
data: null,
errors: [
{
message: 'promiseNonNull',
path: ['promiseNonNull'],
locations: [{ line: 3, column: 11 }],
},
],
});
const counterAtExecutionEnd = counter;
await resolveOnNextTick();
expect(counter).to.equal(counterAtExecutionEnd);
});
});

describe('Handles non-null argument', () => {
const schemaWithNonNullArg = new GraphQLSchema({
query: new GraphQLObjectType({
Expand Down
Loading

0 comments on commit 1387e36

Please sign in to comment.