graphql-anycable 1.2.0 → 1.3.1

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
  SHA256:
3
- metadata.gz: bafa1a80487eeb78627ed0752751eb488dd33ca9709d38b205d61d310f97d7ae
4
- data.tar.gz: 821cba31fd376a471c9db5757a2da3e51d936f618f372a624fab1515dea5d4e6
3
+ metadata.gz: eebb61e4181214e0f86bdf0cc33e7153e0a02c6c8de5e37996ca9457e6231385
4
+ data.tar.gz: 6fa0f37c132f8c22454bcdf9d995ac9b0c3f40c3913b0047f7be3b91eb68d07f
5
5
  SHA512:
6
- metadata.gz: fdad5b3cfcc372ec326ebfa9641ecdc81d5f586aca7fc54edb5bc1985d4e0e8fabd3ba93eb25cf8e9f55556a7d5410a2971596ab52936706199cdb6c6f3faa10
7
- data.tar.gz: f71265d7dc1f0afdb4162e2d6a00d87c92bd53aa484429a5dff138e7f53db754b75a55137d999314f6c600eee3d88d661b6ab91a87b1bdd9f52a0317a7d9e43a
6
+ metadata.gz: 06b6b55507a369100a71e9a30b93cf0fccef1d82c948b71693e43a4629116749453577aa0919d5de12c3bad8f6703d9652b0378c6ad90c4775ab355fe9b4ce7d
7
+ data.tar.gz: d0ed4156e37e936fad396659517245b1f4540451e4b4012ec8d1478cc4c1b1d244c254b690b168f4d8d2d36a3aadf1c22643c5cde639b08c119c8ec801d97152
@@ -0,0 +1,34 @@
1
+ name: Lint Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ paths:
8
+ - "gemfiles/*"
9
+ - "Gemfile"
10
+ - "**/*.rb"
11
+ - "**/*.gemspec"
12
+ - ".github/workflows/lint.yml"
13
+ pull_request:
14
+ paths:
15
+ - "gemfiles/*"
16
+ - "Gemfile"
17
+ - "**/*.rb"
18
+ - "**/*.gemspec"
19
+ - ".github/workflows/lint.yml"
20
+
21
+ jobs:
22
+ rubocop:
23
+ runs-on: ubuntu-latest
24
+ env:
25
+ BUNDLE_GEMFILE: "gemfiles/rubocop.gemfile"
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: 3.1
31
+ bundler-cache: true
32
+ - name: Lint Ruby code with RuboCop
33
+ run: |
34
+ bundle exec rubocop
@@ -24,11 +24,15 @@ jobs:
24
24
  redis_version: latest
25
25
  - ruby: "3.2"
26
26
  graphql: '~> 2.2.0'
27
- anycable: '~> 1.4.0'
27
+ anycable: '~> 1.5.0'
28
28
  redis_version: '7.2'
29
29
  - ruby: "3.1"
30
+ graphql: '~> 2.1.0'
31
+ anycable: '~> 1.4.0'
32
+ redis_version: '6.2'
33
+ - ruby: "3.0"
30
34
  graphql: '~> 2.0.0'
31
- anycable: '~> 1.3.0'
35
+ anycable: '~> 1.4.0'
32
36
  redis_version: '6.2'
33
37
  env:
34
38
  CI: true
@@ -0,0 +1,46 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ # Disable all cops by default,
5
+ # only enable those defined explcitly in this configuration file
6
+ RSpec:
7
+ Enabled: false
8
+
9
+ RSpec/Focus:
10
+ Enabled: true
11
+
12
+ RSpec/EmptyExampleGroup:
13
+ Enabled: true
14
+
15
+ RSpec/EmptyLineAfterExampleGroup:
16
+ Enabled: true
17
+
18
+ RSpec/EmptyLineAfterFinalLet:
19
+ Enabled: true
20
+
21
+ RSpec/EmptyLineAfterHook:
22
+ Enabled: true
23
+
24
+ RSpec/EmptyLineAfterSubject:
25
+ Enabled: true
26
+
27
+ RSpec/HookArgument:
28
+ Enabled: true
29
+
30
+ RSpec/HooksBeforeExamples:
31
+ Enabled: true
32
+
33
+ RSpec/ImplicitExpect:
34
+ Enabled: true
35
+
36
+ RSpec/IteratedExpectation:
37
+ Enabled: true
38
+
39
+ RSpec/LetBeforeExamples:
40
+ Enabled: true
41
+
42
+ RSpec/MissingExampleGroupArgument:
43
+ Enabled: true
44
+
45
+ RSpec/ReceiveCounts:
46
+ Enabled: true
@@ -0,0 +1,7 @@
1
+ Lint/Debugger: # don't leave binding.pry
2
+ Enabled: true
3
+ Exclude: []
4
+
5
+ RSpec/Focus: # run ALL tests on CI
6
+ Enabled: true
7
+ Exclude: []
data/.rubocop.yml CHANGED
@@ -1,46 +1,30 @@
1
- ---
2
- require:
3
- - rubocop-rspec
4
-
5
- AllCops:
6
- TargetRubyVersion: 2.3
1
+ inherit_mode:
2
+ merge:
3
+ - Exclude
7
4
 
8
- Metrics/BlockLength:
9
- Exclude:
10
- - "Gemfile"
11
- - "spec/**/*"
12
-
13
- Style/BracesAroundHashParameters:
14
- EnforcedStyle: context_dependent
5
+ require:
6
+ - standard
7
+ - standard-custom
8
+ - standard-performance
9
+ - rubocop-performance
15
10
 
16
- Style/StringLiterals:
17
- EnforcedStyle: double_quotes
11
+ inherit_gem:
12
+ standard: config/base.yml
13
+ standard-performance: config/base.yml
14
+ standard-custom: config/base.yml
18
15
 
19
- # Allow to use let!
20
- RSpec/LetSetup:
21
- Enabled: false
16
+ inherit_from:
17
+ - .rubocop/rspec.yml
18
+ - .rubocop/strict.yml
22
19
 
23
- RSpec/MultipleExpectations:
24
- Enabled: false
20
+ AllCops:
21
+ NewCops: disable
22
+ SuggestExtensions: false
23
+ TargetRubyVersion: 3.2
25
24
 
26
- Bundler/OrderedGems:
25
+ Style/ArgumentsForwarding:
27
26
  Enabled: false
28
27
 
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
-
28
+ Style/GlobalVars:
29
+ Exclude:
30
+ - "spec/**/*.rb"
data/CHANGELOG.md CHANGED
@@ -7,8 +7,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 1.3.0 - 2024-08-13
11
+
12
+ ### Changed
13
+
14
+ - Redis subscriptions store configuration has been decoupled from AnyCable, so you can use any broadcasting adapter and configure Redis as you like. [@palkan] ([#44](https://github.com/anycable/graphql-anycable/pull/44))
15
+
10
16
  ## 1.2.0 - 2024-05-07
11
17
 
18
+ ### Added
19
+
20
+ - Stats collection about subscriptions, channels, etc via `GraphQL::AnyCable.stats`. [@prog-supdex] ([#37](https://github.com/anycable/graphql-anycable/pull/37))
21
+
22
+ See [Stats](https://github.com/anycable/graphql-anycable?tab=readme-ov-file#stats) section in README for details.
23
+
24
+ - Configuration option `redis_prefix` for namespacing Redis keys. [@prog-supdex] ([#36](https://github.com/anycable/graphql-anycable/pull/36))
25
+
12
26
  ### Changed
13
27
 
14
28
  - Depend on `anycable-core` gem instead of `anycable`.
@@ -197,6 +211,9 @@ Technical release to test publishing via GitHub Actions.
197
211
 
198
212
  Initial version: store subscriptions on redis, re-execute queries in sync. [@Envek]
199
213
 
214
+ [@prog-supdex]: https://github.com/prog-supdex "Igor Platonov"
215
+ [@ilyasgaraev]: https://github.com/ilyasgaraev "Ilyas Garaev"
216
+ [@smasry]: https://github.com/smasry "Samer Masry"
200
217
  [@gsamokovarov]: https://github.com/gsamokovarov "Genadi Samokovarov"
201
218
  [@bibendi]: https://github.com/bibendi "Misha Merkushin"
202
219
  [@FX-HAO]: https://github.com/FX-HAO "Fuxin Hao"
data/Gemfile CHANGED
@@ -7,14 +7,17 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in graphql-anycable.gemspec
8
8
  gemspec
9
9
 
10
- gem "graphql", ENV.fetch("GRAPHQL_RUBY_VERSION", "~> 2.3")
10
+ gem "graphql", ENV.fetch("GRAPHQL_RUBY_VERSION", "~> 2.3")
11
11
  gem "anycable", ENV.fetch("ANYCABLE_VERSION", "~> 1.5")
12
12
  gem "anycable-rails", ENV.fetch("ANYCABLE_RAILS_VERSION", "~> 1.5")
13
+ gem "rack", "< 3.0" if /1\.4/.match?(ENV.fetch("ANYCABLE_VERSION", "~> 1.5"))
14
+
15
+ gem "ostruct"
13
16
 
14
17
  group :development, :test do
15
- gem "pry"
16
- gem "pry-byebug", platform: :mri
18
+ gem "debug", platforms: [:mri] unless ENV["CI"]
19
+ end
17
20
 
18
- gem "rubocop"
19
- gem "rubocop-rspec"
21
+ group :development do
22
+ eval_gemfile "gemfiles/rubocop.gemfile"
20
23
  end
data/README.md CHANGED
@@ -11,58 +11,52 @@ A (mostly) drop-in replacement for default ActionCable subscriptions adapter shi
11
11
 
12
12
  ## Why?
13
13
 
14
- 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).
14
+ 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 Action Cable process. AnyCable doesn't support this (it's possible but hard to implement).
15
15
 
16
16
  See https://github.com/anycable/anycable-rails/issues/40 for more details and discussion.
17
17
 
18
18
  ## Differences
19
19
 
20
- - Subscription information is stored in Redis database configured to be used by AnyCable. Expiration or data cleanup should be configured separately (see below).
21
- - 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)
20
+ - Subscription information is stored in a Redis database. By default, we use AnyCable Redis configuration. Expiration or data cleanup should be configured separately (see below).
21
+ - 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)
22
22
 
23
23
  ## Compatibility
24
24
 
25
- - Should work with ActionCable in development
26
- - Should work without Rails via [LiteCable]
27
-
28
- ## Requirements
29
-
30
- AnyCable must be configured with redis broadcast adapter (this is default).
25
+ - Works with Action Cable (e.g., in development/test)
26
+ - Works without Rails (e.g., via [LiteCable][])
31
27
 
32
28
  ## Installation
33
29
 
34
30
  Add this line to your application's Gemfile:
35
31
 
36
32
  ```ruby
37
- gem 'graphql-anycable', '~> 1.0'
33
+ gem "graphql-anycable", "~> 1.0"
38
34
  ```
39
35
 
40
36
  And then execute:
41
37
 
42
- $ bundle
43
-
44
- Or install it yourself as:
45
-
46
- $ gem install graphql-anycable
38
+ ```sh
39
+ bundle install
40
+ ```
47
41
 
48
42
  ## Usage
49
43
 
50
- 1. Plug it into the schema (replace from ActionCable adapter if you have one):
51
-
44
+ 1. Plug it into the schema (replace the Action Cable adapter if you have one):
45
+
52
46
  ```ruby
53
47
  class MySchema < GraphQL::Schema
54
48
  use GraphQL::AnyCable, broadcast: true
55
-
49
+
56
50
  subscription SubscriptionType
57
51
  end
58
52
  ```
59
-
60
- 2. Execute query in ActionCable/LiteCable channel.
61
-
53
+
54
+ 2. Execute a query within an Action Cable/LiteCable channel.
55
+
62
56
  ```ruby
63
57
  class GraphqlChannel < ApplicationCable::Channel
64
58
  def execute(data)
65
- result =
59
+ result =
66
60
  MySchema.execute(
67
61
  query: data["query"],
68
62
  context: context,
@@ -75,11 +69,11 @@ Or install it yourself as:
75
69
  more: result.subscription?,
76
70
  )
77
71
  end
78
-
72
+
79
73
  def unsubscribed
80
74
  MySchema.subscriptions.delete_channel_subscriptions(self)
81
75
  end
82
-
76
+
83
77
  private
84
78
 
85
79
  def context
@@ -90,29 +84,38 @@ Or install it yourself as:
90
84
  end
91
85
  end
92
86
  ```
93
-
94
- Make sure that you're passing channel instance as `channel` key to the context.
95
-
87
+
88
+ Make sure that you're passing channel instance as `channel` key to the context.
89
+
96
90
  3. Trigger events as usual:
97
-
91
+
98
92
  ```ruby
99
93
  MySchema.subscriptions.trigger(:product_updated, {}, Product.first!, scope: account.id)
100
94
  ```
101
95
 
96
+ 4. (Optional) When using other AnyCable broadcasting adapters than Redis, you MUST configure Redis for graphql-anycable yourself:
97
+
98
+ ```ruby
99
+ GraphQL::AnyCable.redis = Redis.new(url: ENV["REDIS_URL"])
100
+
101
+ # you can also use a Proc (e.g., if you want to use a connection pool)
102
+ redis_pool = ConnectionPool.new(size: 10) { Redis.new(url: ENV["REDIS_URL"]) }
103
+
104
+ GraphQL::AnyCable.redis = ->(&block) { redis_pool.with { |conn| block.call(conn) } }
105
+ ```
106
+
102
107
  ## Broadcasting
103
108
 
104
109
  By default, graphql-anycable evaluates queries and transmits results for every subscription client individually. Of course, it is a waste of resources if you have hundreds or thousands clients subscribed to the same data (and has huge negative impact on performance).
105
110
 
106
111
  Thankfully, GraphQL-Ruby has added [Subscriptions Broadcast](https://graphql-ruby.org/subscriptions/broadcast.html) feature that allows to group exact same subscriptions, execute them and transmit results only once.
107
112
 
108
- To enable this feature, turn on [Interpreter](https://graphql-ruby.org/queries/interpreter.html) and pass `broadcast` option set to `true` to graphql-anycable.
113
+ To enable this feature, pass the `broadcast` option set to `true` to graphql-anycable.
109
114
 
110
115
  By default all fields are marked as _not safe for broadcasting_. If a subscription has at least one non-broadcastable field in its query, GraphQL-Ruby will execute every subscription for every client independently. If you sure that all your fields are safe to be broadcasted, you can pass `default_broadcastable` option set to `true` (but be aware that it can have security impllications!)
111
116
 
112
117
  ```ruby
113
118
  class MySchema < GraphQL::Schema
114
- use GraphQL::Execution::Interpreter # Required for graphql-ruby before 1.12. Remove it when upgrading to 2.0
115
- use GraphQL::Analysis::AST # Required for graphql-ruby before 1.12. Remove it when upgrading to 2.0
116
119
  use GraphQL::AnyCable, broadcast: true, default_broadcastable: true
117
120
 
118
121
  subscription SubscriptionType
@@ -125,7 +128,7 @@ See GraphQL-Ruby [broadcasting docs](https://graphql-ruby.org/subscriptions/broa
125
128
 
126
129
  To avoid filling Redis storage with stale subscription data:
127
130
 
128
- 1. Set `subscription_expiration_seconds` setting to number of seconds (e.g. `604800` for 1 week). See [configuration](#Configuration) section below for details.
131
+ 1. Set `subscription_expiration_seconds` setting to number of seconds (e.g. `604800` for 1 week). See [configuration](#configuration) section below for details.
129
132
 
130
133
  2. Execute `rake graphql:anycable:clean` once in a while to clean up stale subscription data.
131
134
 
@@ -165,39 +168,46 @@ GraphQL-AnyCable uses [anyway_config] to configure itself. There are several pos
165
168
  And any other way provided by [anyway_config]. Check its documentation!
166
169
 
167
170
  ## Emergency actions
168
- In situations when you don't set `subscription_expiration_seconds`, have a lot of inactive subscriptions, and `GraphQL::AnyCable::Cleaner` does`t help in that,
171
+
172
+ In situations when you don't set `subscription_expiration_seconds`, have a lot of inactive subscriptions, and `GraphQL::AnyCable::Cleaner` does`t help in that,
169
173
  you can do the following actions for clearing subscriptions
170
174
 
171
175
  1. Set `config.subscription_expiration_seconds`. After that, the new subscriptions will have `TTL`
176
+
172
177
  2. Run the script
178
+
173
179
  ```ruby
174
- redis = GraphQL::AnyCable.redis
175
- config = GraphQL::AnyCable.config
176
-
177
- # do it for subscriptions
178
- redis.scan_each("graphql-subscription:*") do |key|
179
- redis.expire(key, config.subscription_expiration_seconds) if redis.ttl(key) < 0
180
- # or you can just remove it immediately
181
- # redis.del(key) if redis.ttl(key) < 0
182
- end
183
-
184
- # do it for channels
185
- redis.scan_each("graphql-channel:*") do |key|
186
- redis.expire(key, config.subscription_expiration_seconds) if redis.ttl(key) < 0
187
- # or you can just remove it immediately
188
- # redis.del(key) if redis.ttl(key) < 0
189
- end
180
+ config = GraphQL::AnyCable.config
181
+
182
+ GraphQL::AnyCable.with_redis do |redis|
183
+ # do it for subscriptions
184
+ redis.scan_each("graphql-subscription:*") do |key|
185
+ redis.expire(key, config.subscription_expiration_seconds) if redis.ttl(key) < 0
186
+ # or you can just remove it immediately
187
+ # redis.del(key) if redis.ttl(key) < 0
188
+ end
189
+
190
+ # do it for channels
191
+ redis.scan_each("graphql-channel:*") do |key|
192
+ redis.expire(key, config.subscription_expiration_seconds) if redis.ttl(key) < 0
193
+ # or you can just remove it immediately
194
+ # redis.del(key) if redis.ttl(key) < 0
195
+ end
196
+ end
190
197
  ```
191
198
 
192
- Or you can change the `redis_prefix` in the `configuration` and then remove all records with the old_prefix
193
- For instance:
199
+ Or you can change the `redis_prefix` in the `configuration` and then remove all records with the old_prefix. For instance:
200
+
201
+ 1. Change the `redis_prefix`. The default `redis_prefix` is `graphql`.
202
+
203
+ 2. Run the ruby script, which remove all records with `old prefix`:
194
204
 
195
- 1. Change the `redis_prefix`. The default `redis_prefix` is `graphql`
196
- 2. Run the ruby script, which remove all records with `old prefix`
197
205
  ```ruby
206
+ GraphQL::AnyCable.with_redis do |redis|
198
207
  redis.scan_each("graphql-*") do |key|
199
208
  redis.del(key)
200
209
  end
210
+ end
201
211
  ```
202
212
 
203
213
  ## Data model
@@ -206,21 +216,21 @@ As in AnyCable there is no place to store subscription data in-memory, it should
206
216
 
207
217
  1. Grouped event subscriptions: `graphql-fingerprints:#{event.topic}` sorted set. Used to find all subscriptions on `GraphQLSchema.subscriptions.trigger`.
208
218
 
209
- ```
219
+ ```sh
210
220
  ZREVRANGE graphql-fingerprints:1:myStats: 0 -1
211
221
  => 1:myStats:/MyStats/fBDZmJU1UGTorQWvOyUeaHVwUxJ3T9SEqnetj6SKGXc=/0/RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o=
212
222
  ```
213
223
 
214
224
  2. Event subscriptions: `graphql-subscriptions:#{event.fingerptint}` set containing identifiers for all subscriptions for given operation with certain context and arguments (serialized in _topic_). Fingerprints are already scoped by topic.
215
225
 
216
- ```
226
+ ```sh
217
227
  SMEMBERS graphql-subscriptions:1:myStats:/MyStats/fBDZmJU1UGTorQWvOyUeaHVwUxJ3T9SEqnetj6SKGXc=/0/RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o=
218
228
  => 52ee8d65-275e-4d22-94af-313129116388
219
229
  ```
220
230
 
221
231
  3. Subscription data: `graphql-subscription:#{subscription_id}` hash contains everything required to evaluate subscription on trigger and create data for client.
222
232
 
223
- ```
233
+ ```sh
224
234
  HGETALL graphql-subscription:52ee8d65-275e-4d22-94af-313129116388
225
235
  => {
226
236
  context: '{"user_id":1,"user":{"__gid__":"Z2lkOi8vZWJheS1tYWcyL1VzZXIvMQ"}}',
@@ -232,29 +242,29 @@ As in AnyCable there is no place to store subscription data in-memory, it should
232
242
 
233
243
  4. Channel subscriptions: `graphql-channel:#{subscription_id}` set containing identifiers for subscriptions created in ActionCable channel to delete them on client disconnect.
234
244
 
235
- ```
245
+ ```sh
236
246
  SMEMBERS graphql-channel:17420c6ed9e
237
247
  => 52ee8d65-275e-4d22-94af-313129116388
238
248
  ```
239
249
 
240
250
  ## Stats
241
251
 
242
- You can grab Redis subscription statistics by calling
252
+ You can grab Redis subscription statistics by calling:
243
253
 
244
254
  ```ruby
245
- GraphQL::AnyCable.stats
255
+ GraphQL::AnyCable.stats
246
256
  ```
247
257
 
248
- It will return a total of the amount of the key with the following prefixes
258
+ It will return a total of the amount of the key with the following prefixes:
249
259
 
250
- ```
251
- graphql-subscription
252
- graphql-fingerprints
253
- graphql-subscriptions
254
- graphql-channel
260
+ ```txt
261
+ graphql-subscription
262
+ graphql-fingerprints
263
+ graphql-subscriptions
264
+ graphql-channel
255
265
  ```
256
266
 
257
- The response will look like this
267
+ The response will look like this:
258
268
 
259
269
  ```json
260
270
  {
@@ -267,13 +277,13 @@ The response will look like this
267
277
  }
268
278
  ```
269
279
 
270
- You can also grab the number of subscribers grouped by subscriptions
280
+ You can also grab the number of subscribers grouped by subscriptions:
271
281
 
272
282
  ```ruby
273
- GraphQL::AnyCable.stats(include_subscriptions: true)
283
+ GraphQL::AnyCable.stats(include_subscriptions: true)
274
284
  ```
275
285
 
276
- It will return the response that contains `subscriptions`
286
+ It will return the response that contains `subscriptions`:
277
287
 
278
288
  ```json
279
289
  {
@@ -290,59 +300,58 @@ It will return the response that contains `subscriptions`
290
300
  }
291
301
  ```
292
302
 
293
- Also, you can set another `scan_count`, if needed.
294
- The default value is 1_000
303
+ Also, you can set another `scan_count`, if needed. The default value is 1_000:
295
304
 
296
305
  ```ruby
297
- GraphQL::AnyCable.stats(scan_count: 100)
306
+ GraphQL::AnyCable.stats(scan_count: 100)
298
307
  ```
299
308
 
300
- We can set statistics data to [Yabeda][] for tracking amount of subscriptions
309
+ We can set statistics data to [Yabeda][] for tracking amount of subscriptions:
301
310
 
302
311
  ```ruby
303
- # config/initializers/metrics.rb
304
- Yabeda.configure do
305
- group :graphql_anycable_statistics do
306
- gauge :subscriptions_count, comment: "Number of graphql-anycable subscriptions"
307
- end
312
+ # config/initializers/metrics.rb
313
+ Yabeda.configure do
314
+ group :graphql_anycable_statistics do
315
+ gauge :subscriptions_count, comment: "Number of graphql-anycable subscriptions"
308
316
  end
317
+ end
309
318
  ```
310
319
 
311
320
  ```ruby
312
- # in your app
313
- statistics = GraphQL::AnyCable.stats[:total]
321
+ # in your app
322
+ statistics = GraphQL::AnyCable.stats[:total]
314
323
 
315
- statistics.each do |key , value|
316
- Yabeda.graphql_anycable_statistics.subscriptions_count.set({name: key}, value)
317
- end
324
+ statistics.each do |key , value|
325
+ Yabeda.graphql_anycable_statistics.subscriptions_count.set({name: key}, value)
326
+ end
318
327
  ```
319
328
 
320
- Or you can use `collect`
329
+ Or you can use `collect`:
330
+
321
331
  ```ruby
322
- # config/initializers/metrics.rb
323
- Yabeda.configure do
324
- group :graphql_anycable_statistics do
325
- gauge :subscriptions_count, comment: "Number of graphql-anycable subscriptions"
326
- end
327
-
328
- collect do
329
- statistics = GraphQL::AnyCable.stats[:total]
332
+ # config/initializers/metrics.rb
333
+ Yabeda.configure do
334
+ group :graphql_anycable_statistics do
335
+ gauge :subscriptions_count, comment: "Number of graphql-anycable subscriptions"
336
+ end
330
337
 
331
- statistics.each do |redis_prefix, value|
332
- graphql_anycable_statistics.subscriptions_count.set({name: redis_prefix}, value)
333
- end
338
+ collect do
339
+ statistics = GraphQL::AnyCable.stats[:total]
340
+
341
+ statistics.each do |redis_prefix, value|
342
+ graphql_anycable_statistics.subscriptions_count.set({name: redis_prefix}, value)
334
343
  end
335
344
  end
345
+ end
336
346
  ```
337
347
 
338
-
339
348
  ## Testing applications which use `graphql-anycable`
340
349
 
341
350
  You can pass custom redis-server URL to AnyCable using ENV variable.
342
351
 
343
- ```bash
344
- REDIS_URL=redis://localhost:6379/5 bundle exec rspec
345
- ```
352
+ ```bash
353
+ REDIS_URL=redis://localhost:6379/5 bundle exec rspec
354
+ ```
346
355
 
347
356
  ## Development
348
357
 
@@ -382,7 +391,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
382
391
 
383
392
  7. GitHub Actions will create a new release, build and push gem into [rubygems.org](https://rubygems.org)! You're done!
384
393
 
385
-
386
394
  ## Contributing
387
395
 
388
396
  Bug reports and pull requests are welcome on GitHub at https://github.com/Envek/graphql-anycable.
data/Rakefile CHANGED
@@ -5,4 +5,11 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- task default: :spec
8
+ begin
9
+ require "rubocop/rake_task"
10
+ RuboCop::RakeTask.new
11
+ rescue LoadError
12
+ task(:rubocop) {}
13
+ end
14
+
15
+ task default: [:rubocop, :spec]
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org" do
2
+ gem "standard", "~> 1.28"
3
+
4
+ gem "rubocop-rspec"
5
+ end
@@ -5,31 +5,33 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require "graphql/anycable/version"
6
6
 
7
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"]
8
+ spec.name = "graphql-anycable"
9
+ spec.version = GraphQL::AnyCable::VERSION
10
+ spec.authors = ["Andrey Novikov"]
11
+ spec.email = ["envek@envek.name"]
12
12
 
13
- spec.summary = <<~SUMMARY
13
+ spec.summary = <<~SUMMARY
14
14
  A drop-in replacement for GraphQL ActionCable subscriptions for AnyCable.
15
15
  SUMMARY
16
16
 
17
- spec.homepage = "https://github.com/Envek/graphql-anycable"
18
- spec.license = "MIT"
17
+ spec.homepage = "https://github.com/Envek/graphql-anycable"
18
+ spec.license = "MIT"
19
19
 
20
20
  # Specify which files should be added to the gem when it is released.
21
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
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
23
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
24
  end
25
- spec.bindir = "exe"
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
+ spec.required_ruby_version = ">= 3.0.0"
30
+
29
31
  spec.add_dependency "anycable-core", "~> 1.1"
30
32
  spec.add_dependency "anyway_config", ">= 1.3", "< 3"
31
- spec.add_dependency "graphql", ">= 1.11", "< 3"
32
- spec.add_dependency "redis", ">= 4.2.0"
33
+ spec.add_dependency "graphql", ">= 1.11", "< 3"
34
+ spec.add_dependency "redis", ">= 4.2.0"
33
35
 
34
36
  spec.add_development_dependency "anycable-rails"
35
37
  spec.add_development_dependency "bundler", "~> 2.0"
@@ -16,11 +16,13 @@ module GraphQL
16
16
  return unless config.subscription_expiration_seconds
17
17
  return unless config.use_redis_object_on_cleanup
18
18
 
19
- redis.scan_each(match: "#{redis_key(adapter::CHANNEL_PREFIX)}*") do |key|
20
- idle = redis.object("IDLETIME", key)
21
- next if idle&.<= config.subscription_expiration_seconds
19
+ AnyCable.with_redis do |redis|
20
+ redis.scan_each(match: "#{redis_key(adapter::CHANNEL_PREFIX)}*") do |key|
21
+ idle = redis.object("IDLETIME", key)
22
+ next if idle&.<= config.subscription_expiration_seconds
22
23
 
23
- redis.del(key)
24
+ redis.del(key)
25
+ end
24
26
  end
25
27
  end
26
28
 
@@ -28,31 +30,37 @@ module GraphQL
28
30
  return unless config.subscription_expiration_seconds
29
31
  return unless config.use_redis_object_on_cleanup
30
32
 
31
- redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTION_PREFIX)}*") do |key|
32
- idle = redis.object("IDLETIME", key)
33
- next if idle&.<= config.subscription_expiration_seconds
33
+ AnyCable.with_redis do |redis|
34
+ redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTION_PREFIX)}*") do |key|
35
+ idle = redis.object("IDLETIME", key)
36
+ next if idle&.<= config.subscription_expiration_seconds
34
37
 
35
- redis.del(key)
38
+ redis.del(key)
39
+ end
36
40
  end
37
41
  end
38
42
 
39
43
  def clean_fingerprint_subscriptions
40
- redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTIONS_PREFIX)}*") do |key|
41
- redis.smembers(key).each do |subscription_id|
42
- next if redis.exists?(redis_key(adapter::SUBSCRIPTION_PREFIX) + subscription_id)
44
+ AnyCable.with_redis do |redis|
45
+ redis.scan_each(match: "#{redis_key(adapter::SUBSCRIPTIONS_PREFIX)}*") do |key|
46
+ redis.smembers(key).each do |subscription_id|
47
+ next if redis.exists?(redis_key(adapter::SUBSCRIPTION_PREFIX) + subscription_id)
43
48
 
44
- redis.srem(key, subscription_id)
49
+ redis.srem(key, subscription_id)
50
+ end
45
51
  end
46
52
  end
47
53
  end
48
54
 
49
55
  def clean_topic_fingerprints
50
- redis.scan_each(match: "#{redis_key(adapter::FINGERPRINTS_PREFIX)}*") do |key|
51
- redis.zremrangebyscore(key, '-inf', '0')
52
- redis.zrange(key, 0, -1).each do |fingerprint|
53
- next if redis.exists?(redis_key(adapter::SUBSCRIPTIONS_PREFIX) + fingerprint)
54
-
55
- redis.zrem(key, fingerprint)
56
+ AnyCable.with_redis do |redis|
57
+ redis.scan_each(match: "#{redis_key(adapter::FINGERPRINTS_PREFIX)}*") do |key|
58
+ redis.zremrangebyscore(key, "-inf", "0")
59
+ redis.zrange(key, 0, -1).each do |fingerprint|
60
+ next if redis.exists?(redis_key(adapter::SUBSCRIPTIONS_PREFIX) + fingerprint)
61
+
62
+ redis.zrem(key, fingerprint)
63
+ end
56
64
  end
57
65
  end
58
66
  end
@@ -63,10 +71,6 @@ module GraphQL
63
71
  GraphQL::Subscriptions::AnyCableSubscriptions
64
72
  end
65
73
 
66
- def redis
67
- GraphQL::AnyCable.redis
68
- end
69
-
70
74
  def config
71
75
  GraphQL::AnyCable.config
72
76
  end
@@ -6,7 +6,7 @@ module GraphQL
6
6
  module AnyCable
7
7
  class Config < Anyway::Config
8
8
  config_name :graphql_anycable
9
- env_prefix :graphql_anycable
9
+ env_prefix :graphql_anycable
10
10
 
11
11
  attr_config subscription_expiration_seconds: nil
12
12
  attr_config use_redis_object_on_cleanup: true
@@ -18,12 +18,14 @@ module GraphQL
18
18
  def collect
19
19
  total_subscriptions_result = {total: {}}
20
20
 
21
- list_prefixes_keys.each do |name, prefix|
22
- total_subscriptions_result[:total][name] = count_by_scan(match: "#{prefix}*")
23
- end
21
+ AnyCable.with_redis do |redis|
22
+ list_prefixes_keys.each do |name, prefix|
23
+ total_subscriptions_result[:total][name] = count_by_scan(redis, match: "#{prefix}*")
24
+ end
24
25
 
25
- if include_subscriptions
26
- total_subscriptions_result[:subscriptions] = group_subscription_stats
26
+ if include_subscriptions
27
+ total_subscriptions_result[:subscriptions] = group_subscription_stats(redis)
28
+ end
27
29
  end
28
30
 
29
31
  total_subscriptions_result
@@ -32,22 +34,22 @@ module GraphQL
32
34
  private
33
35
 
34
36
  # Counting all keys, that match the pattern with iterating by count
35
- def count_by_scan(match:)
37
+ def count_by_scan(redis, match:)
36
38
  sb_amount = 0
37
- cursor = '0'
39
+ cursor = "0"
38
40
 
39
41
  loop do
40
42
  cursor, result = redis.scan(cursor, match: match, count: scan_count)
41
43
  sb_amount += result.count
42
44
 
43
- break if cursor == '0'
45
+ break if cursor == "0"
44
46
  end
45
47
 
46
48
  sb_amount
47
49
  end
48
50
 
49
51
  # Calculate subscribes, grouped by subscriptions
50
- def group_subscription_stats
52
+ def group_subscription_stats(redis)
51
53
  subscription_groups = {}
52
54
 
53
55
  redis.scan_each(match: "#{list_prefixes_keys[:fingerprints]}*", count: scan_count) do |fingerprint_key|
@@ -79,10 +81,6 @@ module GraphQL
79
81
  GraphQL::Subscriptions::AnyCableSubscriptions
80
82
  end
81
83
 
82
- def redis
83
- GraphQL::AnyCable.redis
84
- end
85
-
86
84
  def config
87
85
  GraphQL::AnyCable.config
88
86
  end
@@ -5,7 +5,7 @@ require "graphql-anycable"
5
5
  namespace :graphql do
6
6
  namespace :anycable do
7
7
  desc "Clean up stale graphql channels, subscriptions, and events from redis"
8
- task clean: %i[clean:channels clean:subscriptions clean:events clean:fingerprint_subscriptions clean:topic_fingerprints]
8
+ task clean: %i[clean:channels clean:subscriptions clean:fingerprint_subscriptions clean:topic_fingerprints]
9
9
 
10
10
  namespace :clean do
11
11
  # Clean up old channels
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module AnyCable
5
- VERSION = "1.2.0"
5
+ VERSION = "1.3.1"
6
6
  end
7
7
  end
@@ -54,12 +54,13 @@ module GraphQL
54
54
  class AnyCableSubscriptions < GraphQL::Subscriptions
55
55
  extend Forwardable
56
56
 
57
- def_delegators :"GraphQL::AnyCable", :redis, :config
57
+ def_delegators :"GraphQL::AnyCable", :with_redis, :config
58
+ def_delegators :"::AnyCable", :broadcast
58
59
 
59
- SUBSCRIPTION_PREFIX = "subscription:" # HASH: Stores subscription data: query, context, …
60
- FINGERPRINTS_PREFIX = "fingerprints:" # ZSET: To get fingerprints by topic
60
+ SUBSCRIPTION_PREFIX = "subscription:" # HASH: Stores subscription data: query, context, …
61
+ FINGERPRINTS_PREFIX = "fingerprints:" # ZSET: To get fingerprints by topic
61
62
  SUBSCRIPTIONS_PREFIX = "subscriptions:" # SET: To get subscriptions by fingerprint
62
- CHANNEL_PREFIX = "channel:" # SET: Auxiliary structure for whole channel's subscriptions cleanup
63
+ CHANNEL_PREFIX = "channel:" # SET: Auxiliary structure for whole channel's subscriptions cleanup
63
64
 
64
65
  # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
65
66
  def initialize(serializer: Serialize, **rest)
@@ -70,23 +71,25 @@ module GraphQL
70
71
  # An event was triggered.
71
72
  # Re-evaluate all subscribed queries and push the data over ActionCable.
72
73
  def execute_all(event, object)
73
- fingerprints = redis.zrange(redis_key(FINGERPRINTS_PREFIX) + event.topic, 0, -1)
74
+ fingerprints = with_redis { |redis| redis.zrange(redis_key(FINGERPRINTS_PREFIX) + event.topic, 0, -1) }
74
75
  return if fingerprints.empty?
75
76
 
76
- fingerprint_subscription_ids = Hash[fingerprints.zip(
77
- redis.pipelined do |pipeline|
78
- fingerprints.map do |fingerprint|
79
- pipeline.smembers(redis_key(SUBSCRIPTIONS_PREFIX) + fingerprint)
77
+ fingerprint_subscription_ids = with_redis do |redis|
78
+ fingerprints.zip(
79
+ redis.pipelined do |pipeline|
80
+ fingerprints.map do |fingerprint|
81
+ pipeline.smembers(redis_key(SUBSCRIPTIONS_PREFIX) + fingerprint)
82
+ end
80
83
  end
81
- end
82
- )]
84
+ ).to_h
85
+ end
83
86
 
84
87
  fingerprint_subscription_ids.each do |fingerprint, subscription_ids|
85
88
  execute_grouped(fingerprint, subscription_ids, event, object)
86
89
  end
87
90
 
88
91
  # Call to +trigger+ returns this. Convenient for playing in console
89
- Hash[fingerprint_subscription_ids.map { |k,v| [k, v.size] }]
92
+ fingerprint_subscription_ids.map { |k, v| [k, v.size] }.to_h
90
93
  end
91
94
 
92
95
  # The fingerprint has told us that this response should be shared by all subscribers,
@@ -94,7 +97,7 @@ module GraphQL
94
97
  def execute_grouped(fingerprint, subscription_ids, event, object)
95
98
  return if subscription_ids.empty?
96
99
 
97
- subscription_id = subscription_ids.find { |sid| redis.exists?(redis_key(SUBSCRIPTION_PREFIX) + sid) }
100
+ subscription_id = with_redis { |redis| subscription_ids.find { |sid| redis.exists?(redis_key(SUBSCRIPTION_PREFIX) + sid) } }
98
101
  return unless subscription_id # All subscriptions has expired but haven't cleaned up yet
99
102
 
100
103
  result = execute_update(subscription_id, event, object)
@@ -114,8 +117,8 @@ module GraphQL
114
117
  # @param strean_key [String]
115
118
  # @param result [#to_h] result to send to clients
116
119
  def deliver(stream_key, result)
117
- payload = { result: result.to_h, more: true }.to_json
118
- anycable.broadcast(stream_key, payload)
120
+ payload = {result: result.to_h, more: true}.to_json
121
+ broadcast(stream_key, payload)
119
122
  end
120
123
 
121
124
  # Save query to "storage" (in redis)
@@ -138,37 +141,58 @@ module GraphQL
138
141
  variables: query.provided_variables.to_json,
139
142
  context: @serializer.dump(context.to_h),
140
143
  operation_name: query.operation_name.to_s,
141
- events: events.map { |e| [e.topic, e.fingerprint] }.to_h.to_json,
144
+ events: events.map { |e| [e.topic, e.fingerprint] }.to_h.to_json
142
145
  }
