-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Update subscribe function to return a Promise of AsyncIterator or ExecutionResult #918
Conversation
Currently the `subscribe()` function throws Errors, however this is awkward when used along with async functions which would expect a rejected iteration to represent failure. Also GraphQLErrors should be reported back to the client since they represent client-provided errors. Updates test cases to represent this new behavior. Includes a new utility `asyncIteratorReject`, and extends the behavior of `mapAsyncIterator` to help implement this.
If a subscribe resolve function throws or returns an error, that typically indicates an issue to be returned to the requesting client. This coerces errors into located GraphQLErrors so they are correctly reported.
After discussion in #868, decided that errors emitted from a source event stream should be considered "internal" errors and pass through. However errors encountered during GraphQL execution on a source event should be considered "field" or "query" errors and be represented within that Response.
Currently the `subscribe()` function throws Errors, however this is awkward when used along with async functions which would expect a rejected iteration to represent failure. Also GraphQLErrors should be reported back to the client since they represent client-provided errors. Updates test cases to represent this new behavior. Includes a new utility `asyncIteratorReject`, and extends the behavior of `mapAsyncIterator` to help implement this.
If a subscribe resolve function throws or returns an error, that typically indicates an issue to be returned to the requesting client. This coerces errors into located GraphQLErrors so they are correctly reported.
After discussion in #868, decided that errors emitted from a source event stream should be considered "internal" errors and pass through. However errors encountered during GraphQL execution on a source event should be considered "field" or "query" errors and be represented within that Response.
…ql/graphql-js into subscription-promise-union
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks awesome. Sorry for the long wait on a review. I've added a few line items for improvements and some additional suggested tests, but this API is much better.
A follow-up to this should be to extend the top level graphql()
function to directly support subscriptions!
src/subscription/subscribe.js
Outdated
@@ -32,10 +35,17 @@ import type { GraphQLFieldResolver } from '../type/definition'; | |||
/** | |||
* Implements the "Subscribe" algorithm described in the GraphQL specification. | |||
* | |||
* Returns an AsyncIterator | |||
* Returns a Promise<AsyncIterator | Error | ExecutionResult> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Promise<AsyncIterator | ExecutionResult>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe it's easier to read if this spells it out. Something like:
Returns a Promise which resolves to either an ExecutionResult (indicating failure to subscribe) or an AsyncIterator of ExecutionResult values, triggered by the underlying subscribed event stream.
src/subscription/subscribe.js
Outdated
* If the arguments to this function do not result in a legal execution context, | ||
* a GraphQLError will be thrown immediately explaining the invalid input. | ||
* If the the source stream could not be created due to faulty subscription | ||
* resolver logic or underlying systems, an Error will be returned, not thrown. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Promise rejected?
src/subscription/subscribe.js
Outdated
@@ -59,7 +69,7 @@ declare function subscribe( | |||
operationName?: ?string, | |||
fieldResolver?: ?GraphQLFieldResolver<any, any>, | |||
subscribeFieldResolver?: ?GraphQLFieldResolver<any, any> | |||
): AsyncIterator<ExecutionResult>; | |||
): Promise<AsyncIterator<ExecutionResult> | Error | ExecutionResult>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type should match the type above
src/subscription/subscribe.js
Outdated
@@ -96,6 +107,16 @@ export function subscribe( | |||
); | |||
} | |||
|
|||
// This function checks if the error is a GraphQLError. If it is, convert it to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits:
Use /* for multi-line doc blocks.
Also, it would be nice to move this fn lower in the file - keeping subscribe()
and subscribeImpl()
directly next to each other in the file.
src/subscription/subscribe.js
Outdated
fieldResolver | ||
) | ||
); | ||
).then(subscription => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove the wrapping {}
and return
since this arrow function is of a single expression
src/subscription/subscribe.js
Outdated
), | ||
convertOrThrowError | ||
); | ||
}).catch(convertOrThrowError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can provide convertOrThrowError
as the 2nd argument to then()
- which both reduces the number of wrapped promises, but also avoids calling convertOrThrowError
two times in the case an error is thrown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved convertOrThrowError to then and pulled out MapSourceToResponseEvent definition to align more clearly with the spec.
src/subscription/subscribe.js
Outdated
if (subscription instanceof Error) { | ||
throw subscription; | ||
} | ||
return Promise.resolve(subscription).then(resolvedSubscription => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildExecutionContext
above may throw a GraphQLError if client-provided values don't coerce properly - those should be properly formatted in the response via returning a rejected promise. You can test this by passing bad variable values along with a query.
The fix should probably be to wrap return new Promise(resolve => {})
starting above line 196, ending in resolve(subscription)
and followed by this .then()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean returning an ExecutionResult instead of rejecting the Promise? Bad variables seems to be an error case for which we can give the client useful feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a test case that validates bad query variables are returned as an ExecutionResult. However, the return new Promise
approach you suggested led to pretty gnarly logic for dealing with the nested promise coming back from resolveFieldValueOrError. The current version uses await/async to work around it, but happy to revise if you don't think the tradeoff in code clarity is worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's what the code looks like using a wrapped promise: https://gist.github.com/robzhu/d2d9e3238b586e67569646a5944154b8
Thx for this. I hope the next release will include this PR :) |
* | ||
* This may be useful when hosting the stateful subscription service in a | ||
* different process or machine than the stateless GraphQL execution engine, | ||
* or otherwise separating these two steps. For more on this, see the | ||
* "Supporting Subscriptions at Scale" information in the GraphQL specification. | ||
*/ | ||
export function createSourceEventStream( | ||
export async function createSourceEventStream( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My only mild concern is that by including an async function in non-test code that we may need to add a dependency to package.json for an async function runtime.
As a follow-up task, let's test this to see if we need to do so, ideally we can ensure no new dependencies.
No excuse for my long delay on this supremely excellent PR. Error behavior is so difficult, and getting all the corner cases correct and well tested deserves a serious shout out. Thank you for making GraphQL more awesome! |
This change updates the Subscribe() function to return a Promise<AsyncIterator | ExecutionResult>.
The reason for this change is to allow callers to distinguish between initialization and publish errors.
When calling
subscribe
:If the operation is successful, returns an AsyncIterator representing the response stream.
If the operation failed with an informative error for the client, returns an ExecutionResult with an
errors
field but novalue
field.If the operation failed due to a system related problem, such as an unavailable pubsub, system, an error will be thrown.
Example of how to call
subscribe
after these changes: