graphql-streaming 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d5ec7f6cbeb19b73697a7a70311260bd8a741316
4
- data.tar.gz: 4fa0ca9c16f693ed294860248a55559ef8060471
3
+ metadata.gz: 2997980ed238638983e004e2291f16a5c5b4539c
4
+ data.tar.gz: 6f8e53cc1153a79f6560fc60a266b12f04e6d52e
5
5
  SHA512:
6
- metadata.gz: 1b1ece8cadcbaf967a114bf782f96845cbc5600686278b3a443844a850031b8d935a251ce9cec70e4e16e38b78109a374029b81405aa6798ca69f351d2d68ede
7
- data.tar.gz: 9a0e5d527a300ae45377bfdf12e6ea3538574f6c2b5b4fd7d2134f08e28b26e20075d8b6e9dc787207d7ce30f2e5981d8e87ba46b0b7b4891650a54f67b1a2b7
6
+ metadata.gz: 4f020c2238d33f5fb86ef144b14630baad9ea458535eb40465a0b7839c08b5fa68962e4097fc0e70efef18f1e79d1effab0e31ef58852092d133f411acec6b30
7
+ data.tar.gz: 98bc9d602d0cd84fa266f1e844e8840abdff20ed133c58c8a2a71c2af16fdacf215beb01300f4a18cbf4e5333d45cf4a4469f9e9e589fce9ce8d3d08dac62a47
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Graphql::Streaming
1
+ # GraphQL::Streaming [![Build Status](https://travis-ci.org/rmosolgo/graphql-streaming.svg?branch=master)](https://travis-ci.org/rmosolgo/graphql-streaming) [![Gem Version](https://badge.fury.io/rb/graphql-streaming.svg)](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 add an `ActionCableCollector` as `context[:collector]`, using `data["query_id"]` from the client. For example:
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
- # Run the query
104
- Schema.execute(query_string, variables: variables, context: context)
105
-
106
- # Tell the client to stop listening for patches
107
- context[:collector].close
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
- # Get the query ID from the client
161
- query_id = data["query_id"]
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
- # If there are no outstanding subscriptions,
175
- # tell the client to stop listening for patches
176
- if !context[:subscriber].subscribed?
177
- context[:collector].close
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
- - Public alternative to `@channel.send(:transmit, payload)`?
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
@@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :default => :spec
10
+ task default: :test
@@ -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 = Graphql::Streaming::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(query_id, broadcaster)
41
+ def initialize(channel, query_id)
42
42
  @query_id = query_id
43
- @broadcaster = broadcaster
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
- @broadcaster.broadcast({
51
- query_id: @query_id,
52
- patch: {
53
- path: path,
54
- value: value,
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
- @broadcaster.broadcast({
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("#{CHANNEL_PREFIX}#{subscription_handle}") do |trigger_json|
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
- payload = {
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
 
@@ -1,5 +1,5 @@
1
- module Graphql
1
+ module GraphQL
2
2
  module Streaming
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -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.1.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-10 00:00:00.000000000 Z
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