graphql-anycable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c494ac593becba11ed50d2ffe1af1c16afa91e4dec8393b59a4d3e17fedaf926
4
+ data.tar.gz: 15682a1ca82073419efb27600383443f0db2bbf2288b3b494d77512eace7096e
5
+ SHA512:
6
+ metadata.gz: b630183f77e927586116388aa8c975bbb04242ca956a8dfcb10453afa7c6d1ba324a3eaca85cb6237d855dab4fd612ae269d380ecc0a5c3e786186c1836b0067
7
+ data.tar.gz: b35bd5536f6d6460aa97af5d69aa0fab63962111a53854f429796e70d8b5b9831a550f9d19698853e4bd7b551096d90a653bd616a3f6100a7b54bc592126adee
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ .rspec_status
11
+
12
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,46 @@
1
+ ---
2
+ require:
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.3
7
+
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - "Gemfile"
11
+ - "spec/**/*"
12
+
13
+ Style/BracesAroundHashParameters:
14
+ EnforcedStyle: context_dependent
15
+
16
+ Style/StringLiterals:
17
+ EnforcedStyle: double_quotes
18
+
19
+ # Allow to use let!
20
+ RSpec/LetSetup:
21
+ Enabled: false
22
+
23
+ RSpec/MultipleExpectations:
24
+ Enabled: false
25
+
26
+ Bundler/OrderedGems:
27
+ Enabled: false
28
+
29
+ Style/TrailingCommaInArguments:
30
+ Description: 'Checks for trailing comma in argument lists.'
31
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma'
32
+ Enabled: true
33
+ EnforcedStyleForMultiline: consistent_comma
34
+
35
+ Style/TrailingCommaInArrayLiteral:
36
+ Description: 'Checks for trailing comma in array literals.'
37
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
38
+ Enabled: true
39
+ EnforcedStyleForMultiline: consistent_comma
40
+
41
+ Style/TrailingCommaInHashLiteral:
42
+ Description: 'Checks for trailing comma in hash literals.'
43
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
44
+ Enabled: true
45
+ EnforcedStyleForMultiline: consistent_comma
46
+
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.1
7
+ before_install: gem install bundler -v 1.16.4
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in graphql-anycable.gemspec
8
+ gemspec
9
+
10
+ group :development, :test do
11
+ gem "pry"
12
+ gem "pry-byebug", platform: :mri
13
+
14
+ gem "rubocop"
15
+ gem "rubocop-rspec"
16
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Andrey Novikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # GraphQL subscriptions for AnyCable
2
+
3
+ A (mostly) drop-in replacement for default ActionCable subscriptions adapter shipped with [graphql gem] but works with [AnyCable]!
4
+
5
+ **IMPORTANT**: This gem is still in _proof of concept_ stage. It is not yet tested in production and everything may change without any notice. You have warned.
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/graphql-anycable.svg)](https://badge.fury.io/rb/graphql-anycable)
8
+
9
+ <a href="https://evilmartians.com/?utm_source=graphql-anycable&utm_campaign=project_page">
10
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
11
+ </a>
12
+
13
+ ## Why?
14
+
15
+ AnyCable is fast because it does not execute any Ruby code. But default subscription implementation shipped with [graphql gem] requires to do exactly that: re-evaluate GraphQL queries in ActionCable process. AnyCable doesn't support this (it's possible but hard to implement).
16
+
17
+ See https://github.com/anycable/anycable-rails/issues/40 for more details and discussion.
18
+
19
+ ## Differences
20
+
21
+ - Subscription information is stored in Redis database configured to be used by AnyCable. No expiration or data cleanup yet.
22
+ - GraphQL queries for all subscriptions are re-executed in the process that triggers event (it may be web server, async jobs, rake tasks or whatever)
23
+
24
+ ## Compatibility
25
+
26
+ - Should work with ActionCable in development
27
+ - Should work without Rails via [LiteCable]
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ ```ruby
34
+ gem 'graphql-anycable'
35
+ ```
36
+
37
+ And then execute:
38
+
39
+ $ bundle
40
+
41
+ Or install it yourself as:
42
+
43
+ $ gem install graphql-anycable
44
+
45
+ ## Usage
46
+
47
+ 1. Plug it into the schema (replace from ActionCable adapter if you have one):
48
+
49
+ ```ruby
50
+ class MySchema < GraphQL::Schema
51
+ use GraphQL::Subscriptions::AnyCableSubscriptions
52
+
53
+ subscription SubscriptionType
54
+ end
55
+ ```
56
+
57
+ 2. Execute query in ActionCable/LiteCable channel.
58
+
59
+ ```ruby
60
+ class GraphqlChannel < ApplicationCable::Channel
61
+ def execute(data)
62
+ result =
63
+ MySchema.execute(
64
+ query: data["query"],
65
+ context: context,
66
+ variables: Hash(data["variables"]),
67
+ operation_name: data["operationName"],
68
+ )
69
+
70
+ transmit(
71
+ result: result.subscription? ? { data: nil } : result.to_h,
72
+ more: result.subscription?,
73
+ )
74
+ end
75
+
76
+ def unsubscribed
77
+ channel_id = params.fetch("channelId")
78
+ MySchema.subscriptions.delete_channel_subscriptions(channel_id)
79
+ end
80
+
81
+ private
82
+
83
+ def context
84
+ {
85
+ account_id: account&.id,
86
+ channel: self,
87
+ }
88
+ end
89
+ end
90
+ ```
91
+
92
+ Make sure that you're passing channel instance as `channel` key to the context.
93
+
94
+ 3. Trigger events as usual:
95
+
96
+ ```ruby
97
+ MySchema.subscriptions.trigger(:product_updated, {}, Product.first!, scope: account.id)
98
+ ```
99
+
100
+ ## Development
101
+
102
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
103
+
104
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
105
+
106
+ ## Contributing
107
+
108
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Envek/graphql-anycable.
109
+
110
+ ## License
111
+
112
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
113
+
114
+ [graphql gem]: https://github.com/rmosolgo/graphql-ruby "Ruby implementation of GraphQL"
115
+ [AnyCable]: https://github.com/anycable/anycable "Polyglot replacement for Ruby WebSocket servers with Action Cable protocol"
116
+ [LiteCable]: https://github.com/palkan/litecable "Lightweight Action Cable implementation (Rails-free)"
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "graphql/anycable"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "graphql/anycable/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "graphql-anycable"
9
+ spec.version = Graphql::Anycable::VERSION
10
+ spec.authors = ["Andrey Novikov"]
11
+ spec.email = ["envek@envek.name"]
12
+
13
+ spec.summary = <<~SUMMARY
14
+ A drop-in replacement for GraphQL ActionCable subscriptions for AnyCable.
15
+ SUMMARY
16
+
17
+ spec.homepage = "https://github.com/Envek/graphql-anycable"
18
+ spec.license = "MIT"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "anycable", "~> 0.5"
30
+ spec.add_dependency "graphql", "~> 1.8"
31
+
32
+ spec.add_development_dependency "bundler", "~> 1.16"
33
+ spec.add_development_dependency "fakeredis"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graphql
4
+ module Anycable
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../graphql-anycable"
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable"
4
+ require "graphql/subscriptions"
5
+
6
+ # rubocop: disable Metrics/AbcSize, Metrics/LineLength, Metrics/MethodLength
7
+
8
+ # A subscriptions implementation that sends data as AnyCable broadcastings.
9
+ #
10
+ # Since AnyCable is aimed to be compatible with ActionCable, this adapter
11
+ # may be used as (practically) drop-in replacement to ActionCable adapter
12
+ # shipped with graphql-ruby.
13
+ #
14
+ # @example Adding AnyCableSubscriptions to your schema
15
+ # MySchema = GraphQL::Schema.define do
16
+ # use GraphQL::Subscriptions::AnyCableSubscriptions
17
+ # end
18
+ #
19
+ # @example Implementing a channel for GraphQL Subscriptions
20
+ # class GraphqlChannel < ApplicationCable::Channel
21
+ # def execute(data)
22
+ # query = data["query"]
23
+ # variables = ensure_hash(data["variables"])
24
+ # operation_name = data["operationName"]
25
+ # context = {
26
+ # current_user: current_user,
27
+ # # Make sure the channel is in the context
28
+ # channel: self,
29
+ # }
30
+ #
31
+ # result = MySchema.execute({
32
+ # query: query,
33
+ # context: context,
34
+ # variables: variables,
35
+ # operation_name: operation_name
36
+ # })
37
+ #
38
+ # payload = {
39
+ # result: result.subscription? ? {data: nil} : result.to_h,
40
+ # more: result.subscription?,
41
+ # }
42
+ #
43
+ # transmit(payload)
44
+ # end
45
+ #
46
+ # def unsubscribed
47
+ # channel_id = params.fetch("channelId")
48
+ # MySchema.subscriptions.delete_channel_subscriptions(channel_id)
49
+ # end
50
+ # end
51
+ #
52
+ module GraphQL
53
+ class Subscriptions
54
+ class AnyCableSubscriptions < GraphQL::Subscriptions
55
+ SUBSCRIPTION_PREFIX = "graphql-subscription:"
56
+ EVENT_PREFIX = "graphql-event:"
57
+ CHANNEL_PREFIX = "graphql-channel:"
58
+
59
+ # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
60
+ def initialize(serializer: Serialize, **rest)
61
+ @serializer = serializer
62
+ super
63
+ end
64
+
65
+ # An event was triggered.
66
+ # Re-evaluate all subscribed queries and push the data over ActionCable.
67
+ def execute_all(event, object)
68
+ redis.smembers(EVENT_PREFIX + event.topic).each do |subscription_id|
69
+ execute(subscription_id, event, object)
70
+ end
71
+ end
72
+
73
+ # This subscription was re-evaluated.
74
+ # Send it to the specific stream where this client was waiting.
75
+ def deliver(subscription_id, result)
76
+ payload = { result: result.to_h, more: true }
77
+ anycable.broadcast(SUBSCRIPTION_PREFIX + subscription_id, payload.to_json)
78
+ end
79
+
80
+ # Save query to "storage" (in redis)
81
+ def write_subscription(query, events)
82
+ context = query.context.to_h
83
+ subscription_id = context[:subscription_id] ||= build_id
84
+ channel = context.delete(:channel)
85
+ stream = context[:action_cable_stream] ||= SUBSCRIPTION_PREFIX + subscription_id
86
+ channel.stream_from(stream)
87
+
88
+ data = {
89
+ query_string: query.query_string,
90
+ variables: query.provided_variables.to_json,
91
+ context: @serializer.dump(context.to_h),
92
+ operation_name: query.operation_name,
93
+ events: events.map(&:topic).to_json,
94
+ }
95
+
96
+ redis.multi do
97
+ redis.sadd(CHANNEL_PREFIX + channel.params["channelId"], subscription_id)
98
+ redis.mapped_hmset(SUBSCRIPTION_PREFIX + subscription_id, data)
99
+ events.each do |event|
100
+ redis.sadd(EVENT_PREFIX + event.topic, subscription_id)
101
+ end
102
+ end
103
+ end
104
+
105
+ # Return the query from "storage" (in redis)
106
+ def read_subscription(subscription_id)
107
+ redis.mapped_hmget(
108
+ "#{SUBSCRIPTION_PREFIX}#{subscription_id}",
109
+ :query_string, :variables, :context, :operation_name,
110
+ ).tap do |subscription|
111
+ subscription[:context] = @serializer.load(subscription[:context])
112
+ subscription[:variables] = JSON.parse(subscription[:variables])
113
+ end
114
+ end
115
+
116
+ # The channel was closed, forget about it.
117
+ def delete_subscription(subscription_id)
118
+ # Remove subscription ids from all events
119
+ events_data = redis.hget(SUBSCRIPTION_PREFIX + subscription_id, :events)
120
+ events_data && JSON.parse(events_data).each do |event_topic|
121
+ redis.srem(EVENT_PREFIX + event_topic, subscription_id)
122
+ end
123
+ # Delete subscription itself
124
+ redis.del(SUBSCRIPTION_PREFIX + subscription_id)
125
+ end
126
+
127
+ def delete_channel_subscriptions(channel_id)
128
+ redis.smembers(CHANNEL_PREFIX + channel_id).each do |subscription_id|
129
+ delete_subscription(subscription_id)
130
+ end
131
+ redis.del(CHANNEL_PREFIX + channel_id)
132
+ end
133
+
134
+ private
135
+
136
+ def anycable
137
+ @pub_sub ||= Anycable::PubSub.new
138
+ end
139
+
140
+ def redis
141
+ @redis ||= anycable.redis_conn
142
+ end
143
+ end
144
+ end
145
+ end
146
+ # rubocop: enable Metrics/AbcSize, Metrics/LineLength, Metrics/MethodLength
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql"
4
+
5
+ require_relative "graphql/anycable/version"
6
+ require_relative "graphql/subscriptions/anycable_subscriptions"
7
+
8
+ module Graphql
9
+ module Anycable
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-anycable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrey Novikov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: anycable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: graphql
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fakeredis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ description:
98
+ email:
99
+ - envek@envek.name
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - graphql-anycable.gemspec
115
+ - lib/graphql-anycable.rb
116
+ - lib/graphql/anycable.rb
117
+ - lib/graphql/anycable/version.rb
118
+ - lib/graphql/subscriptions/anycable_subscriptions.rb
119
+ homepage: https://github.com/Envek/graphql-anycable
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 2.7.6
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: A drop-in replacement for GraphQL ActionCable subscriptions for AnyCable.
143
+ test_files: []