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

feat: execution cancelation #3197

Merged
merged 27 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e53b90f
feat: forward signal to executor
n1ru4l Mar 9, 2024
bef0925
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 9, 2024
9c04ced
Update tests
ardatan Mar 11, 2024
929d6d6
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 11, 2024
5d4979c
use fixed executor version
n1ru4l Mar 21, 2024
036d83f
correct message
n1ru4l Mar 21, 2024
14109e4
test: DOMException thrown from resolver
n1ru4l Mar 21, 2024
944ee2e
add changeset
n1ru4l Mar 21, 2024
ae5260f
integration tests
n1ru4l Mar 25, 2024
b2aa2b3
no global
n1ru4l Mar 25, 2024
730a80e
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 25, 2024
f081ce4
alpha release testing (#3208)
n1ru4l Mar 25, 2024
02be538
purge changeset
n1ru4l Mar 25, 2024
f059e05
remove unused import
n1ru4l Mar 25, 2024
a1d1145
update changeset
n1ru4l Mar 25, 2024
2bdc07c
:shrug"
n1ru4l Mar 25, 2024
ce185de
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 25, 2024
3e8d114
defer stream cancelation
n1ru4l Mar 28, 2024
6fe67d1
use helper function
n1ru4l Mar 28, 2024
4169e42
make request cancellation opt-in
n1ru4l Mar 28, 2024
4aeeb6c
gbae
n1ru4l Mar 28, 2024
9a07506
docs
n1ru4l Mar 28, 2024
b0174c3
spelling
n1ru4l Mar 28, 2024
aeba523
more spelling and resources
n1ru4l Mar 28, 2024
7233a02
leaks
n1ru4l Mar 29, 2024
a1c3467
them leaks
n1ru4l Mar 29, 2024
a0b4d86
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Mar 29, 2024
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
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
Loading