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 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