-
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
Subscriptions support #846
Conversation
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.
So excited about this addition!
src/subscription/subscribe.js
Outdated
addPath(undefined, responseName) | ||
); | ||
|
||
// TODO: handle the error |
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 is the biggest open question in my mind. What should happen if an error is encountered not while producing the result for a published payload, but when originally setting up the subscription?
My thinking is that this should probably have a very similar kind of error as if you submitted a subscription request that violated a validation rule.
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.
Hm, do we consider the initial response to be privileged in some way?
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'm not sure if privileged is how I would characterize it, but it's certainly different. There's a difference between failing to create a subscription and an error during the first published payload. Clients attempting to subscribe need to be given the appropriate info to distinguish between the two.
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.
In graphql-subscriptions
, in case of an error during the creation of the subscription we returned the error as a subscription error immediately (here, and a test for it).
We didn't see people complaining about this approach, and it looks like that the timing of the error was enough until now for people to understand which type of error occurred.
I've changed it, and now errors thrown by resolveFieldValueOrError
will be thrown (and the developer need to fix it, just like the Subscription must return Async Iterable
error).
Do you think it makes sense?
|
||
const pubsub = new EventEmitter(); | ||
|
||
const createSubscription = pubsub => { |
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.
minor style detail: functions defined at the top level we use function createSubscription(pubsub) {
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.
Fixed
null, // context | ||
{ priority: 1 } | ||
), | ||
sendImportantEmail, |
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.
small thing, but moving this to the first key in this return object would make it more obvious that more than one thing are being returned, otherwise it's easy to mistake this as another argument to subscribe
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.
Fixed
fields: { | ||
importantEmail: { | ||
type: GraphQLString, | ||
resolve: () => 'test', |
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 think you mean to test subscribe: () => 'test'
here?
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.
Another test to add (which will not have correct behavior just yet) is if a subscription resolver returns an error or throws one: subscribe: () => new Error('test')
or subscribe: () => { throw new Error('test'); }
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.
Fixed the first one, and added a test case for the error
ast, | ||
null, | ||
null, // context | ||
{ priority: 1 }); |
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.
note: all the {priority: 1}
aren't being used by the test AST (those are variables), so these tests could all be simplified to subscribe(schema, ast)
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.
Removed
}).not.to.throw(); | ||
}); | ||
|
||
it('throws when subscribe does not return a valid iterator', () => { |
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.
Is this the same test as the one on line 404?
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.
Removed
I noticed that all test subscription types have only a single field ( |
@OlegIlyenko that's reasonable given that the spec currently only allows one field right? |
is there a reason to introduce new subscribe callback? |
I think a core part of the design here is that subscription resolvers are not special, and in fact could be called from an entirely separate service. In one production ready design for subscriptions, in particular the one used at Facebook, the GraphQL API service is completely stateless and called once for every subscription event. If resolvers returned an iterator, that architecture wouldn't really be possible. |
@stubailo not sure how current approach fixes it, |
@stubailo I don't mean 2 fields in the query but rather in the schema. E.g. const SubscriptionType = new GraphQLObjectType({
name: 'Subscription',
fields: {
importantEmail: { type: EmailEventType },
newContact: { type: Person },
}
}); query still can use only one of these fields.
From what I can tell, proposed changes to the spec indeed allow to query only a single field. Though this feature is still in PR stage, so it's subject for a discussion, if I understand it correctly. |
schema, | ||
document, | ||
payload, | ||
contextValue, |
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.
Can there be some way to customize this context, possibly by adding some way to preprocess the arguments to execute
here? (This could go well with converting these function signatures to take objects, per #356.)
In our current implementation, we use time-limited auth tokens. For our subscription implementation, we allow the client to update the auth token used.
This auth token is bound to the client session (it comes from somewhere separate). Implementation-wise, we currently swap out the context used for resolving subscriptions, which allows us to continue to use an up-to-date auth token without e.g. tearing down and recreating all the subscriptions.
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.
@taion could you offer a suggestion as to how this might impact the actual reference implementation? One of the goals here is to illustrate how subscriptions are a thin layer atop the existing execution operation.
By the way, the pattern you're talking about is often implemented in pure-functional systems using an "atom", which is essentially a mutable reference point. I could imagine that rather than passing the auth token directly as context, you could pass an object which contains a reference to an auth token, that way when you need to update the auth token, you can just mutate the context object without requiring tearing down and recreating subscriptions.
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 could hold onto a reference to the context
with which I called subscribe
, and mutate it, but that feels ugly.
This subscribe
function is what I call to set up a subscription, right? The most light-touch way to do this would be to just allow a custom execute
callback. That gives a fairly generic way for me to modify the context
passed down to execute
.
@DxCx - there's an important distinction to be made between the creation of a subscription and the execution of a response per each published event from that subscription. Critically, as @stubailo pointed out, these two steps may occur on different services. In that case, it's important for the execution portion of the subscription to remain separately invokable and not special from the GraphQL executor's point of view. |
How does unsubscribing work if |
Thinking about this a little more, I'm not sure this is the best or simplest API possible. I do see and appreciate the elegance of However, I think the API could be significantly simpler and more flexible if control were un-inverted. Instead of This sort of API would also allow patterns like centralizing subscription-level auth checks. Additionally, it would make it easier to handle unsubscribing, since the opaque |
Concretely, I'm suggesting that Alternatively, it would also make sense to both expose the lower-level API that I propose, and then build the current API on top of that lower-level API. |
Oops, my bad, didn't notice the |
This idea is reasonable and easy to add - we simply need to move the first two steps of |
I would suggest |
I would suggest calling it something like I'd argue that in this pattern, it's possible and perhaps even desirable for the |
Good feedback. "resolve" already has a meaning here as a more specific function that is part of what we're talking about - resolving a subscription event source refers to getting the event stream (asynciterable) for a specific subscription field - it's input is a Right now, there isn't a substantial difference between the two since we're starting with limiting subscriptions to include only one top level field - so there is a 1:1 relationship at play. However if we decide in the future to allow multiple top level fields, then this top level function would be responsible for merging event streams. I suggested |
…ion fields and multiple schema subscriptions
@OlegIlyenko I added test cases for multiple subscriptions defined in the schema (and query for one of them), and multiple subscriptions defined in schema and query for two of them (throws error). In case of Do you still think we need to add tests for specific types? we have tests that checks the return value of |
I was imagining that |
@taion can you clarify what you mean by:
Wouldn't you want to use an opaque wrapper to remove the ability to operate on the response rather than adding new capabilities? Perhaps I don't fully understand the use case you have in mind. |
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.
So close!!
): AsyncIterator<U> { | ||
// Fixes a temporary issue with Regenerator/Babel | ||
// https://github.com/facebook/regenerator/pull/290 | ||
const iterator = iterable.next ? (iterable: any) : getAsyncIterator(iterable); |
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 issue was fixed! Pretty sure this can now be safely replaced with const iterator = getAsyncIterator(iterable);
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.
facebook/regenerator#290 was merged but not included in a release yet. maybe we make release happen? (for regenerator-transform
and babel-plugin-transform-regenerator
).
cc @benjamn
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 believe regenerator-transform
and babel-plugin-transform-regenerator
automatically include the latest point releases of regenerator
, but explicitly bumping their minimum required version would be a good idea to help close the gap on this bug
src/subscription/subscribe.js
Outdated
); | ||
|
||
// TODO: make GraphQLSubscription flow type special to support defining these? | ||
const resolveFn = (fieldDef: any).subscribe || defaultFieldResolver; |
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.
To fix this (and remove the any cast) I think you can just add a subscribe
entry that mirrors resolve
to GraphQLField
and GraphQLFieldConfig
in type/definition.js. Should probably also add a similar use of isValidResolver
(L527).
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've added subscribe
to the Flow types.
Are you sure that isValidResolver
can help us here? because isValidResolver
allows null
or function
as resolvers, but in this case we want to enforce a resolver (and disallow null
value).
In our case we want to make sure that subscribe
resolver exists and returns a valid AsyncIterable
, otherwise we can't resolve the subscription.
I take back what I said – my current API design is a bit silly, in that I have my subscribe resolvers return Redis topics. It would be cleaner for them to return async iterables. That said, is there some valid use case where the service processing subscription requests is not the service dispatching subscription updates? Maybe some sort of persistent subscriptions, like for push notifications? Or is that best handled through a different mechanism than this? |
This is absolutely a valid use case. However if you're spreading work across services like this, then you're probably not using AsyncIterators to get the job done. That's totally fine. |
@leebyron In which case, |
Actually, in which case |
It'd be nice to have some way to run the subscription's |
@taion There is an implementation for Redis with GraphQL subscription, but it need some changes after merging this PR to support Note that you can always wrap every |
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 just went over this PR with latest spec PR side-by-side. See inline comments.
ast, | ||
null, | ||
null); | ||
}).to.throw('A subscription must contain exactly one field.'); |
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.
Maybe word this as "A subscription operation must contain exactly one root field"?
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.
Done!
null); | ||
}).to.throw('test error'); | ||
}); | ||
}); |
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.
Can we add another test case with two subscriptions on importantEmail and verify that they both receive the payload?
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.
Done!
src/subscription/subscribe.js
Outdated
OperationDefinitionNode, | ||
} from '../language/ast'; | ||
|
||
export function getSubscriptionEventSource( |
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.
Nit: can we rename this to "createSourceEventStream" to be consistent with the spec language?
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.
Agree except it should probably clarify that it is part of the subscription API in the name, yeah? How about createSubscriptionSourceEventStream
? Long but more accurate. We could change the name in the spec too if matching is desirable
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 slightly prefer "CreateSourceEventStream". In the spec, "subscription" is fairly obvious based on context.
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.
Changed to createSubscriptionSourceEventStream
, is that ok?
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.
Let's go with "CreateSourceEventStream"
addPath(undefined, responseName) | ||
); | ||
|
||
const subscription = resolveFieldValueOrError( |
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.
Might be useful to add a comment that this mirrors the "ResolveFieldEventStream" step in the spec algo: https://github.com/robzhu/graphql/blob/master/spec/Section%206%20--%20Execution.md
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.
Do you want me to add this comment along with other comments to mirror the spec? or only this specific one?
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.
Yeah, I think comments are useful wherever the function names diverge from the spec or where the implementation doesn't match the spec algo step-by-step.
src/execution/values.js
Outdated
@@ -191,7 +191,7 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed { | |||
const itemType = type.ofType; | |||
if (isCollection(_value)) { | |||
const coercedValues = []; | |||
const valueIter = createIterator(_value); | |||
const valueIter = createIterator((_value: any)); |
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.
Could you remove this line and rebase this PR from master branch again?
Some of the less-related parts of this PR have been already merged into master
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.
Done!
# Conflicts: # package.json
I agree that we should follow this up by adding in some additional comments. It's always nice to reference the spec from the doc blocks above functions. Similarly, createSubscriptionSourceEventStream is exported but doesn't have a doc block. It should include a description of what its for and how to use it. And specifically call out the use case of building a cross-service subscriptions installation. |
woohoo! 🎉 |
@leebyron can you give an example of how cross-service subscriptions should work? |
This adds an argument to `execute()`, `createSourceEventStream()` and `subscribe()` which allows for providing a custom field resolver. For subscriptions, this allows for externalizing the resolving of event streams to a separate function (mentioned in #846) or to provide a custom function for externalizing the resolving of values (mentioned in #733) cc @stubailo @helfer
This adds an argument to `execute()`, `createSourceEventStream()` and `subscribe()` which allows for providing a custom field resolver. For subscriptions, this allows for externalizing the resolving of event streams to a separate function (mentioned in #846) or to provide a custom function for externalizing the resolving of values (mentioned in #733) cc @stubailo @helfer
This adds an argument to `execute()`, `createSourceEventStream()` and `subscribe()` which allows for providing a custom field resolver. For subscriptions, this allows for externalizing the resolving of event streams to a separate function (mentioned in #846) or to provide a custom function for externalizing the resolving of values (mentioned in #733) cc @stubailo @helfer
This is a PR to add
subscriptions
support to the official Javascript reference implementation.This is based on the new PR to the official GraphQL spec by @robzhu - graphql/graphql-spec#305
This is a joint work of the GraphQL team on Facebook, Apollo and many people from the community, so thanks to everyone who used Subscriptions till today and gave feedback and PRs for the early libraries implementations we've created.
All the existing Apollo libraries for GraphQL Subscriptions that you use today are ready for the new upgrade and would be released once this PR will be merged.
To track those changes in advance check out those PRs: