juniper/master/advanced/subscriptions.html

345 lines
19 KiB
HTML
Raw Normal View History

2020-04-17 01:25:26 -05:00
<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Subscriptions - Juniper - GraphQL Server for Rust</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="Documentation for juniper, a GraphQL server library for Rust.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body class="light">
<!-- Provide site root to javascript -->
<script type="text/javascript">var path_to_root = "../";</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
document.body.className = theme;
document.querySelector('html').className = theme + ' js';
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<ol class="chapter"><li class="affix"><a href="../index.html">Introduction</a></li><li class="affix"><a href="../quickstart.html">Quickstart</a></li><li class="affix"><a href="../types/index.html">Type System</a></li><li><a href="../types/objects/defining_objects.html"><strong aria-hidden="true">1.</strong> Defining objects</a></li><li><ol class="section"><li><a href="../types/objects/complex_fields.html"><strong aria-hidden="true">1.1.</strong> Complex fields</a></li><li><a href="../types/objects/using_contexts.html"><strong aria-hidden="true">1.2.</strong> Using contexts</a></li><li><a href="../types/objects/error_handling.html"><strong aria-hidden="true">1.3.</strong> Error handling</a></li></ol></li><li><a href="../types/other-index.html"><strong aria-hidden="true">2.</strong> Other types</a></li><li><ol class="section"><li><a href="../types/enums.html"><strong aria-hidden="true">2.1.</strong> Enums</a></li><li><a href="../types/interfaces.html"><strong aria-hidden="true">2.2.</strong> Interfaces</a></li><li><a href="../types/input_objects.html"><strong aria-hidden="true">2.3.</strong> Input objects</a></li><li><a href="../types/scalars.html"><strong aria-hidden="true">2.4.</strong> Scalars</a></li><li><a href="../types/unions.html"><strong aria-hidden="true">2.5.</strong> Unions</a></li></ol></li><li><a href="../schema/schemas_and_mutations.html"><strong aria-hidden="true">3.</strong> Schemas and mutations</a></li><li><a href="../servers/index.html"><strong aria-hidden="true">4.</strong> Adding A Server</a></li><li><ol class="section"><li><a href="../servers/official.html"><strong aria-hidden="true">4.1.</strong> Official Server Integrations</a></li><li><ol class="section"><li><a href="../servers/warp.html"><strong aria-hidden="true">4.1.1.</strong> Warp</a></li><li><a href="../servers/rocket.html"><strong aria-hidden="true">4.1.2.</strong> Rocket</a></li><li><a href="../servers/iron.html"><strong aria-hidden="true">4.1.3.</strong> Iron</a></li><li><a href="../servers/hyper.html"><strong aria-hidden="true">4.1.4.</strong> Hyper</a></li></ol></li><li><a href="../servers/third-party.html"><strong aria-hidden="true">4.2.</strong> Third Party Integrations</a></li></ol></li><li><a href="../advanced/index.html"><strong aria-hidden="true">5.</strong> Advanced Topics</a></li><li><ol class="section"><li><a href="../advanced/introspection.html"><strong aria-hidden="true">5.1.</strong> Introspection</a></li><li><a href="../advanced/non_struct_objects.html"><strong aria-hidden="true">5.2.</strong> Non-struct objects</a></li><li><a href="../advanced/objects_and_generics.html"><strong aria-hidden="true">5.3.</strong> Objects and generics</a></li><li><a href="../advanced/multiple_ops_per_request.html"><strong aria-hidden="true">5.4.</strong> Multiple operations per request</a></li><li><a href="../advanced/dataloaders.html"><strong aria-hidden="true">5.5.</strong> Dataloaders</a></li><li><a href="../advanced/subscriptions.html" class="active"><strong aria-hidden="true">5.6.</strong> Subscriptions</a></li></ol></li></ol>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div id="menu-bar-sticky-container">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Juniper - GraphQL Server for Rust</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<a class="header" href="#subscriptions" id="subscriptions"><h1>Subscriptions</h1></a>
<a class="header" href="#how-to-achieve-realtime-data-with-graphql-subscriptions" id="how-to-achieve-realtime-data-with-graphql-subscriptions"><h3>How to achieve realtime data with GraphQL subscriptions</h3></a>
<p>GraphQL subscriptions are a way to push data from the server to clients requesting real-time messages
from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client,
but instead of immediately returning a single answer, a result is sent every time a particular event happens on the
server.</p>
<p>In order to execute subscriptions you need a coordinator (that spawns connections)
and a GraphQL object that can be resolved into a stream--elements of which will then
be returned to the end user. The <a href="https://github.com/graphql-rust/juniper/tree/master/juniper_subscriptions">juniper_subscriptions</a> crate
provides a default connection implementation. Currently subscriptions are only supported on the <code>master</code> branch. Add the following to your <code>Cargo.toml</code>:</p>
<pre><code class="language-toml">[dependencies]
juniper = { git = &quot;https://github.com/graphql-rust/juniper&quot;, branch = &quot;master&quot; }
juniper_subscriptions = { git = &quot;https://github.com/graphql-rust/juniper&quot;, branch = &quot;master&quot; }
</code></pre>
<a class="header" href="#schema-definition" id="schema-definition"><h3>Schema Definition</h3></a>
<p>The Subscription is just a GraphQL object, similar to the Query root and Mutations object that you defined for the
operations in your [Schema][Schema], the difference is that all the operations defined there should be async and the return of it
should be a <a href="https://docs.rs/futures/0.3.4/futures/stream/trait.Stream.html">Stream</a>.</p>
<p>This example shows a subscription operation that returns two events, the strings <code>Hello</code> and <code>World!</code>
sequentially:</p>
<pre><pre class="playpen"><code class="language-rust"># use juniper::http::GraphQLRequest;
# use juniper::{DefaultScalarValue, FieldError, SubscriptionCoordinator};
# use juniper_subscriptions::Coordinator;
# use futures::{Stream, StreamExt};
# use std::pin::Pin;
# #[derive(Clone)]
# pub struct Database;
# impl juniper::Context for Database {}
# impl Database {
# fn new() -&gt; Self {
# Self {}
# }
# }
# pub struct Query;
# #[juniper::graphql_object(Context = Database)]
# impl Query {
# fn hello_world() -&gt; &amp;str {
# &quot;Hello World!&quot;
# }
# }
pub struct Subscription;
type StringStream = Pin&lt;Box&lt;dyn Stream&lt;Item = Result&lt;String, FieldError&gt;&gt; + Send&gt;&gt;;
#[juniper::graphql_subscription(Context = Database)]
impl Subscription {
async fn hello_world() -&gt; StringStream {
let stream = tokio::stream::iter(vec![
Ok(String::from(&quot;Hello&quot;)),
Ok(String::from(&quot;World!&quot;))
]);
Box::pin(stream)
}
}
# fn main () {}
</code></pre></pre>
<a class="header" href="#coordinator" id="coordinator"><h3>Coordinator</h3></a>
<p>Subscriptions require a bit more resources than regular queries, since they can provide a great vector
for DOS attacks and can bring down a server easily if not handled right. [SubscriptionCoordinator][SubscriptionCoordinator] trait provides the coordination logic.
It contains the schema and can keep track of opened connections, handle subscription
start and maintains a global subscription id. Once connection is established, subscription
coordinator spawns a [SubscriptionConnection][SubscriptionConnection], which handles a
single connection, provides resolver logic for a client stream and can provide re-connection
and shutdown logic.</p>
<p>The [Coordinator][Coordinator] struct is a simple implementation of the trait [SubscriptionCoordinator][SubscriptionCoordinator]
that is responsible for handling the execution of subscription operation into your schema. The execution of the <code>subscribe</code>
operation returns a [Future][Future] with a Item value of a Result&lt;[Connection][Connection], [GraphQLError][GraphQLError]&gt;,
where the connection is the Stream of values returned by the operation and the GraphQLError is the error that occurred in the
resolution of this connection, which means that the subscription failed.</p>
<pre><pre class="playpen"><code class="language-rust"># use juniper::http::GraphQLRequest;
# use juniper::{DefaultScalarValue, EmptyMutation, FieldError, RootNode, SubscriptionCoordinator};
# use juniper_subscriptions::Coordinator;
# use futures::{Stream, StreamExt};
# use std::pin::Pin;
# use tokio::runtime::Runtime;
# use tokio::task;
#
# #[derive(Clone)]
# pub struct Database;
#
# impl juniper::Context for Database {}
#
# impl Database {
# fn new() -&gt; Self {
# Self {}
# }
# }
#
# pub struct Query;
#
# #[juniper::graphql_object(Context = Database)]
# impl Query {
# fn hello_world() -&gt; &amp;str {
# &quot;Hello World!&quot;
# }
# }
#
# pub struct Subscription;
#
# type StringStream = Pin&lt;Box&lt;dyn Stream&lt;Item = Result&lt;String, FieldError&gt;&gt; + Send&gt;&gt;;
#
# #[juniper::graphql_subscription(Context = Database)]
# impl Subscription {
# async fn hello_world() -&gt; StringStream {
# let stream =
# tokio::stream::iter(vec![Ok(String::from(&quot;Hello&quot;)), Ok(String::from(&quot;World!&quot;))]);
# Box::pin(stream)
# }
# }
type Schema = RootNode&lt;'static, Query, EmptyMutation&lt;Database&gt;, Subscription&gt;;
fn schema() -&gt; Schema {
Schema::new(Query {}, EmptyMutation::new(), Subscription {})
}
async fn run_subscription() {
let schema = schema();
let coordinator = Coordinator::new(schema);
let req: GraphQLRequest&lt;DefaultScalarValue&gt; = serde_json::from_str(
r#&quot;
{
&quot;query&quot;: &quot;subscription { helloWorld }&quot;
}
&quot;#,
)
.unwrap();
let ctx = Database::new();
let mut conn = coordinator.subscribe(&amp;req, &amp;ctx).await.unwrap();
while let Some(result) = conn.next().await {
println!(&quot;{}&quot;, serde_json::to_string(&amp;result).unwrap());
}
}
# fn main() { }
</code></pre></pre>
<a class="header" href="#web-integration-and-examples" id="web-integration-and-examples"><h3>Web Integration and Examples</h3></a>
<p>Currently there is an example of subscriptions with [warp][warp], but it still in an alpha state.
GraphQL over [WS][WS] is not fully supported yet and is non-standard.</p>
<ul>
<li><a href="https://github.com/graphql-rust/juniper/tree/master/examples/warp_subscriptions">Warp Subscription Example</a></li>
<li><a href="https://github.com/graphql-rust/juniper/tree/master/examples/basic_subscriptions">Small Example</a></li>
</ul>
<!-- TODO: Fix these links when the documentation for the `juniper_subscriptions` are defined in the docs. --->
[Coordinator]: https://docs.rs/juniper_subscriptions/0.15.0/struct.Coordinator.html
[SubscriptionCoordinator]: https://docs.rs/juniper_subscriptions/0.15.0/trait.SubscriptionCoordinator.html
[Connection]: https://docs.rs/juniper_subscriptions/0.15.0/struct.Connection.html
[SubscriptionConnection]: https://docs.rs/juniper_subscriptions/0.15.0/trait.SubscriptionConnection.html
<!--- --->
[Future]: https://docs.rs/futures/0.3.4/futures/future/trait.Future.html
[warp]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp
[WS]: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
[GraphQLError]: https://docs.rs/juniper/0.14.2/juniper/enum.GraphQLError.html
[Schema]: ../schema/schemas_and_mutations.md
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../advanced/dataloaders.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a href="../advanced/dataloaders.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
</nav>
</div>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>