graphql-streaming 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -32
- data/Rakefile +1 -1
- data/graphql-streaming.gemspec +1 -1
- data/lib/graphql/streaming/action_cable_channel.rb +71 -0
- data/lib/graphql/streaming/action_cable_collector.rb +14 -10
- data/lib/graphql/streaming/action_cable_subscriber.rb +31 -11
- data/lib/graphql/streaming/clients/graphql-streaming/graphql_channel.js +16 -0
- data/lib/graphql/streaming/version.rb +2 -2
- data/lib/graphql/streaming.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2997980ed238638983e004e2291f16a5c5b4539c
|
4
|
+
data.tar.gz: 6f8e53cc1153a79f6560fc60a266b12f04e6d52e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f020c2238d33f5fb86ef144b14630baad9ea458535eb40465a0b7839c08b5fa68962e4097fc0e70efef18f1e79d1effab0e31ef58852092d133f411acec6b30
|
7
|
+
data.tar.gz: 98bc9d602d0cd84fa266f1e844e8840abdff20ed133c58c8a2a71c2af16fdacf215beb01300f4a18cbf4e5333d45cf4a4469f9e9e589fce9ce8d3d08dac62a47
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# GraphQL::Streaming [](https://travis-ci.org/rmosolgo/graphql-streaming) [](https://badge.fury.io/rb/graphql-streaming)
|
2
2
|
|
3
3
|
Experimental tools for GraphQL over a long-lived connection, including `subscription`, `@defer` and `@stream`. ([demo](https://github.com/rmosolgo/graphql-ruby-stream-defer-demo))
|
4
4
|
|
@@ -82,29 +82,31 @@ The `onResponse` function will be called with GraphQL response after each patch
|
|
82
82
|
|
83
83
|
You can use [Rails 5's `ActionCable`](http://edgeguides.rubyonrails.org/action_cable_overview.html) to send and receive GraphQL.
|
84
84
|
|
85
|
-
In your channel, implement an action called `#fetch(data)` for executing GraphQL queries. It should
|
85
|
+
In your channel, implement an action called `#fetch(data)` for executing GraphQL queries. It should use `stream_graphql_query` using `data["query_id"]` from the client. For example:
|
86
86
|
|
87
87
|
```ruby
|
88
88
|
class GraphqlChannel < ApplicationCable::Channel
|
89
|
-
|
89
|
+
include GraphQL::Streaming::ActionCableChannel
|
90
90
|
|
91
91
|
def fetch(data)
|
92
92
|
query_string = data["query"]
|
93
93
|
variables = ensure_hash(data["variables"] || {})
|
94
|
-
context = {
|
94
|
+
context = {
|
95
|
+
# ...
|
96
|
+
}
|
95
97
|
|
96
98
|
# Get the query ID, which is added by the GraphQLChannel client
|
97
99
|
query_id = data["query_id"]
|
98
|
-
# Get a broadcaster, which is the target for patches
|
99
|
-
broadcaster = ActionCable.server.broadcaster_for(channel_name)
|
100
|
-
# The collector passes patches on to the broadcaster
|
101
|
-
context[:collector] = GraphQL::Streaming::ActionCableCollector.new(query_id, broadcaster)
|
102
100
|
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
101
|
+
# Make the query within a `stream_graphql_query` block
|
102
|
+
stream_graphql_query(query_id: query_id) do |stream_ctx|
|
103
|
+
# the block provides a subscriber and collector,
|
104
|
+
# merge them into your context:
|
105
|
+
merged_ctx = context.merge(stream_ctx)
|
106
|
+
# don't forget to prevent stale data
|
107
|
+
merged_ctx[:current_user].reload
|
108
|
+
GraphQL.execute(query_string, variables: variables, context: merged_ctx)
|
109
|
+
end
|
108
110
|
end
|
109
111
|
end
|
110
112
|
```
|
@@ -157,24 +159,17 @@ The `onResponse` handler will be called with the whole response _each time_ a pa
|
|
157
159
|
The subscriber rides along with your query (as `context[:subscriber]`). It listens for triggers from the application, and when they happen, it re-evaluates the query and pushes an update over its channel.
|
158
160
|
|
159
161
|
```ruby
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
context = { current_user: user_from_session(session) }
|
164
|
-
|
165
|
-
context[:subscriber] = GraphQL::Streaming::ActionCableSubscriber.new(self, query_id) do
|
166
|
-
# This block is called when a matching trigger occurs
|
167
|
-
context[:current_user].reload
|
168
|
-
Schema.execute(query_string, variables: variables, context: context)
|
169
|
-
end
|
170
|
-
|
171
|
-
Schema.execute(query_string, variables: variables, context: context)
|
172
|
-
|
162
|
+
class GraphqlChannel < ApplicationCable::Channel
|
163
|
+
include GraphQL::Streaming::ActionCableChannel
|
173
164
|
|
174
|
-
|
175
|
-
#
|
176
|
-
|
177
|
-
|
165
|
+
def fetch(data)
|
166
|
+
# ...
|
167
|
+
query_id = data["query_id"]
|
168
|
+
stream_graphql_query(query_id: query_id) do |stream_ctx|
|
169
|
+
stream_ctx[:subscriber] # => #<ActionCableSubscriber ... >
|
170
|
+
# ...
|
171
|
+
end
|
172
|
+
end
|
178
173
|
end
|
179
174
|
```
|
180
175
|
|
@@ -227,6 +222,19 @@ would be updated by
|
|
227
222
|
GraphQL::Streaming::ActionCableSubscriber.trigger(:post, {id: 1})
|
228
223
|
```
|
229
224
|
|
225
|
+
#### Unsubscribing
|
226
|
+
|
227
|
+
A client can unsubscribe from future patches with `.clear`. For example:
|
228
|
+
|
229
|
+
```js
|
230
|
+
// Subscribe to data from the server
|
231
|
+
var queryHandle = App.graphqlChannel.fetch(queryString, queryVariables, onResponse)
|
232
|
+
// Unsubscribe from server pushes
|
233
|
+
App.graphqlChannel.clear(queryHandle)
|
234
|
+
```
|
235
|
+
|
236
|
+
No further patches will be sent to the client.
|
237
|
+
|
230
238
|
## Development
|
231
239
|
|
232
240
|
- `bundle exec rake test` to run the tests
|
@@ -236,10 +244,11 @@ GraphQL::Streaming::ActionCableSubscriber.trigger(:post, {id: 1})
|
|
236
244
|
|
237
245
|
- What happens to subscriptions when you redeploy or ActionCable loses its connection? Need to handle reconnecting in some way.
|
238
246
|
- Handle errors in subscriber block
|
239
|
-
- Improve middleware so you don't have to manually close `ActionCableCollector`s
|
240
247
|
- Tests for JS?
|
241
248
|
- Other platforms (Pusher, HTTP/2)?
|
242
|
-
-
|
249
|
+
- Request features from ActionCable
|
250
|
+
- Public alternative to `@channel.send(:transmit, payload)`?
|
251
|
+
- Some way to stop certain streams (see monkey patches in action_cable_channel.rb)
|
243
252
|
|
244
253
|
## License
|
245
254
|
|
data/Rakefile
CHANGED
data/graphql-streaming.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'graphql/streaming/version'
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "graphql-streaming"
|
8
|
-
spec.version =
|
8
|
+
spec.version = GraphQL::Streaming::VERSION
|
9
9
|
spec.authors = ["Robert Mosolgo"]
|
10
10
|
spec.email = ["rdmosolgo@gmail.com"]
|
11
11
|
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Streaming
|
3
|
+
# TODO: This contains MONKEYPATCHES to support stopping certain streams
|
4
|
+
module ActionCableChannel
|
5
|
+
# MONKEY PATCH
|
6
|
+
# Return the newly-created stream, so you can stop it later
|
7
|
+
def stream_from(*args, &block)
|
8
|
+
super
|
9
|
+
streams.last
|
10
|
+
end
|
11
|
+
|
12
|
+
# Stop streams which were captured from stream_from
|
13
|
+
def stop_specific_streams(streams_to_stop)
|
14
|
+
@_streams -= streams_to_stop
|
15
|
+
streams_to_stop.each do |broadcasting, callback|
|
16
|
+
pubsub.unsubscribe broadcasting, callback
|
17
|
+
logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
|
18
|
+
end
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Work around the fact that `transmit` is private
|
23
|
+
def send_graphql_payload(payload)
|
24
|
+
transmit(payload)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Setup a subscriber and collector and yield them to the block
|
30
|
+
def stream_graphql_query(query_id:, &query_exec)
|
31
|
+
# This object emits patches
|
32
|
+
collector = GraphQL::Streaming::ActionCableCollector.new(self, query_id)
|
33
|
+
|
34
|
+
# This re-evals the query in response to triggers
|
35
|
+
subscriber = GraphQL::Streaming::ActionCableSubscriber.new(self, query_id) do
|
36
|
+
# No subscriber so we don't re-subscribe
|
37
|
+
reeval_stream_ctx = { collector: collector }
|
38
|
+
query_exec.call(reeval_stream_ctx)
|
39
|
+
end
|
40
|
+
|
41
|
+
graphql_queries[query_id] << collector
|
42
|
+
graphql_queries[query_id] << subscriber
|
43
|
+
|
44
|
+
stream_ctx = {
|
45
|
+
collector: collector,
|
46
|
+
subscriber: subscriber,
|
47
|
+
}
|
48
|
+
# make the first GraphQL call
|
49
|
+
query_exec.call(stream_ctx)
|
50
|
+
|
51
|
+
if !subscriber.subscribed?
|
52
|
+
# If there are no ongoing subscriptions,
|
53
|
+
# tell the client to stop listening for patches
|
54
|
+
clear_graphql_query(query_id)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Remove any subscriptions or collectors for this query
|
59
|
+
def clear_graphql_query(query_id)
|
60
|
+
graphql_queries[query_id].map(&:close)
|
61
|
+
graphql_queries[query_id].clear
|
62
|
+
end
|
63
|
+
|
64
|
+
# A registry of queries for this channel,
|
65
|
+
# keys are query_ids, values are subscribers or collectors
|
66
|
+
def graphql_queries
|
67
|
+
@graphql_queries ||= Hash.new { |h, k| h[k] = [] }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -38,31 +38,35 @@ module GraphQL
|
|
38
38
|
class ActionCableCollector
|
39
39
|
# @param [String] A unique identifier for this query (probably provided by the client)
|
40
40
|
# @param [ActionCable::Server::Broadcasting::Broadcaster] The broadcast target for GraphQL's patches
|
41
|
-
def initialize(
|
41
|
+
def initialize(channel, query_id)
|
42
42
|
@query_id = query_id
|
43
|
-
@
|
43
|
+
@channel = channel
|
44
|
+
@closed = false
|
44
45
|
end
|
45
46
|
|
46
47
|
# Implements the "collector" API for DeferredExecution.
|
47
48
|
# Sends `{patch: {...}, query_id: @query_id}` over `@broadcaster`.
|
48
49
|
# @return [void]
|
49
50
|
def patch(path:, value:)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
51
|
+
if !@closed
|
52
|
+
@channel.send_graphql_payload({
|
53
|
+
query_id: @query_id,
|
54
|
+
patch: {
|
55
|
+
path: path,
|
56
|
+
value: value,
|
57
|
+
},
|
58
|
+
})
|
59
|
+
end
|
57
60
|
end
|
58
61
|
|
59
62
|
# Broadcast a message to terminate listeners on this query
|
60
63
|
# @return [void]
|
61
64
|
def close
|
62
|
-
@
|
65
|
+
@channel.send_graphql_payload({
|
63
66
|
query_id: @query_id,
|
64
67
|
close: true,
|
65
68
|
})
|
69
|
+
@closed = true
|
66
70
|
end
|
67
71
|
end
|
68
72
|
end
|
@@ -29,9 +29,9 @@ module GraphQL
|
|
29
29
|
@query_id = query_id
|
30
30
|
@query_exec = query_exec
|
31
31
|
@subscribed = false
|
32
|
+
@own_streams = []
|
32
33
|
end
|
33
34
|
|
34
|
-
|
35
35
|
# Trigger an event with arguments
|
36
36
|
#
|
37
37
|
# @example Trigger post_changed
|
@@ -45,22 +45,19 @@ module GraphQL
|
|
45
45
|
ActionCable.server.broadcast("#{CHANNEL_PREFIX}#{subscription_handle}", trigger_options)
|
46
46
|
end
|
47
47
|
|
48
|
+
# Subscribe to event named `subscription_handle`, but only
|
49
|
+
# when called with arguments `arguments`
|
50
|
+
# @param [String] the event name to subscribe to
|
51
|
+
# @param [Hash] the arguments to subscribe to
|
48
52
|
def register(subscription_handle, arguments)
|
49
53
|
@subscribed = true
|
54
|
+
handle = "#{CHANNEL_PREFIX}#{subscription_handle}"
|
50
55
|
original_args = stringify_hash(arguments)
|
51
56
|
|
52
|
-
@channel.stream_from(
|
57
|
+
@own_streams << @channel.stream_from(handle) do |trigger_json|
|
53
58
|
trigger_args = JSON.parse(trigger_json)
|
54
59
|
if original_args == trigger_args
|
55
|
-
|
56
|
-
patch: {
|
57
|
-
path: [],
|
58
|
-
value: @query_exec.call,
|
59
|
-
},
|
60
|
-
query_id: @query_id,
|
61
|
-
}
|
62
|
-
# TODO: request public method for this?
|
63
|
-
@channel.send(:transmit, payload)
|
60
|
+
reevaluate_query
|
64
61
|
end
|
65
62
|
end
|
66
63
|
end
|
@@ -70,8 +67,31 @@ module GraphQL
|
|
70
67
|
@subscribed
|
71
68
|
end
|
72
69
|
|
70
|
+
# Tell this subscriber to stop sending patches
|
71
|
+
# @return [void]
|
72
|
+
def close
|
73
|
+
@channel.stop_specific_streams(@own_streams)
|
74
|
+
@own_streams.clear
|
75
|
+
@subscribed = false
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
73
79
|
private
|
74
80
|
|
81
|
+
# Re-evaluate the given block
|
82
|
+
# and send the result as a patch to the
|
83
|
+
# root of the query result
|
84
|
+
def reevaluate_query
|
85
|
+
payload = {
|
86
|
+
patch: {
|
87
|
+
path: [],
|
88
|
+
value: @query_exec.call,
|
89
|
+
},
|
90
|
+
query_id: @query_id,
|
91
|
+
}
|
92
|
+
@channel.send_graphql_payload(payload)
|
93
|
+
end
|
94
|
+
|
75
95
|
def stringify_hash(value)
|
76
96
|
case value
|
77
97
|
when Hash
|
@@ -30,6 +30,7 @@ var GraphQLChannel = {
|
|
30
30
|
}
|
31
31
|
},
|
32
32
|
|
33
|
+
// @return [Any] A handle for cancelling this query
|
33
34
|
fetch: function(queryString, variables, onResponse) {
|
34
35
|
var queryId = GraphQLChannel._getQueryId()
|
35
36
|
GraphQLChannel.registry.set(queryId, onResponse)
|
@@ -40,6 +41,21 @@ var GraphQLChannel = {
|
|
40
41
|
variables: JSON.stringify(variables),
|
41
42
|
query_id: queryId,
|
42
43
|
})
|
44
|
+
return queryId
|
45
|
+
},
|
46
|
+
|
47
|
+
// Unsubscribe from any future patches
|
48
|
+
//
|
49
|
+
// @example Ignoring any subscription or deferred fields
|
50
|
+
// var queryHandle = App.GraphqlChannel.fetch(/* ... */)
|
51
|
+
// App.GraphqlChannel.clear(queryHandle)
|
52
|
+
//
|
53
|
+
// @param [Any] a handle returned by `fetch`
|
54
|
+
// @return void
|
55
|
+
clear: function(queryId) {
|
56
|
+
GraphQLChannel.log("[GraphQLChannel clearing]", queryId)
|
57
|
+
GraphQLChannel.registry.unset(queryId)
|
58
|
+
this.perform("clear", {query_id: queryId})
|
43
59
|
}
|
44
60
|
},
|
45
61
|
|
data/lib/graphql/streaming.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "graphql"
|
2
2
|
require "graphql/streaming/action_cable_collector"
|
3
|
+
require "graphql/streaming/action_cable_channel"
|
3
4
|
require "graphql/streaming/action_cable_subscriber"
|
4
5
|
require "graphql/streaming/assign_subscription_field"
|
5
6
|
require "graphql/streaming/stream_collector"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-streaming
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- bin/setup
|
84
84
|
- graphql-streaming.gemspec
|
85
85
|
- lib/graphql/streaming.rb
|
86
|
+
- lib/graphql/streaming/action_cable_channel.rb
|
86
87
|
- lib/graphql/streaming/action_cable_collector.rb
|
87
88
|
- lib/graphql/streaming/action_cable_subscriber.rb
|
88
89
|
- lib/graphql/streaming/assign_subscription_field.rb
|