143
146
 
144
- redis.multi do |pipeline|
145
- pipeline.sadd(redis_key(CHANNEL_PREFIX) + subscription_id, [subscription_id])
146
- pipeline.mapped_hmset(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, data)
147
- events.each do |event|
148
- pipeline.zincrby(redis_key(FINGERPRINTS_PREFIX) + event.topic, 1, event.fingerprint)
149
- pipeline.sadd(redis_key(SUBSCRIPTIONS_PREFIX) + event.fingerprint, [subscription_id])
147
+ with_redis do |redis|
148
+ redis.multi do |pipeline|
149
+ pipeline.sadd(redis_key(CHANNEL_PREFIX) + subscription_id, [subscription_id])
150
+ pipeline.mapped_hmset(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, data)
151
+ events.each do |event|
152
+ pipeline.zincrby(redis_key(FINGERPRINTS_PREFIX) + event.topic, 1, event.fingerprint)
153
+ pipeline.sadd(redis_key(SUBSCRIPTIONS_PREFIX) + event.fingerprint, [subscription_id])
154
+ end
155
+ next unless config.subscription_expiration_seconds
156
+ pipeline.expire(redis_key(CHANNEL_PREFIX) + subscription_id, config.subscription_expiration_seconds)
157
+ pipeline.expire(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, config.subscription_expiration_seconds)
150
158
  end
