Skip to content

Commit

Permalink
feat: execution cancelation (#3197)
Browse files Browse the repository at this point in the history
* feat: forward signal to executor

* chore(dependencies): updated changesets for modified dependencies

* Update tests

* chore(dependencies): updated changesets for modified dependencies

* use fixed executor version

* correct message

* test: DOMException thrown from resolver

* add changeset

* integration tests

* no global

* chore(dependencies): updated changesets for modified dependencies

* alpha release testing (#3208)

* alpha release testing

* Try the new alpha fastify

* fastify subscription cleanup test

* update integration guides

* use proper version

* chore(dependencies): updated changesets for modified dependencies

---------

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* purge changeset

* remove unused import

* update changeset

* :shrug"

* chore(dependencies): updated changesets for modified dependencies

* defer stream cancelation

* use helper function

* make request cancellation opt-in

* gbae

* docs

* spelling

* more spelling and resources

* leaks

* them leaks

* chore(dependencies): updated changesets for modified dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
3 people authored Mar 29, 2024
1 parent f89a1aa commit f775b34
Show file tree
Hide file tree
Showing 52 changed files with 1,493 additions and 239 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-yoga/plugin-apollo-inline-trace': patch
---
dependencies updates:
- Updated dependency [`@whatwg-node/fetch@^0.9.17`
↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.9.17) (from `^0.9.7`, in
`peerDependencies`)
13 changes: 13 additions & 0 deletions .changeset/graphql-yoga-3197-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'graphql-yoga': patch
---
dependencies updates:
- Updated dependency [`@graphql-tools/executor@^1.2.5`
↗︎](https://www.npmjs.com/package/@graphql-tools/executor/v/1.2.5) (from `^1.2.2`, in
`dependencies`)
- Updated dependency [`@whatwg-node/fetch@^0.9.17`
↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.9.17) (from `^0.9.7`, in
`dependencies`)
- Updated dependency [`@whatwg-node/server@^0.9.32`
↗︎](https://www.npmjs.com/package/@whatwg-node/server/v/0.9.32) (from `^0.9.1`, in
`dependencies`)
30 changes: 30 additions & 0 deletions .changeset/green-badgers-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'graphql-yoga': minor
---

Experimental support for aborting GraphQL execution when the HTTP request is canceled.

The execution of subsequent GraphQL resolvers is now aborted if the incoming HTTP request is canceled from the client side.
This reduces the load of your API in case incoming requests with deep GraphQL operation selection sets are canceled.

```ts
import { createYoga, useExecutionCancellation } from 'graphql-yoga'

const yoga = createYoga({
plugins: [useExecutionCancellation()]
})
```

[Learn more in our docs](https://graphql-yoga.com/docs/features/execution-cancelation)

**Action Required** In order to benefit from this new feature, you need to update your integration setup for Fastify, Koa and Hapi.

```diff
- const response = await yoga.handleNodeRequest(req, { ... })
+ const response = await yoga.handleNodeRequestAndResponse(req, res, { ... })
```

Please refer to the corresponding integration guides for examples.
- [Fastify](https://graphql-yoga.com/docs/integrations/integration-with-fastify#example)
- [Koa](https://graphql-yoga.com/docs/integrations/integration-with-koa#example)
- [Hapi](https://graphql-yoga.com/docs/integrations/integration-with-hapi#example)
2 changes: 1 addition & 1 deletion examples/apollo-federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
},
"devDependencies": {
"@apollo/gateway": "2.4.7",
"@whatwg-node/fetch": "^0.9.0"
"@whatwg-node/fetch": "^0.9.17"
}
}
2 changes: 1 addition & 1 deletion examples/bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"graphql-yoga": "5.2.0"
},
"devDependencies": {
"@whatwg-node/fetch": "^0.9.0"
"@whatwg-node/fetch": "^0.9.17"
}
}
2 changes: 1 addition & 1 deletion examples/cloudflare-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"devDependencies": {
"@cloudflare/workers-types": "4.20230518.0",
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"typescript": "5.1.6",
"wrangler": "3.1.0"
}
Expand Down
10 changes: 0 additions & 10 deletions examples/defer-stream/__integration-tests__/defer-stream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { yoga } from '../src/yoga';

describe('Defer / Stream', () => {
it('stream', async () => {
const start = Date.now();
const response = await yoga.fetch('/graphql', {
method: 'POST',
headers: {
Expand All @@ -21,14 +20,9 @@ describe('Defer / Stream', () => {
const contentType = response.headers.get('Content-Type');
expect(contentType).toEqual('multipart/mixed; boundary="-"');
const responseText = await response.text();
const end = Date.now();
expect(responseText).toMatchSnapshot('stream');
const diff = end - start;
expect(diff).toBeLessThan(2650);
expect(diff > 2550).toBeTruthy();
});
it('defer', async () => {
const start = Date.now();
const response = await yoga.fetch('/graphql', {
method: 'POST',
headers: {
Expand All @@ -50,10 +44,6 @@ describe('Defer / Stream', () => {
const contentType = response.headers.get('Content-Type');
expect(contentType).toEqual('multipart/mixed; boundary="-"');
const responseText = await response.text();
const end = Date.now();
expect(responseText).toMatchSnapshot('defer');
const diff = end - start;
expect(diff).toBeLessThan(1600);
expect(diff > 1450).toBeTruthy();
});
});
2 changes: 1 addition & 1 deletion examples/error-handling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"start": "ts-node src/index.ts"
},
"dependencies": {
"@whatwg-node/fetch": "^0.9.7",
"@whatwg-node/fetch": "^0.9.17",
"graphql": "^16.1.0",
"graphql-yoga": "5.2.0"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/fastify-modules/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function createGraphQLHandler(): RouteHandlerMethod & {
});

const handler = async (req, reply) => {
const response = await graphQLServer.handleNodeRequest(req, {
const response = await graphQLServer.handleNodeRequestAndResponse(req, reply, {
req,
reply,
});
Expand Down
126 changes: 123 additions & 3 deletions examples/fastify/__integration-tests__/fastify.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import request from 'supertest';
import { fetch } from '@whatwg-node/fetch';
import { eventStream } from '../../../packages/graphql-yoga/__tests__/utilities.js';
import { buildApp } from '../src/app.js';

describe('fastify example integration', () => {
const [app] = buildApp(false);
let app: ReturnType<typeof buildApp>[0];

beforeAll(async () => {
beforeEach(async () => {
[app] = buildApp(false);
await app.ready();
});

afterAll(async () => {
afterEach(async () => {
await app.close();
});

Expand Down Expand Up @@ -176,6 +179,7 @@ event: complete
data"
`);
});

it('handles subscription operations via POST', async () => {
const response = await request(app.server)
.post('/graphql')
Expand Down Expand Up @@ -229,6 +233,7 @@ event: complete
data"
`);
});

it('should handle file uploads', async () => {
const response = await request(app.server)
.post('/graphql')
Expand All @@ -251,4 +256,119 @@ data"
},
});
});

it('request cancelation', async () => {
const slowFieldResolverInvoked = createDeferred();
const slowFieldResolverCanceled = createDeferred();
const address = await app.listen({
port: 0,
});

// we work with logger statements to detect when the slow field resolver is invoked and when it is canceled
const loggerOverwrite = (part: unknown) => {
if (part === 'Slow resolver invoked') {
slowFieldResolverInvoked.resolve();
}
if (part === 'Slow field got cancelled') {
slowFieldResolverCanceled.resolve();
}
};

const info = app.log.info;
app.log.info = loggerOverwrite;
app.log.debug = loggerOverwrite;

try {
const abortController = new AbortController();
const response$ = fetch(`${address}/graphql`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
query: /* GraphQL */ `
query {
slow {
field
}
}
`,
}),
signal: abortController.signal,
});

await slowFieldResolverInvoked.promise;
abortController.abort();
await expect(response$).rejects.toMatchInlineSnapshot(
`[AbortError: The operation was aborted]`,
);
await slowFieldResolverCanceled.promise;
} finally {
app.log.info = info;
}
});

it('subscription cancelation', async () => {
const cancelationIsLoggedPromise = createDeferred();
const address = await app.listen({
port: 0,
});

// we work with logger statements to detect when the subscription source is cleaned up.
const loggerOverwrite = (part: unknown) => {
if (part === 'countdown aborted') {
cancelationIsLoggedPromise.resolve();
}
};

const info = app.log.info;
app.log.info = loggerOverwrite;

try {
const abortController = new AbortController();
const url = new URL(`${address}/graphql`);
url.searchParams.set(
'query',
/* GraphQL */ `
subscription {
countdown(from: 10, interval: 5)
}
`,
);
const response = await fetch(url, {
method: 'GET',
headers: {
'content-type': 'application/json',
accept: 'text/event-stream',
},
signal: abortController.signal,
});

const iterator = eventStream(response.body!);
const next = await iterator.next();
expect(next.value).toEqual({ data: { countdown: 10 } });
abortController.abort();
await expect(iterator.next()).rejects.toMatchInlineSnapshot(
`[AbortError: The operation was aborted]`,
);
await cancelationIsLoggedPromise.promise;
} finally {
app.log.info = info;
}
});
});

type Deferred<T = void> = {
resolve: (value: T) => void;
reject: (value: unknown) => void;
promise: Promise<T>;
};

function createDeferred<T = void>(): Deferred<T> {
const d = {} as Deferred<T>;
d.promise = new Promise<T>((resolve, reject) => {
d.resolve = resolve;
d.reject = reject;
});
return d;
}
1 change: 1 addition & 0 deletions examples/fastify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"start": "ts-node src/index.ts"
},
"dependencies": {
"@whatwg-node/fetch": "^0.9.17",
"fastify": "4.17.0",
"graphql-yoga": "5.2.0",
"pino-pretty": "10.0.0"
Expand Down
Loading

0 comments on commit f775b34

Please sign in to comment.