Skip to content

Commit db17ed1

Browse files
authored
Make the plugin (and gateway) API almost entirely async (#5295)
Before this PR, some plugin methods were sync, some were async, and some returned ValueOrPromise (ie, could be sync or async). This led to a lack of functionality for the never-async methods, confusing inconsistency, and generally vagueness as to what types things are. It's 2021: making a function async is as simple as adding the word `async` and maybe wrapping a return type in `Promise<>`. This PR simplifies matters by making almost every plugin and gateway method always async. The one exception is `willResolveField`, which is called far more than any other plugin method. We already know that schema instrumentation has unfortunate performance overhead so adding to it by adding more Promises to every field doesn't seem like a good idea for now. We reserve the write to change `willResolveField` to a `PromiseOrValue` sometimes-async method later. (Note that in practice, we usually either `await` or call `Promise.all` on the return values from plugins rather than directly calling `.then`, so if you're not using TypeScript you can probably get away with writing sync methods; and if you are using TypeScript the compiler will quickly show you where you're missing your `async`s.) Specifically, this PR: - Makes the following formerly-`ValueOrPromise` methods into async methods: `serverWillStart`, `serverWillStop`, `didResolveSource`, `didResolveOperation`, `didEncounterErrors`, `responseForOperation`, `willSendResponse` - Makes the following formerly-sync methods into async methods: `requestDidStart`, `renderLandingPage`, `parsingDidStart` and its stop hook, `validationDidStart` and its stop hook, `executionDidStart`, `executionDidEnd` - `executionWillStart` can no longer longer return an end hook as a function; `executionWillEnd` must be returned in an object (which was one of two options before) - Changes the methods on `Dispatcher` so that there's only one "sync" method (for `willResolveField`) and the other methods aren't explicitly named with `Async` - Simplifies the `GraphQLService` interface (used for `@apollo/gateway`) to require the `stop` method to exist, to require `executor` to be async, and to note that we always pass `apollo` to `load`. (All recent versions of `@apollo/gateway` satisfy this.) Also require the `GraphQLExecutor` function type to be async. This also avoids the use of `ValueOrPromise<void>`, a somewhat confusing type that means "either a Promise or a return value we shouldn't look at" though we do still have `Promise<X|void>` types. Note that we allow `options` and `context` functions to still have `ValueOrPromise` semantics. Fixes #4103. Fixes #4999. Incorporates work by @lucasconstantino from #4050 and #4051.
1 parent 36410aa commit db17ed1

File tree

22 files changed

+305
-302
lines changed

22 files changed

+305
-302
lines changed

docs/source/api/plugin/usage-reporting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ The only properties of the reported error you can modify are its `message` and i
113113

114114
Specify this asynchronous function to configure which requests are included in usage reports sent to Apollo Studio. For example, you can omit requests that execute a particular operation or requests that include a particular HTTP header.
115115

116-
This function is called for each received request. It takes a [`GraphQLRequestContext`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L115-L150) object and must return a `Promise<Boolean>` that indicates whether to include the request. It's called either after the operation is successfully resolved (via [the `didResolveOperation` event](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#didresolveoperation)), or after it generates an error (via [the `didEncounterErrors` event](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#didencountererrors)).
116+
This function is called for each received request. It takes a [`GraphQLRequestContext`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-types/src/index.ts#L115-L150) object and must return a `Promise<Boolean>` that indicates whether to include the request. It's called either after the operation is successfully resolved (via [the `didResolveOperation` event](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#didresolveoperation)), or when sending the final error response if the operation was not successfully resolved (via [the `willSendResponse` event](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#willsendresponse)).
117117

118118
By default, all requests are included in usage reports.
119119

docs/source/integrations/plugins.md

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ events. Here's a basic plugin that responds to the `serverWillStart` event:
1919

2020
```js:title=index.js
2121
const myPlugin = {
22-
serverWillStart() {
22+
async serverWillStart() {
2323
console.log('Server starting up!');
2424
},
2525
};
@@ -32,7 +32,7 @@ you can export it as a separate module:
3232

3333
```js:title=myplugin.js
3434
module.exports = {
35-
serverWillStart() {
35+
async serverWillStart() {
3636
console.log('Server starting up!');
3737
},
3838
};
@@ -44,7 +44,7 @@ To create a plugin that accepts options, create a function that accepts an
4444
```js:title=myplugin.js
4545
module.exports = (options) => {
4646
return {
47-
serverWillStart() {
47+
async serverWillStart() {
4848
console.log(options.logMessage);
4949
},
5050
};
@@ -57,7 +57,9 @@ module.exports = (options) => {
5757
A plugin specifies exactly which [events](#apollo-server-event-reference)
5858
it responds to by implementing functions that correspond to those events.
5959
The plugin in the examples above responds to the `serverWillStart` event, which
60-
fires when Apollo Server is preparing to start up.
60+
fires when Apollo Server is preparing to start up. Almost all plugin events
61+
are `async` functions (ie, functions that return `Promise`s); the one exception
62+
is [`willResolveField`](#willresolvefield).
6163

6264
A plugin can respond to any combination of supported events.
6365

@@ -79,7 +81,7 @@ lifecycle:
7981

8082
```js
8183
const myPlugin = {
82-
requestDidStart() {
84+
async requestDidStart() {
8385
console.log('Request started!');
8486
},
8587
};
@@ -92,19 +94,17 @@ just like you respond to `serverWillStart`, but you _also_ use this function
9294

9395
```js
9496
const myPlugin = {
95-
requestDidStart(requestContext) {
97+
asyn crequestDidStart(requestContext) {
9698
console.log('Request started!');
9799

98100
return {
99-
100-
parsingDidStart(requestContext) {
101+
async parsingDidStart(requestContext) {
101102
console.log('Parsing started!');
102103
},
103104

104-
validationDidStart(requestContext) {
105+
async validationDidStart(requestContext) {
105106
console.log('Validation started!');
106107
}
107-
108108
}
109109
},
110110
};
@@ -150,42 +150,47 @@ that is invoked after the corresponding lifecycle phase _ends_:
150150

151151
* [`parsingDidStart`](#parsingdidstart)
152152
* [`validationDidStart`](#validationdidstart)
153-
* [`executionDidStart`](#executiondidstart) (this handler can alternatively return an _object_ containing an `executionDidEnd` function)
154153
* [`willResolveField`](#willresolvefield)
155154

155+
([`executionDidStart`](#executiondidstart) returns an _object_ containing an `executionDidEnd` function instead of just a function as an end handler; that's because the returned object can also contain `willResolveField`.)
156+
157+
Just like the event handers themselves, these end hooks are async functions (except for the end hook for `willResolveField`).
158+
156159
These **end hooks** are passed any errors that occurred during the
157160
execution of that lifecycle phase. For example, the following plugin logs
158161
any errors that occur during any of the above lifecycle events:
159162

160163
```js
161164
const myPlugin = {
162-
requestDidStart() {
165+
async requestDidStart() {
163166
return {
164-
parsingDidStart() {
165-
return (err) => {
167+
async parsingDidStart() {
168+
return async (err) => {
166169
if (err) {
167170
console.error(err);
168171
}
169172
}
170173
},
171-
validationDidStart() {
174+
async validationDidStart() {
172175
// This end hook is unique in that it can receive an array of errors,
173176
// which will contain every validation error that occurred.
174-
return (errs) => {
177+
return async (errs) => {
175178
if (errs) {
176179
errs.forEach(err => console.error(err));
177180
}
178181
}
179182
},
180-
executionDidStart() {
181-
return (err) => {
182-
if (err) {
183-
console.error(err);
183+
async executionDidStart() {
184+
return {
185+
async executionDidEnd(err) {
186+
if (err) {
187+
console.error(err);
188+
}
184189
}
185-
}
186-
}
187-
}
188-
}
190+
};
191+
},
192+
};
193+
},
189194
}
190195
```
191196

@@ -230,7 +235,7 @@ const server = new ApolloServer({
230235

231236
/* This plugin is defined in-line. */
232237
{
233-
serverWillStart() {
238+
async serverWillStart() {
234239
console.log('Server starting up!');
235240
},
236241
}
@@ -252,8 +257,7 @@ Request lifecycle events are associated with a specific request. You define resp
252257

253258
### `serverWillStart`
254259

255-
The `serverWillStart` event fires when Apollo Server is preparing to start serving GraphQL requests. If you respond to this event with an `async` function (or if the function returns a `Promise`), the server doesn't start until the asynchronous operation completes. If the `Promise` is _rejected_, startup _fails_ (**unless you're using [Express middleware](/integrations/middleware/)**). This helps you make sure all
256-
of your server's dependencies are available before attempting to begin serving requests.
260+
The `serverWillStart` event fires when Apollo Server is preparing to start serving GraphQL requests. The server doesn't start until this asynchronous method completes. If it throws (ie, if the `Promise` it returns is _rejected_), startup _fails_ and your server will not serve GraphQL operations. This helps you make sure all of your server's dependencies are available before attempting to begin serving requests. (Specifically, this is fired from the `listen()` method in `apollo-server`, from the `start()` method for a framework integration like `apollo-server-express`, and during the first request for a serverless integration like `apollo-server-lambda`.)
257261

258262
#### Example
259263

@@ -263,7 +267,7 @@ const server = new ApolloServer({
263267

264268
plugins: [
265269
{
266-
serverWillStart() {
270+
async serverWillStart() {
267271
console.log('Server starting!');
268272
}
269273
}
@@ -285,10 +289,10 @@ const server = new ApolloServer({
285289

286290
plugins: [
287291
{
288-
serverWillStart() {
292+
async serverWillStart() {
289293
const interval = setInterval(doSomethingPeriodically, 1000);
290294
return {
291-
serverWillStop() {
295+
async serverWillStop() {
292296
clearInterval(interval);
293297
}
294298
}
@@ -311,9 +315,9 @@ const server = new ApolloServer({
311315

312316
plugins: [
313317
{
314-
serverWillStart() {
318+
async serverWillStart() {
315319
return {
316-
renderLandingPage() {
320+
async renderLandingPage() {
317321
return { html: `<html><body>Welcome to your server!</body></html>` };
318322
}
319323
}
@@ -334,7 +338,7 @@ requestDidStart?(
334338
GraphQLRequestContext<TContext>,
335339
'request' | 'context' | 'logger'
336340
>
337-
): GraphQLRequestListener<TContext> | void;
341+
): Promise<GraphQLRequestListener<TContext> | void>;
338342
```
339343

340344
This function can optionally return an object that includes functions for responding
@@ -346,16 +350,14 @@ const server = new ApolloServer({
346350

347351
plugins: [
348352
{
349-
requestDidStart(requestContext) {
350-
351-
/* Within this returned object, define functions that respond
352-
to request-specific lifecycle events. */
353+
async requestDidStart(requestContext) {
354+
// Within this returned object, define functions that respond
355+
// to request-specific lifecycle events.
353356
return {
354-
355-
/* The `parsingDidStart` request lifecycle event fires
356-
when parsing begins. The event is scoped within an
357-
associated `requestDidStart` server lifecycle event. */
358-
parsingDidStart(requestContext) {
357+
// The `parsingDidStart` request lifecycle event fires
358+
// when parsing begins. The event is scoped within an
359+
// associated `requestDidStart` server lifecycle event.
360+
async parsingDidStart(requestContext) {
359361
console.log('Parsing started!')
360362
},
361363
}
@@ -386,7 +388,7 @@ didResolveSource?(
386388
requestContext: WithRequired<
387389
GraphQLRequestContext<TContext>, 'source' | 'logger'>,
388390
>,
389-
): ValueOrPromise<void>;
391+
): Promise<void>;
390392
```
391393

392394
### `parsingDidStart`
@@ -405,7 +407,7 @@ parsingDidStart?(
405407
GraphQLRequestContext<TContext>,
406408
'metrics' | 'source' | 'logger'
407409
>,
408-
): (err?: Error) => void | void;
410+
): Promise<void | (err?: Error) => Promise<void>>;
409411
```
410412

411413
### `validationDidStart`
@@ -425,7 +427,7 @@ validationDidStart?(
425427
GraphQLRequestContext<TContext>,
426428
'metrics' | 'source' | 'document' | 'logger'
427429
>,
428-
): (err?: ReadonlyArray<Error>) => void | void;
430+
): Promise<void | (err?: ReadonlyArray<Error>) => Promise<void>>;
429431
```
430432

431433
### `didResolveOperation`
@@ -444,7 +446,7 @@ didResolveOperation?(
444446
GraphQLRequestContext<TContext>,
445447
'metrics' | 'source' | 'document' | 'operationName' | 'operation' | 'logger'
446448
>,
447-
): ValueOrPromise<void>;
449+
): Promise<void>;
448450
```
449451

450452
### `responseForOperation`
@@ -460,7 +462,7 @@ responseForOperation?(
460462
GraphQLRequestContext<TContext>,
461463
'metrics' | 'source' | 'document' | 'operationName' | 'operation' | 'logger'
462464
>,
463-
): ValueOrPromise<GraphQLResponse | null>;
465+
): Promise<GraphQLResponse | null>;
464466
```
465467

466468
### `executionDidStart`
@@ -474,10 +476,10 @@ executionDidStart?(
474476
GraphQLRequestContext<TContext>,
475477
'metrics' | 'source' | 'document' | 'operationName' | 'operation' | 'logger'
476478
>,
477-
): (err?: Error) => void | void;
479+
): Promise<GraphQLRequestExecutionListener | void>;
478480
```
479481

480-
`executionDidStart` may return an ["end hook"](#end-hooks) function. Alternatively, it may return an object with one or both of the methods `executionDidEnd` and `willResolveField`. `executionDidEnd` is treated identically to an end hook: it is called after execution with any errors that occurred. `willResolveField` is documented in the next section.
482+
`executionDidStart` may return an object with one or both of the methods `executionDidEnd` and `willResolveField`. `executionDidEnd` is treated like an end hook: it is called after execution with any errors that occurred. `willResolveField` is documented in the next section. (In Apollo Server 2, `executionDidStart` could return also return an end hook directly.)
481483

482484
### `willResolveField`
483485

@@ -487,6 +489,8 @@ You provide your `willResolveField` handler in the object returned by your [`exe
487489

488490
Your `willResolveField` handler can optionally return an ["end hook"](#end-hooks) function that's invoked with the resolver's result (or the error that it throws). The end hook is called when your resolver has _fully_ resolved (e.g., if the resolver returns a Promise, the hook is called with the Promise's eventual resolved result).
489491

492+
`willResolveField` and its end hook are the only synchronous plugin APIs (ie, they do not return `Promise`s).
493+
490494
#### Example
491495

492496
```js
@@ -495,9 +499,9 @@ const server = new ApolloServer({
495499

496500
plugins: [
497501
{
498-
requestDidStart(initialRequestContext) {
502+
async requestDidStart(initialRequestContext) {
499503
return {
500-
executionDidStart(executionRequestContext) {
504+
async executionDidStart(executionRequestContext) {
501505
return {
502506
willResolveField({source, args, context, info}) {
503507
const start = process.hrtime.bigint();
@@ -531,7 +535,7 @@ didEncounterErrors?(
531535
GraphQLRequestContext<TContext>,
532536
'metrics' | 'source' | 'errors' | 'logger'
533537
>,
534-
): ValueOrPromise<void>;
538+
): Promise<void>;
535539
```
536540

537541
### `willSendResponse`
@@ -546,5 +550,5 @@ willSendResponse?(
546550
GraphQLRequestContext<TContext>,
547551
'metrics' | 'response' | 'logger'
548552
>,
549-
): ValueOrPromise<void>;
553+
): Promise<void>;
550554
```

docs/source/monitoring/metrics.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,21 @@ For a list of available lifecycle events and their descriptions, see [Plugins](.
9292

9393
```js
9494
const myPlugin = {
95-
9695
// Fires whenever a GraphQL request is received from a client.
97-
requestDidStart(requestContext) {
96+
async requestDidStart(requestContext) {
9897
console.log('Request started! Query:\n' +
9998
requestContext.request.query);
10099

101100
return {
102-
103101
// Fires whenever Apollo Server will parse a GraphQL
104102
// request to create its associated document AST.
105-
parsingDidStart(requestContext) {
103+
async parsingDidStart(requestContext) {
106104
console.log('Parsing started!');
107105
},
108106

109107
// Fires whenever Apollo Server will validate a
110108
// request's document AST against your GraphQL schema.
111-
validationDidStart(requestContext) {
109+
async validationDidStart(requestContext) {
112110
console.log('Validation started!');
113111
},
114112

packages/apollo-server-core/src/ApolloServer.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,8 @@ export class ApolloServerBase {
448448
if (taggedServerListenersWithRenderLandingPage.length > 1) {
449449
throw Error('Only one plugin can implement renderLandingPage.');
450450
} else if (taggedServerListenersWithRenderLandingPage.length) {
451-
this.landingPage =
452-
taggedServerListenersWithRenderLandingPage[0].serverListener
453-
.renderLandingPage!();
451+
this.landingPage = await taggedServerListenersWithRenderLandingPage[0]
452+
.serverListener.renderLandingPage!();
454453
} else {
455454
this.landingPage = null;
456455
}

packages/apollo-server-core/src/__tests__/logger.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async function triggerLogMessage(loggerToUse: Logger) {
2323
logger: loggerToUse,
2424
plugins: [
2525
{
26-
requestDidStart({ logger }) {
26+
async requestDidStart({ logger }) {
2727
logger.debug(KNOWN_DEBUG_MESSAGE);
2828
},
2929
},

0 commit comments

Comments
 (0)