151
- next unless config.subscription_expiration_seconds
152
- pipeline.expire(redis_key(CHANNEL_PREFIX) + subscription_id, config.subscription_expiration_seconds)
153
- pipeline.expire(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, config.subscription_expiration_seconds)
154
159
  end
155
160
  end
156
161
 
157
162
  # Return the query from "storage" (in redis)
158
163
  def read_subscription(subscription_id)
159
- redis.mapped_hmget(
160
- "#{redis_key(SUBSCRIPTION_PREFIX)}#{subscription_id}",
161
- :query_string, :variables, :context, :operation_name
162
- ).tap do |subscription|
163
- return if subscription.values.all?(&:nil?) # Redis returns hash with all nils for missing key
164
-
165
- subscription[:context] = @serializer.load(subscription[:context])
166
- subscription[:variables] = JSON.parse(subscription[:variables])
167
- subscription[:operation_name] = nil if subscription[:operation_name].strip == ""
164
+ with_redis do |redis|
165
+ redis.mapped_hmget(
166
+ "#{redis_key(SUBSCRIPTION_PREFIX)}#{subscription_id}",
167
+ :query_string, :variables, :context, :operation_name
168
+ ).tap do |subscription|
169
+ next if subscription.values.all?(&:nil?) # Redis returns hash with all nils for missing key
170
+
171
+ subscription[:context] = @serializer.load(subscription[:context])
172
+ subscription[:variables] = JSON.parse(subscription[:variables])
173
+ subscription[:operation_name] = nil if subscription[:operation_name].strip == ""
174
+ end
168
175
  end
169
176
  end
170
177
 
171
- def delete_subscription(subscription_id)
178
+ # The channel was closed, forget about it and its subscriptions
179
+ def delete_channel_subscriptions(channel)
180
+ raise(ArgumentError, "Please pass channel instance to #{__method__} in your #unsubscribed method") if channel.is_a?(String)
181
+
182
+ channel_id = read_subscription_id(channel)
183
+
184
+ # Missing in case disconnect happens before #execute
185
+ return unless channel_id
186
+
187
+ with_redis do |redis|
188
+ redis.smembers(redis_key(CHANNEL_PREFIX) + channel_id).each do |subscription_id|
189
+ delete_subscription(subscription_id, redis: redis)
190
+ end
191
+ redis.del(redis_key(CHANNEL_PREFIX) + channel_id)
192
+ end
193
+ end
194
+
195
+ def delete_subscription(subscription_id, redis: AnyCable.redis)
172
196
  events = redis.hget(redis_key(SUBSCRIPTION_PREFIX) + subscription_id, :events)
173
197
  events = events ? JSON.parse(events) : {}
174
198
  fingerprint_subscriptions = {}
@@ -184,32 +208,13 @@ module GraphQL
184
208
  # Clean up fingerprints that doesn't have any subscriptions left
185
209
  redis.pipelined do |pipeline|
186
210
  fingerprint_subscriptions.each do |key, score|
187
- pipeline.zremrangebyscore(key, '-inf', '0') if score.value.zero?
211
+ pipeline.zremrangebyscore(key, "-inf", "0") if score.value.zero?
188
212
  end
189
213
  end
190
214
  end
191
215
 
192
- # The channel was closed, forget about it and its subscriptions
193
- def delete_channel_subscriptions(channel)
194
- raise(ArgumentError, "Please pass channel instance to #{__method__} in your #unsubscribed method") if channel.is_a?(String)
195
-
196
- channel_id = read_subscription_id(channel)
197
-
198
- # Missing in case disconnect happens before #execute
199
- return unless channel_id
200
-
201
- redis.smembers(redis_key(CHANNEL_PREFIX) + channel_id).each do |subscription_id|
202
- delete_subscription(subscription_id)
203
- end
204
- redis.del(redis_key(CHANNEL_PREFIX) + channel_id)
205
- end
206
-
207
216
  private
208
217
 
209
- def anycable
210
- @anycable ||= ::AnyCable.broadcast_adapter
211
- end
212
-
213
218
  def read_subscription_id(channel)
214
219
  return channel.instance_variable_get(:@__sid__) if channel.instance_variable_defined?(:@__sid__)
215
220
 
@@ -11,33 +11,55 @@ require_relative "graphql/subscriptions/anycable_subscriptions"
11
11
 
12
12
  module GraphQL
13
13
  module AnyCable
14
- def self.use(schema, **options)
15
- schema.use GraphQL::Subscriptions::AnyCableSubscriptions, **options
16
- end
14
+ class << self
15
+ def use(schema, **opts)
16
+ schema.use(GraphQL::Subscriptions::AnyCableSubscriptions, **opts)
17
+ end
17
18
 
18
- def self.stats(**options)
19
- Stats.new(**options).collect
20
- end
19
+ def stats(**opts)
20
+ Stats.new(**opts).collect
21
+ end
22
+
23
+ def redis
24
+ warn "Usage of `GraphQL::AnyCable.redis` is deprecated. Instead of `GraphQL::AnyCable.redis.whatever` use `GraphQL::AnyCable.with_redis { |redis| redis.whatever }`"
25
+ @redis ||= with_redis { |conn| conn }
26
+ end
27
+
28
+ def redis=(connector)
29
+ @redis_connector = if connector.is_a?(::Proc)
30
+ connector
31
+ else
32
+ ->(&block) { block.call connector }
33
+ end
34
+ end
35
+
36
+ def with_redis(&block)
37
+ @redis_connector || default_redis_connector
38
+ @redis_connector.call(&block)
39
+ end
40
+
41
+ def config
42
+ @config ||= Config.new
43
+ end
44
+
45
+ def configure
46
+ yield(config) if block_given?
47
+ end
21
48
 
22
- module_function
49
+ private
23
50
 
24
- def redis
25
- @redis ||= begin
51
+ def default_redis_connector
26
52
  adapter = ::AnyCable.broadcast_adapter
27
53
  unless adapter.is_a?(::AnyCable::BroadcastAdapters::Redis)
28
54
  raise "Unsupported AnyCable adapter: #{adapter.class}. " \
29
- "graphql-anycable works only with redis broadcast adapter."
55
+ "Please, configure Redis connector manually:\n\n" \
56
+ " GraphQL::AnyCable.configure do |config|\n" \
57
+ " config.redis = Redis.new(url: 'redis://localhost:6379/0')\n" \
58
+ " end\n"
30
59
  end
31
- ::AnyCable.broadcast_adapter.redis_conn
32
- end
33
- end
34
60
 
35
- def config
36
- @config ||= GraphQL::AnyCable::Config.new
37
- end
38
-
39
- def configure
40
- yield(config) if block_given?
61
+ self.redis = ::AnyCable.broadcast_adapter.redis_conn
62
+ end
41
63
  end
42
64
  end
43
65
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-anycable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Novikov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-07 00:00:00.000000000 Z
11
+ date: 2025-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable-core
@@ -162,7 +162,7 @@ dependencies:
162
162
  - - "~>"
163
163
  - !ruby/object:Gem::Version
164
164
  version: '3.0'
165
- description:
165
+ description:
166
166
  email:
167
167
  - envek@envek.name
168
168
  executables: []
@@ -171,11 +171,14 @@ extra_rdoc_files: []
171
171
  files:
172
172
  - ".github/ISSUE_TEMPLATE/bug_report.md"
173
173
  - ".github/ISSUE_TEMPLATE/feature_request.md"
174
+ - ".github/workflows/lint.yml"
174
175
  - ".github/workflows/release.yml"
175
176
  - ".github/workflows/test.yml"
176
177
  - ".gitignore"
177
178
  - ".rspec"
178
179
  - ".rubocop.yml"
180
+ - ".rubocop/rspec.yml"
181
+ - ".rubocop/strict.yml"
179
182
  - CHANGELOG.md
180
183
  - Gemfile
181
184
  - LICENSE.txt
@@ -183,6 +186,7 @@ files:
183
186
  - Rakefile
184
187
  - bin/console
185
188
  - bin/setup
189
+ - gemfiles/rubocop.gemfile
186
190
  - graphql-anycable.gemspec
187
191
  - lib/Rakefile
188
192
  - lib/graphql-anycable.rb
@@ -199,7 +203,7 @@ homepage: https://github.com/Envek/graphql-anycable
199
203
  licenses:
200
204
  - MIT
201
205
  metadata: {}
202
- post_install_message:
206
+ post_install_message:
203
207
  rdoc_options: []
204
208
  require_paths:
205
209
  - lib
@@ -207,15 +211,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
207
211
  requirements:
208
212
  - - ">="
209
213
  - !ruby/object:Gem::Version
210
- version: '0'
214
+ version: 3.0.0
211
215
  required_rubygems_version: !ruby/object:Gem::Requirement
212
216
  requirements:
213
217
  - - ">="
214
218
  - !ruby/object:Gem::Version
215
219
  version: '0'
216
220
  requirements: []
217
- rubygems_version: 3.5.9
218
- signing_key:
221
+ rubygems_version: 3.5.22
222
+ signing_key:
219
223
  specification_version: 4
220
224
  summary: A drop-in replacement for GraphQL ActionCable subscriptions for AnyCable.
221
225
  test_files: []