-
Notifications
You must be signed in to change notification settings - Fork 423
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
Add subscription support #433
Add subscription support #433
Conversation
Hey, awesome that you are working on this! I'll browse the code. |
@theduke this is still quite a WIP and we will come through multiple iterations until it will be ready for a serious review. However, your comments and considerations would be vital for us at any stage. |
Great, thanks for code review. It is quite in some state at the moment: it's possible to create a subscription, map it to some user code with Issues I know about that I will try to resolve today/tomorrow:
|
juniper/src/executor/mod.rs
Outdated
OperationType::Subscription => root_node | ||
.schema | ||
.subscription_type() | ||
.expect("No mutation type found"), |
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: s/mutation/subscription/
This branch is still WIP, but slowly getting there.
|
Ok, I landed @tyranron's PR and rebased the async branch onto master. |
examples/warp_async/Cargo.toml
Outdated
@@ -10,7 +10,7 @@ edition = "2018" | |||
log = "0.4.8" | |||
env_logger = "0.6.2" | |||
warp = "0.1.19" | |||
futures-preview = { version = "0.3.0-alpha.19", features = ["nightly", "async-await", "compat"] } | |||
futures-preview = { version = "0.3.0-alpha.19", features = ["async-await", "compat"] } |
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.
@nWacky async-await
is unnecessary with 0.3.0-alpha.19
Still WIP. This week I added functionality for filtering out response fields (for synchronous subscriptions only). For now it works this way: after subscription fields are resolved and some iterator over user's type is returned, each returned object is treated as a separate query. (for example, got The issue I had was with ...
request is deserialized --->
request.subscribe(root_node, context) --->
...
executor::execute_validated_subcription(...)
// Parse definitions and set default variables
let errors = RwLock::new(Vec::new()); // create a variable to store errors in
// Create `Executor` (&errors and other references are passed there)
executor.resolve_into_iterator(...) // next function for execution process
Ok((value, errors)) //returns execution result and errors
--->
...
value.resolve_into_stream(..., &executor) --->
let iter: Iterator<Obj> = (|| {
// User creates `Iterator<Obj>`
})();
// At this point it's necessary to add user's object resolver
iter.map(|x| executor.resolve_with_ctx(x))
--->
HTTP server got iterator / stream --->
rather return it as a bunch of objects or one by one; So that When we discussed it we decided to create new So (at least for now) I just moved all these variables to a function where let mut executor_variables = juniper::OwnedExecutor::new(); // this is where Executor variables are stored (and it can return Executor)
let mut fragments = vec![]; // this is here to escape `OwnedExecutor` self referencing
let mut executor = juniper::OptionalExecutor::new(); // this is `Option<Executor>` to keep because executor is still declared in `execute_validated_subcription`
let (res, err) = request.subscribe(
root_node,
context,
&mut executor_variables,
&mut fragments,
&mut executor
).0.unwrap(); |
Sorry for the delay, I'll finally be able to carve out some time this weekend. |
It's still in progress, so you didn't miss anything important 🙃 |
We're pretty happy about this PR. Keep up the good work! |
Still WIP, but this push can be considered somewhat like early preview.
What's left:
|
… different modules
Cleaned up code, updated docs, formatted and tested it. Updated description. Everything should be ready for a pre-review now A little side note: benchmarks and server integration crates do not support subscriptions for now and there is a PR for making clippy lints pass. I believe it could be reviewed and merged after this PR is merged (not to make the diff too big) and then further subscription functionality can be added |
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.
WOW, amazing work! Thank you sooooo much 🎉 🍺 🚀 .
I did a review sweep. It looks like all the commits are from you, even the ones already on master. Is that just a weird display thing in github or was there a git mistake? We'll squash this at merge time so maybe it doesn't matter?
I'll wait on doing committing anything on master until this lands to prevent more rebase pain...sorry for that 😥!
async-await
branchasync-await
branch
async-await
branchCo-Authored-By: Christian Legnitto <LegNeato@users.noreply.github.com>
To fix the release stuff you probably need to add |
…riptions # Conflicts: # juniper/src/lib.rs
Before merge: Resolve
TODO#433
(import crates in subscriptions example from GitHub instead of local)Related issue: #54
How to implement custom GraphQL subscription
Note: by
SubscriptionCoordinator
andSubscriptionConnection
I mean types implementing these traitsCurrent execution logic is inspired with this and this idea. In a nutshell:
When a request is received, it gets processed by a
SubscriptionCoordinator
. This struct decides if incoming request should be resolved into a new stream or if one of already existing streams (that coordinator knows about) is good enough.juniper_subscriptions::Coordinator
resolves each request into a new stream.SubscriptionCoordinator
should calljuniper::http::resolve_into_stream
each time it needs to resolve aGraphQLRequest
toValue<Stream<Item = Value<S>>
. If subscripiton-related macros are used,Value::Object<Stream<Item = Value<_>>
is returned. It keeps each field and correspondingStream<Item = Value<_>>
.Object
s can be iterated over and needed GraphQL objects can be assembled from returnedStream
s. When iterating over an object returned from subscription macros, each object field should be mapped toValue::Scalar<StreamT>
, whereStreamT
is the type user has specified in field resolvers.#[juniper::graphql_subscription]
macro implementsGraphQLType
+GraphQLSubscriptionType
.GraphQLType
is used to store which fields this subscription has, which types they have, etc. Its resolvers (resolve
,resolve_field
,resolve_into_type
) are not used.GraphQLSubscriptionType
'sresolve_into_stream
(called by Executor),resolve_field_into_stream
,resolve_type_into_stream
(called byresolve_into_stream
) are used instead.Once a
Value<Stream>
is returned fromresolve_into_stream
, it is returned to the user (user most probably calledjuniper::http::resolve_into_stream
to get to resolvers and resolve a request into stream).Once a stream is returned, it could be put inside a
SubscriptionConnection
(if called bySubscriptionCoordinator
).SubscriptionConnection
implementsStream<Item = GraphQLResponse>
.GraphQLResponse
can be deserialized and sent to end user =>SubscriptionConnection
can be polled by server implementation.SubscriptionConnection
could also have functionality for stopping, reconnecting, etc (will be implemented in the follow-up PRs).SubscriptionCoordinator
andSubscriptionConnection
are types that manage and control already obtainedStream
s.GraphQLSubscriptionType
manages resolvers on GraphQL objects.Data flow in `warp_subscriptions` example
RootNode
andSubscriptionCoordinator
GraphQlRequest
graphql_subscriptions_async(ws_connection, coordinator)
- example's helper for subscription executioncoordinator.subscribe(request, context)
- resolveGraphQlRequest
intoStream
and returnSubscriptionConnection
containing that streamjuniper::http::resolve_into_stream(...)
- get operation name and variables from requestcrate::resolve_into_stream(...)
- parse document, make sure no errors are foundexecutor::resolve_validated_subscription(...)
- create executor and resolve subscription query intoValue<Stream<Item = Value<S>>>
executor.resolve_into_stream(...)
- check ifself.subscribe
has errors and push them to executorself.subscribe(...)
- returnvalue.resolve_into_stream(...)
.value.resolve_into_stream(...)
- user logic how to resolve this value into streamV
returned from stream is treated as a if a query was executed asynchronously andV
was returned. Ongoing execution logic creates newValue::Object
with fields requested by user(for example, if
user {id name}
was requested, eachUser { id name email }
returned from stream will be returned asUser { id name }
GraphQL object)SubscriptionConnection
can be used as aStream
overGraphQLResponse
s, which can be deserialized and sent to the end user.juniper_subscriptions::Connection
waits until all object's fields are resolved and then returns the whole object.GraphQlResponse
is yielded from a stream, send it to end userStarting playground server
examples/warp_subscriptions
andcargo run
. New warp server should start on localhost:8080Note: playground doesn't support anonymous subscriptions referencing fragments (graphql/graphql-playground/issues/1067), so the following request can be sent to `localhost:8000/subscriptions` to test it