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

Update subscribe function to return a Promise of AsyncIterator or ExecutionResult #918

Merged
merged 19 commits into from
Aug 14, 2017

Conversation

robzhu
Copy link
Contributor

@robzhu robzhu commented Jun 16, 2017

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 no value 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:

try {
  const result = await subscribe(...);
  if (result.errors) {
    // errors are informative and can be sent to the client
  } else {
    // result is guaranteed to be an AsyncIterable at this point
    return result;
  }
} catch (error) {
  // system error, log the problem and send a failure response to the client.
}

leebyron and others added 6 commits May 30, 2017 13:33
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.
leebyron and others added 8 commits June 18, 2017 14:58
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.
Copy link
Contributor

@leebyron leebyron left a 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!

@@ -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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise<AsyncIterator | ExecutionResult>

Copy link
Contributor

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.

* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise rejected?

@@ -59,7 +69,7 @@ declare function subscribe(
operationName?: ?string,
fieldResolver?: ?GraphQLFieldResolver<any, any>,
subscribeFieldResolver?: ?GraphQLFieldResolver<any, any>
): AsyncIterator<ExecutionResult>;
): Promise<AsyncIterator<ExecutionResult> | Error | ExecutionResult>;
Copy link
Contributor

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

@@ -96,6 +107,16 @@ export function subscribe(
);
}

// This function checks if the error is a GraphQLError. If it is, convert it to
Copy link
Contributor

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.

fieldResolver
)
);
).then(subscription => {
Copy link
Contributor

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

),
convertOrThrowError
);
}).catch(convertOrThrowError);
Copy link
Contributor

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.

Copy link
Contributor Author

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.

if (subscription instanceof Error) {
throw subscription;
}
return Promise.resolve(subscription).then(resolvedSubscription => {
Copy link
Contributor

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()

Copy link
Contributor Author

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.

Copy link
Contributor Author

@robzhu robzhu Jul 11, 2017

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.

Copy link
Contributor Author

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

@cedrelek
Copy link

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(
Copy link
Contributor

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.

@leebyron
Copy link
Contributor

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants