graphql-anycable 0.4.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build-release.yml +29 -11
- data/.github/workflows/test.yml +20 -13
- data/CHANGELOG.md +85 -0
- data/Gemfile +1 -1
- data/README.md +51 -18
- data/graphql-anycable.gemspec +3 -3
- data/lib/graphql/anycable/cleaner.rb +28 -3
- data/lib/graphql/anycable/config.rb +11 -1
- data/lib/graphql/anycable/errors.rb +20 -0
- data/lib/graphql/anycable/railtie.rb +1 -1
- data/lib/graphql/anycable/tasks/clean_expired_subscriptions.rake +15 -8
- data/lib/graphql/anycable/version.rb +2 -2
- data/lib/graphql/subscriptions/anycable_subscriptions.rb +126 -26
- data/lib/graphql-anycable.rb +6 -2
- metadata +10 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 335a3ba62f65f0d6dfdd5eab2e1361382d192b5fbc910c591ced6f78296fb4ff
|
4
|
+
data.tar.gz: 9f392057c605fc0b74d6ea2c940409540b87e25ddb0a6dee34e47ad3929f652a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55b8fbf37efbdeff43beb7266ed627491b1a592f9cfe7e803dfd420b49d988a8057cb4ad7df7dea916df4f4c555868e29bace42cc76d2ae692755421a05f8bd5
|
7
|
+
data.tar.gz: d0665b9ec01879f5898c7638a9325c156c2665966552edc20cd013d3dfefbbbbada8e7e22d32e31ce7d8e6e0f59576c9a377ff92ffd1eb8e674e4cef533c7659
|
@@ -1,4 +1,4 @@
|
|
1
|
-
name: Build and release gem
|
1
|
+
name: Build and release gem
|
2
2
|
|
3
3
|
on:
|
4
4
|
push:
|
@@ -21,16 +21,26 @@ jobs:
|
|
21
21
|
git fetch --tags --force # Really fetch annotated tag. See https://github.com/actions/checkout/issues/290#issuecomment-680260080
|
22
22
|
echo ::set-output name=version::${GITHUB_REF#refs/tags/v}
|
23
23
|
echo ::set-output name=subject::$(git for-each-ref $GITHUB_REF --format='%(contents:subject)')
|
24
|
-
# Multiline body for release. See https://github.community/t/set-output-truncates-multiline-strings/16852/5
|
25
24
|
BODY="$(git for-each-ref $GITHUB_REF --format='%(contents:body)')"
|
25
|
+
# Extract changelog entries between this and previous version headers
|
26
|
+
escaped_version=$(echo ${GITHUB_REF#refs/tags/v} | sed -e 's/[]\/$*.^[]/\\&/g')
|
27
|
+
changelog=$(awk "BEGIN{inrelease=0} /## ${escaped_version}/{inrelease=1;next} /## [0-9]+\.[0-9]+\.[0-9]+/{inrelease=0;exit} {if (inrelease) print}" CHANGELOG.md)
|
28
|
+
# Multiline body for release. See https://github.community/t/set-output-truncates-multiline-strings/16852/5
|
29
|
+
BODY="${BODY}"$'\n'"${changelog}"
|
26
30
|
BODY="${BODY//'%'/'%25'}"
|
27
31
|
BODY="${BODY//$'\n'/'%0A'}"
|
28
32
|
BODY="${BODY//$'\r'/'%0D'}"
|
29
33
|
echo "::set-output name=body::$BODY"
|
34
|
+
# Add pre-release option if tag name has any suffix after vMAJOR.MINOR.PATCH
|
35
|
+
if [[ ${GITHUB_REF#refs/tags/} =~ ^v[0-9]+\.[0-9]+\.[0-9]+.+ ]]; then
|
36
|
+
echo ::set-output name=prerelease::true
|
37
|
+
fi
|
30
38
|
- name: Build gem
|
31
39
|
run: gem build
|
40
|
+
- name: Calculate checksums
|
41
|
+
run: sha256sum graphql-anycable-${{ steps.tag.outputs.version }}.gem > SHA256SUM
|
32
42
|
- name: Check version
|
33
|
-
run: ls graphql-anycable-${{ steps.tag.outputs.version }}.gem
|
43
|
+
run: ls -l graphql-anycable-${{ steps.tag.outputs.version }}.gem
|
34
44
|
- name: Create Release
|
35
45
|
id: create_release
|
36
46
|
uses: actions/create-release@v1
|
@@ -41,9 +51,8 @@ jobs:
|
|
41
51
|
release_name: ${{ steps.tag.outputs.subject }}
|
42
52
|
body: ${{ steps.tag.outputs.body }}
|
43
53
|
draft: false
|
44
|
-
prerelease:
|
45
|
-
- name: Upload
|
46
|
-
id: upload-release-asset
|
54
|
+
prerelease: ${{ steps.tag.outputs.prerelease }}
|
55
|
+
- name: Upload built gem as release asset
|
47
56
|
uses: actions/upload-release-asset@v1
|
48
57
|
env:
|
49
58
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@@ -52,13 +61,22 @@ jobs:
|
|
52
61
|
asset_path: graphql-anycable-${{ steps.tag.outputs.version }}.gem
|
53
62
|
asset_name: graphql-anycable-${{ steps.tag.outputs.version }}.gem
|
54
63
|
asset_content_type: application/x-tar
|
55
|
-
- name:
|
64
|
+
- name: Upload checksums as release asset
|
65
|
+
uses: actions/upload-release-asset@v1
|
66
|
+
env:
|
67
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
68
|
+
with:
|
69
|
+
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
70
|
+
asset_path: SHA256SUM
|
71
|
+
asset_name: SHA256SUM
|
72
|
+
asset_content_type: text/plain
|
73
|
+
- name: Publish to GitHub packages
|
74
|
+
env:
|
75
|
+
GEM_HOST_API_KEY: Bearer ${{ secrets.GITHUB_TOKEN }}
|
56
76
|
run: |
|
57
|
-
|
58
|
-
sudo apt-get install oathtool
|
77
|
+
gem push graphql-anycable-${{ steps.tag.outputs.version }}.gem --host https://rubygems.pkg.github.com/${{ github.repository_owner }}
|
59
78
|
- name: Publish to RubyGems
|
60
79
|
env:
|
61
80
|
GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_API_KEY }}"
|
62
|
-
RUBYGEMS_OTP_KEY: "${{ secrets.RUBYGEMS_OTP_KEY }}"
|
63
81
|
run: |
|
64
|
-
gem push graphql-anycable-${{ steps.tag.outputs.version }}.gem
|
82
|
+
gem push graphql-anycable-${{ steps.tag.outputs.version }}.gem
|
data/.github/workflows/test.yml
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
name:
|
1
|
+
name: Tests
|
2
2
|
|
3
3
|
on:
|
4
4
|
pull_request:
|
@@ -10,36 +10,43 @@ on:
|
|
10
10
|
|
11
11
|
jobs:
|
12
12
|
test:
|
13
|
-
name: "
|
14
|
-
if: "! contains(toJSON(github.event.commits.latest.message), '[ci skip]')"
|
13
|
+
name: "GraphQL-Ruby ${{ matrix.graphql }} (interpreter: ${{ matrix.interpreter }}) with AnyCable ${{ matrix.anycable }} on Ruby ${{ matrix.ruby }}"
|
15
14
|
runs-on: ubuntu-latest
|
16
15
|
strategy:
|
17
16
|
fail-fast: false
|
18
17
|
matrix:
|
19
18
|
include:
|
19
|
+
- ruby: "3.0"
|
20
|
+
graphql: '~> 1.12.0'
|
21
|
+
anycable: '~> 1.1.0'
|
22
|
+
interpreter: yes
|
23
|
+
- ruby: "3.0"
|
24
|
+
graphql: '~> 1.12.0'
|
25
|
+
anycable: '~> 1.1.0'
|
26
|
+
interpreter: no
|
20
27
|
- ruby: 2.7
|
21
|
-
graphql: '~> 1.
|
22
|
-
anycable: '~> 1.
|
28
|
+
graphql: '~> 1.12.0'
|
29
|
+
anycable: '~> 1.1.0'
|
23
30
|
interpreter: yes
|
24
31
|
- ruby: 2.7
|
25
|
-
graphql: '~> 1.
|
26
|
-
anycable: '~> 1.
|
32
|
+
graphql: '~> 1.12.0'
|
33
|
+
anycable: '~> 1.1.0'
|
27
34
|
interpreter: no
|
28
35
|
- ruby: 2.6
|
29
|
-
graphql: '~> 1.
|
36
|
+
graphql: '~> 1.11.0'
|
30
37
|
anycable: '~> 1.0.0'
|
31
38
|
interpreter: yes
|
32
39
|
- ruby: 2.6
|
33
|
-
graphql: '~> 1.
|
40
|
+
graphql: '~> 1.11.0'
|
34
41
|
anycable: '~> 1.0.0'
|
35
42
|
interpreter: no
|
36
43
|
- ruby: 2.5
|
37
|
-
graphql: '~> 1.
|
38
|
-
anycable: '~> 0.
|
44
|
+
graphql: '~> 1.11.0'
|
45
|
+
anycable: '~> 1.0.0'
|
39
46
|
interpreter: yes
|
40
47
|
- ruby: 2.5
|
41
|
-
graphql: '~> 1.
|
42
|
-
anycable: '~> 0.
|
48
|
+
graphql: '~> 1.11.0'
|
49
|
+
anycable: '~> 1.0.0'
|
43
50
|
interpreter: no
|
44
51
|
container:
|
45
52
|
image: ruby:${{ matrix.ruby }}
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,91 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## Unreleased
|
9
|
+
|
10
|
+
## 1.1.0 - 2021-11-17
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Support for generating unique channel IDs server-side and storing them in the channel states.
|
15
|
+
|
16
|
+
Currently, we rely on `params["channelId"]` to track subscriptions. This value is random when using `graphql-ruby` JS client, but is not guaranteed to be random in general.
|
17
|
+
|
18
|
+
Now you can opt-in to use server-side IDs by specifying `use_client_provided_uniq_id: false` in YAML config or thru the `GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID=false` env var.
|
19
|
+
|
20
|
+
NOTE: Relying on client-side IDs is deprecated and will be removed in the future versions.
|
21
|
+
|
22
|
+
You must also update your cleanup code in the `Channel#unsubscribed`:
|
23
|
+
|
24
|
+
```diff
|
25
|
+
- channel_id = params.fetch("channelId")
|
26
|
+
- MySchema.subscriptions.delete_channel_subscriptions(channel_id)
|
27
|
+
+ MySchema.subscriptions.delete_channel_subscriptions(self)
|
28
|
+
```
|
29
|
+
|
30
|
+
## 1.0.1 - 2021-04-16
|
31
|
+
|
32
|
+
### Added
|
33
|
+
|
34
|
+
- Guard check for presence of ActionCable channel instance in the GraphQL execution context.
|
35
|
+
|
36
|
+
This allows to detect wrong configuration (user forgot to pass channel into context) or wrong usage (subscription query was sent via HTTP request, not via WebSocket channel) of the library and provides clear error message to gem users.
|
37
|
+
|
38
|
+
## 1.0.0 - 2021-04-01
|
39
|
+
|
40
|
+
### Added
|
41
|
+
|
42
|
+
- Support for [Subscriptions Broadcast](https://graphql-ruby.org/subscriptions/broadcast.html) feature in GraphQL-Ruby 1.11+. [@Envek] ([#15](https://github.com/anycable/graphql-anycable/pull/15))
|
43
|
+
|
44
|
+
### Changed
|
45
|
+
|
46
|
+
- Subscription data storage format changed to support broadcasting feature (see [#15](https://github.com/anycable/graphql-anycable/pull/15))
|
47
|
+
|
48
|
+
### Removed
|
49
|
+
|
50
|
+
- Drop support for GraphQL-Ruby before 1.11
|
51
|
+
|
52
|
+
- Drop support for AnyCable before 1.0
|
53
|
+
|
54
|
+
- Drop `:action_cable_stream` option from context: it is not used in reality.
|
55
|
+
|
56
|
+
See [rmosolgo/graphql-ruby#3076](https://github.com/rmosolgo/graphql-ruby/pull/3076) for details
|
57
|
+
|
58
|
+
### Upgrading notes
|
59
|
+
|
60
|
+
1. Change method of plugging in of this gem from `use GraphQL::Subscriptions::AnyCableSubscriptions` to `use GraphQL::AnyCable`:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
use GraphQL::AnyCable
|
64
|
+
```
|
65
|
+
|
66
|
+
If you need broadcasting, add `broadcast: true` option and ensure that [Interpreter mode](https://graphql-ruby.org/queries/interpreter.html) is enabled.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
use GraphQL::Execution::Interpreter
|
70
|
+
use GraphQL::Analysis::AST
|
71
|
+
use GraphQL::AnyCable, broadcast: true, default_broadcastable: true
|
72
|
+
```
|
73
|
+
|
74
|
+
2. Enable `handle_legacy_subscriptions` setting for seamless upgrade from previous versions:
|
75
|
+
|
76
|
+
```sh
|
77
|
+
GRAPHQL_ANYCABLE_HANDLE_LEGACY_SUBSCRIPTIONS=true
|
78
|
+
```
|
79
|
+
|
80
|
+
Disable or remove this setting when you sure that all clients has re-subscribed (e.g. after `subscription_expiration_seconds` has passed after upgrade) as it imposes small performance penalty.
|
81
|
+
|
82
|
+
## 0.5.0 - 2020-08-26
|
83
|
+
|
84
|
+
### Changed
|
85
|
+
|
86
|
+
- Allow to plug in this gem by calling `use GraphQL::AnyCable` instead of `use GraphQL::Subscriptions::AnyCableSubscriptions`. [@Envek]
|
87
|
+
- Rename `GraphQL::Anycable` constant to `GraphQL::AnyCable` for consistency with AnyCable itself. [@Envek]
|
88
|
+
|
89
|
+
## 0.4.2 - 2020-08-25
|
90
|
+
|
91
|
+
Technical release to test publishing via GitHub Actions.
|
92
|
+
|
8
93
|
## 0.4.1 - 2020-08-21
|
9
94
|
|
10
95
|
### Fixed
|
data/Gemfile
CHANGED
@@ -7,7 +7,7 @@ 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", "~> 1.
|
10
|
+
gem "graphql", ENV.fetch("GRAPHQL_RUBY_VERSION", "~> 1.12")
|
11
11
|
gem "anycable", ENV.fetch("ANYCABLE_VERSION", "~> 1.0")
|
12
12
|
|
13
13
|
group :development, :test do
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
A (mostly) drop-in replacement for default ActionCable subscriptions adapter shipped with [graphql gem] but works with [AnyCable]!
|
4
4
|
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/graphql-anycable.svg)](https://badge.fury.io/rb/graphql-anycable)
|
6
|
+
[![Tests](https://github.com/anycable/graphql-anycable/actions/workflows/test.yml/badge.svg)](https://github.com/anycable/graphql-anycable/actions/workflows/test.yml)
|
6
7
|
|
7
8
|
<a href="https://evilmartians.com/?utm_source=graphql-anycable&utm_campaign=project_page">
|
8
9
|
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
|
@@ -33,7 +34,7 @@ AnyCable must be configured with redis broadcast adapter (this is default).
|
|
33
34
|
Add this line to your application's Gemfile:
|
34
35
|
|
35
36
|
```ruby
|
36
|
-
gem 'graphql-anycable'
|
37
|
+
gem 'graphql-anycable', '~> 1.0'
|
37
38
|
```
|
38
39
|
|
39
40
|
And then execute:
|
@@ -50,7 +51,7 @@ Or install it yourself as:
|
|
50
51
|
|
51
52
|
```ruby
|
52
53
|
class MySchema < GraphQL::Schema
|
53
|
-
use GraphQL::
|
54
|
+
use GraphQL::AnyCable, broadcast: true
|
54
55
|
|
55
56
|
subscription SubscriptionType
|
56
57
|
end
|
@@ -76,8 +77,7 @@ Or install it yourself as:
|
|
76
77
|
end
|
77
78
|
|
78
79
|
def unsubscribed
|
79
|
-
|
80
|
-
MySchema.subscriptions.delete_channel_subscriptions(channel_id)
|
80
|
+
MySchema.subscriptions.delete_channel_subscriptions(self)
|
81
81
|
end
|
82
82
|
|
83
83
|
private
|
@@ -99,6 +99,28 @@ Or install it yourself as:
|
|
99
99
|
MySchema.subscriptions.trigger(:product_updated, {}, Product.first!, scope: account.id)
|
100
100
|
```
|
101
101
|
|
102
|
+
## Broadcasting
|
103
|
+
|
104
|
+
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
|
+
|
106
|
+
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
|
+
|
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.
|
109
|
+
|
110
|
+
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
|
+
|
112
|
+
```ruby
|
113
|
+
class MySchema < GraphQL::Schema
|
114
|
+
use GraphQL::Execution::Interpreter # Required for graphql-ruby before 1.12.4
|
115
|
+
use GraphQL::Analysis::AST
|
116
|
+
use GraphQL::AnyCable, broadcast: true, default_broadcastable: true
|
117
|
+
|
118
|
+
subscription SubscriptionType
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
See GraphQL-Ruby [broadcasting docs](https://graphql-ruby.org/subscriptions/broadcast.html) for more details.
|
123
|
+
|
102
124
|
## Operations
|
103
125
|
|
104
126
|
To avoid filling Redis storage with stale subscription data:
|
@@ -111,13 +133,15 @@ To avoid filling Redis storage with stale subscription data:
|
|
111
133
|
|
112
134
|
## Configuration
|
113
135
|
|
114
|
-
GraphQL-
|
136
|
+
GraphQL-AnyCable uses [anyway_config] to configure itself. There are several possibilities to configure this gem:
|
115
137
|
|
116
138
|
1. Environment variables:
|
117
139
|
|
118
140
|
```.env
|
119
141
|
GRAPHQL_ANYCABLE_SUBSCRIPTION_EXPIRATION_SECONDS=604800
|
120
142
|
GRAPHQL_ANYCABLE_USE_REDIS_OBJECT_ON_CLEANUP=true
|
143
|
+
GRAPHQL_ANYCABLE_HANDLE_LEGACY_SUBSCRIPTIONS=false
|
144
|
+
GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID=false
|
121
145
|
```
|
122
146
|
|
123
147
|
2. YAML configuration files:
|
@@ -127,12 +151,14 @@ GraphQL-Anycable uses [anyway_config] to configure itself. There are several pos
|
|
127
151
|
production:
|
128
152
|
subscription_expiration_seconds: 300 # 5 minutes
|
129
153
|
use_redis_object_on_cleanup: false # For restricted redis installations
|
154
|
+
handle_legacy_subscriptions: false # For seamless upgrade from pre-1.0 versions
|
155
|
+
use_client_provided_uniq_id: false # To avoid problems with non-uniqueness of Apollo channel identifiers
|
130
156
|
```
|
131
157
|
|
132
158
|
3. Configuration from your application code:
|
133
159
|
|
134
160
|
```ruby
|
135
|
-
GraphQL::
|
161
|
+
GraphQL::AnyCable.configure do |config|
|
136
162
|
config.subscription_expiration_seconds = 3600 # 1 hour
|
137
163
|
end
|
138
164
|
```
|
@@ -143,39 +169,46 @@ And any other way provided by [anyway_config]. Check its documentation!
|
|
143
169
|
|
144
170
|
As in AnyCable there is no place to store subscription data in-memory, it should be persisted somewhere to be retrieved on `GraphQLSchema.subscriptions.trigger` and sent to subscribed clients. `graphql-anycable` uses the same Redis database as AnyCable itself.
|
145
171
|
|
146
|
-
1.
|
172
|
+
1. Grouped event subscriptions: `graphql-fingerprints:#{event.topic}` sorted set. Used to find all subscriptions on `GraphQLSchema.subscriptions.trigger`.
|
147
173
|
|
148
174
|
```
|
149
|
-
|
175
|
+
ZREVRANGE graphql-fingerprints:1:myStats: 0 -1
|
176
|
+
=> 1:myStats:/MyStats/fBDZmJU1UGTorQWvOyUeaHVwUxJ3T9SEqnetj6SKGXc=/0/RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o=
|
177
|
+
```
|
178
|
+
|
179
|
+
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.
|
180
|
+
|
181
|
+
```
|
182
|
+
SMEMBERS graphql-subscriptions:1:myStats:/MyStats/fBDZmJU1UGTorQWvOyUeaHVwUxJ3T9SEqnetj6SKGXc=/0/RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o=
|
150
183
|
=> 52ee8d65-275e-4d22-94af-313129116388
|
151
184
|
```
|
152
185
|
|
153
|
-
|
186
|
+
> For backward compatibility with pre-1.0 versions of this gem older `graphql-event:#{event.topic}` set containing subscription identifiers is also supported.
|
187
|
+
>
|
188
|
+
> ```
|
189
|
+
> SMEMBERS graphql-event:1:myStats:
|
190
|
+
> => 52ee8d65-275e-4d22-94af-313129116388
|
191
|
+
> ```
|
192
|
+
|
193
|
+
3. Subscription data: `graphql-subscription:#{subscription_id}` hash contains everything required to evaluate subscription on trigger and create data for client.
|
154
194
|
|
155
195
|
```
|
156
196
|
HGETALL graphql-subscription:52ee8d65-275e-4d22-94af-313129116388
|
157
197
|
=> {
|
158
|
-
context: '{"user_id":1,"user":{"__gid__":"Z2lkOi8vZWJheS1tYWcyL1VzZXIvMQ"}
|
198
|
+
context: '{"user_id":1,"user":{"__gid__":"Z2lkOi8vZWJheS1tYWcyL1VzZXIvMQ"}}',
|
159
199
|
variables: '{}',
|
160
200
|
operation_name: 'MyStats'
|
161
201
|
query_string: 'subscription MyStats { myStatsUpdated { completed total processed __typename } }',
|
162
202
|
}
|
163
203
|
```
|
164
204
|
|
165
|
-
|
205
|
+
4. Channel subscriptions: `graphql-channel:#{channel_id}` set containing identifiers for subscriptions created in ActionCable channel to delete them on client disconnect.
|
166
206
|
|
167
207
|
```
|
168
208
|
SMEMBERS graphql-channel:17420c6ed9e
|
169
209
|
=> 52ee8d65-275e-4d22-94af-313129116388
|
170
210
|
```
|
171
211
|
|
172
|
-
4. Subscription events: `graphql-subscription-events:#{subscription_id}` set containing event topics to delete subscription identifier from event subscriptions set on unsubscribe (or client disconnect).
|
173
|
-
|
174
|
-
```
|
175
|
-
SMEMBERS graphql-subscription-events:52ee8d65-275e-4d22-94af-313129116388
|
176
|
-
=> 1:myStats:
|
177
|
-
```
|
178
|
-
|
179
212
|
## Development
|
180
213
|
|
181
214
|
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.
|
data/graphql-anycable.gemspec
CHANGED
@@ -6,7 +6,7 @@ require "graphql/anycable/version"
|
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = "graphql-anycable"
|
9
|
-
spec.version = GraphQL::
|
9
|
+
spec.version = GraphQL::AnyCable::VERSION
|
10
10
|
spec.authors = ["Andrey Novikov"]
|
11
11
|
spec.email = ["envek@envek.name"]
|
12
12
|
|
@@ -26,9 +26,9 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ["lib"]
|
28
28
|
|
29
|
-
spec.add_dependency "anycable", "
|
29
|
+
spec.add_dependency "anycable", "~> 1.0"
|
30
30
|
spec.add_dependency "anyway_config", ">= 1.3", "< 3"
|
31
|
-
spec.add_dependency "graphql", "~> 1.
|
31
|
+
spec.add_dependency "graphql", "~> 1.11"
|
32
32
|
spec.add_dependency "redis", ">= 4.2.0"
|
33
33
|
|
34
34
|
spec.add_development_dependency "bundler", "~> 2.0"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module GraphQL
|
4
|
-
module
|
4
|
+
module AnyCable
|
5
5
|
module Cleaner
|
6
6
|
extend self
|
7
7
|
|
@@ -9,6 +9,8 @@ module GraphQL
|
|
9
9
|
clean_channels
|
10
10
|
clean_subscriptions
|
11
11
|
clean_events
|
12
|
+
clean_fingerprint_subscriptions
|
13
|
+
clean_topic_fingerprints
|
12
14
|
end
|
13
15
|
|
14
16
|
def clean_channels
|
@@ -36,6 +38,8 @@ module GraphQL
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def clean_events
|
41
|
+
return unless config.handle_legacy_subscriptions
|
42
|
+
|
39
43
|
redis.scan_each(match: "#{adapter::SUBSCRIPTION_EVENTS_PREFIX}*") do |key|
|
40
44
|
subscription_id = key.sub(/\A#{adapter::SUBSCRIPTION_EVENTS_PREFIX}/, "")
|
41
45
|
next if redis.exists?(adapter::SUBSCRIPTION_PREFIX + subscription_id)
|
@@ -48,6 +52,27 @@ module GraphQL
|
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
55
|
+
def clean_fingerprint_subscriptions
|
56
|
+
redis.scan_each(match: "#{adapter::SUBSCRIPTIONS_PREFIX}*") do |key|
|
57
|
+
redis.smembers(key).each do |subscription_id|
|
58
|
+
next if redis.exists?(adapter::SUBSCRIPTION_PREFIX + subscription_id)
|
59
|
+
|
60
|
+
redis.srem(key, subscription_id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def clean_topic_fingerprints
|
66
|
+
redis.scan_each(match: "#{adapter::FINGERPRINTS_PREFIX}*") do |key|
|
67
|
+
redis.zremrangebyscore(key, '-inf', '0')
|
68
|
+
redis.zrange(key, 0, -1).each do |fingerprint|
|
69
|
+
next if redis.exists?(adapter::SUBSCRIPTIONS_PREFIX + fingerprint)
|
70
|
+
|
71
|
+
redis.zrem(key, fingerprint)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
51
76
|
private
|
52
77
|
|
53
78
|
def adapter
|
@@ -55,11 +80,11 @@ module GraphQL
|
|
55
80
|
end
|
56
81
|
|
57
82
|
def redis
|
58
|
-
GraphQL::
|
83
|
+
GraphQL::AnyCable.redis
|
59
84
|
end
|
60
85
|
|
61
86
|
def config
|
62
|
-
GraphQL::
|
87
|
+
GraphQL::AnyCable.config
|
63
88
|
end
|
64
89
|
end
|
65
90
|
end
|
@@ -3,13 +3,23 @@
|
|
3
3
|
require "anyway"
|
4
4
|
|
5
5
|
module GraphQL
|
6
|
-
module
|
6
|
+
module AnyCable
|
7
7
|
class Config < Anyway::Config
|
8
8
|
config_name :graphql_anycable
|
9
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
|
13
|
+
attr_config handle_legacy_subscriptions: false
|
14
|
+
attr_config use_client_provided_uniq_id: true
|
15
|
+
|
16
|
+
on_load do
|
17
|
+
next unless use_client_provided_uniq_id?
|
18
|
+
|
19
|
+
warn "[Deprecated] Using client provided channel uniq IDs could lead to unexpected behaviour, " \
|
20
|
+
" please, set GraphQL::AnyCable.config.use_client_provided_uniq_id = false or GRAPHQL_ANYCABLE_USE_CLIENT_PROVIDED_UNIQ_ID=false, " \
|
21
|
+
" and update the `#unsubscribed` callback code according to the latest docs."
|
22
|
+
end
|
13
23
|
end
|
14
24
|
end
|
15
25
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module AnyCable
|
3
|
+
# This error is thrown when ActionCable channel wasn't provided to subscription implementation.
|
4
|
+
# Typical cases:
|
5
|
+
# 1. application developer forgot to pass ActionCable channel into context
|
6
|
+
# 2. subscription query was sent via usual HTTP request, not websockets as intended
|
7
|
+
class ChannelConfigurationError < ::RuntimeError
|
8
|
+
def initialize(msg = nil)
|
9
|
+
super(msg || <<~DEFAULT_MESSAGE)
|
10
|
+
ActionCable channel wasn't provided in the context for GraphQL query execution!
|
11
|
+
|
12
|
+
This can occur in the following cases:
|
13
|
+
1. ActionCable channel instance wasn't passed into GraphQL execution context in the channel's execute method.
|
14
|
+
See https://github.com/anycable/graphql-anycable#usage
|
15
|
+
2. Subscription query was sent via usual HTTP request, not via WebSocket as intended
|
16
|
+
DEFAULT_MESSAGE
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -5,25 +5,32 @@ 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]
|
9
|
-
|
10
|
-
# Old name that was used earlier
|
11
|
-
task clean_expired_subscriptions: :clean
|
8
|
+
task clean: %i[clean:channels clean:subscriptions clean:events clean:fingerprint_subscriptions clean:topic_fingerprints]
|
12
9
|
|
13
10
|
namespace :clean do
|
14
11
|
# Clean up old channels
|
15
12
|
task :channels do
|
16
|
-
GraphQL::
|
13
|
+
GraphQL::AnyCable::Cleaner.clean_channels
|
17
14
|
end
|
18
15
|
|
19
16
|
# Clean up old subscriptions (they should have expired by themselves)
|
20
17
|
task :subscriptions do
|
21
|
-
GraphQL::
|
18
|
+
GraphQL::AnyCable::Cleaner.clean_subscriptions
|
22
19
|
end
|
23
20
|
|
24
|
-
# Clean up subscription_ids from events for expired subscriptions
|
21
|
+
# Clean up legacy subscription_ids from events for expired subscriptions
|
25
22
|
task :events do
|
26
|
-
GraphQL::
|
23
|
+
GraphQL::AnyCable::Cleaner.clean_events
|
24
|
+
end
|
25
|
+
|
26
|
+
# Clean up subscription_ids from event fingerprints for expired subscriptions
|
27
|
+
task :fingerprint_subscriptions do
|
28
|
+
GraphQL::AnyCable::Cleaner.clean_fingerprint_subscriptions
|
29
|
+
end
|
30
|
+
|
31
|
+
# Clean up fingerprints from event topics. for expired subscriptions
|
32
|
+
task :topic_fingerprints do
|
33
|
+
GraphQL::AnyCable::Cleaner.clean_topic_fingerprints
|
27
34
|
end
|
28
35
|
end
|
29
36
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "anycable"
|
4
4
|
require "graphql/subscriptions"
|
5
|
+
require "graphql/anycable/errors"
|
5
6
|
|
6
7
|
# rubocop: disable Metrics/AbcSize, Metrics/LineLength, Metrics/MethodLength
|
7
8
|
|
@@ -44,8 +45,7 @@ require "graphql/subscriptions"
|
|
44
45
|
# end
|
45
46
|
#
|
46
47
|
# def unsubscribed
|
47
|
-
#
|
48
|
-
# MySchema.subscriptions.delete_channel_subscriptions(channel_id)
|
48
|
+
# MySchema.subscriptions.delete_channel_subscriptions(self)
|
49
49
|
# end
|
50
50
|
# end
|
51
51
|
#
|
@@ -54,12 +54,15 @@ module GraphQL
|
|
54
54
|
class AnyCableSubscriptions < GraphQL::Subscriptions
|
55
55
|
extend Forwardable
|
56
56
|
|
57
|
-
def_delegators :"GraphQL::
|
57
|
+
def_delegators :"GraphQL::AnyCable", :redis, :config
|
58
58
|
|
59
|
-
SUBSCRIPTION_PREFIX
|
60
|
-
|
59
|
+
SUBSCRIPTION_PREFIX = "graphql-subscription:" # HASH: Stores subscription data: query, context, …
|
60
|
+
FINGERPRINTS_PREFIX = "graphql-fingerprints:" # ZSET: To get fingerprints by topic
|
61
|
+
SUBSCRIPTIONS_PREFIX = "graphql-subscriptions:" # SET: To get subscriptions by fingerprint
|
62
|
+
CHANNEL_PREFIX = "graphql-channel:" # SET: Auxiliary structure for whole channel's subscriptions cleanup
|
63
|
+
# For backward compatibility:
|
61
64
|
EVENT_PREFIX = "graphql-event:"
|
62
|
-
|
65
|
+
SUBSCRIPTION_EVENTS_PREFIX = "graphql-subscription-events:"
|
63
66
|
|
64
67
|
# @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
|
65
68
|
def initialize(serializer: Serialize, **rest)
|
@@ -70,43 +73,102 @@ module GraphQL
|
|
70
73
|
# An event was triggered.
|
71
74
|
# Re-evaluate all subscribed queries and push the data over ActionCable.
|
72
75
|
def execute_all(event, object)
|
76
|
+
execute_legacy(event, object) if config.handle_legacy_subscriptions
|
77
|
+
|
78
|
+
fingerprints = redis.zrange(FINGERPRINTS_PREFIX + event.topic, 0, -1)
|
79
|
+
return if fingerprints.empty?
|
80
|
+
|
81
|
+
fingerprint_subscription_ids = Hash[fingerprints.zip(
|
82
|
+
redis.pipelined do
|
83
|
+
fingerprints.map do |fingerprint|
|
84
|
+
redis.smembers(SUBSCRIPTIONS_PREFIX + fingerprint)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
)]
|
88
|
+
|
89
|
+
fingerprint_subscription_ids.each do |fingerprint, subscription_ids|
|
90
|
+
execute_grouped(fingerprint, subscription_ids, event, object)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Call to +trigger+ returns this. Convenient for playing in console
|
94
|
+
Hash[fingerprint_subscription_ids.map { |k,v| [k, v.size] }]
|
95
|
+
end
|
96
|
+
|
97
|
+
# The fingerprint has told us that this response should be shared by all subscribers,
|
98
|
+
# so just run it once, then deliver the result to every subscriber
|
99
|
+
def execute_grouped(fingerprint, subscription_ids, event, object)
|
100
|
+
return if subscription_ids.empty?
|
101
|
+
|
102
|
+
subscription_id = subscription_ids.find { |sid| redis.exists?(SUBSCRIPTION_PREFIX + sid) }
|
103
|
+
return unless subscription_id # All subscriptions has expired but haven't cleaned up yet
|
104
|
+
|
105
|
+
result = execute_update(subscription_id, event, object)
|
106
|
+
return unless result
|
107
|
+
|
108
|
+
# Having calculated the result _once_, send the same payload to all subscribers
|
109
|
+
deliver(SUBSCRIPTIONS_PREFIX + fingerprint, result)
|
110
|
+
end
|
111
|
+
|
112
|
+
# For migration from pre-1.0 graphql-anycable gem
|
113
|
+
def execute_legacy(event, object)
|
73
114
|
redis.smembers(EVENT_PREFIX + event.topic).each do |subscription_id|
|
74
115
|
next unless redis.exists?(SUBSCRIPTION_PREFIX + subscription_id)
|
75
|
-
|
116
|
+
result = execute_update(subscription_id, event, object)
|
117
|
+
next unless result
|
118
|
+
|
119
|
+
deliver(SUBSCRIPTION_PREFIX + subscription_id, result)
|
76
120
|
end
|
77
121
|
end
|
78
122
|
|
123
|
+
# Disable this method as there is no fingerprint (it can be retrieved from subscription though)
|
124
|
+
def execute(subscription_id, event, object)
|
125
|
+
raise NotImplementedError, "Use execute_all method instead of execute to get actual event fingerprint"
|
126
|
+
end
|
127
|
+
|
79
128
|
# This subscription was re-evaluated.
|
80
129
|
# Send it to the specific stream where this client was waiting.
|
81
|
-
|
82
|
-
|
83
|
-
|
130
|
+
# @param strean_key [String]
|
131
|
+
# @param result [#to_h] result to send to clients
|
132
|
+
def deliver(stream_key, result)
|
133
|
+
payload = { result: result.to_h, more: true }.to_json
|
134
|
+
anycable.broadcast(stream_key, payload)
|
84
135
|
end
|
85
136
|
|
86
137
|
# Save query to "storage" (in redis)
|
87
138
|
def write_subscription(query, events)
|
88
139
|
context = query.context.to_h
|
89
|
-
subscription_id = context
|
140
|
+
subscription_id = context.delete(:subscription_id) || build_id
|
90
141
|
channel = context.delete(:channel)
|
91
|
-
|
92
|
-
channel
|
142
|
+
|
143
|
+
raise GraphQL::AnyCable::ChannelConfigurationError unless channel
|
144
|
+
|
145
|
+
channel_uniq_id = config.use_client_provided_uniq_id? ? channel.params["channelId"] : subscription_id
|
146
|
+
|
147
|
+
# Store subscription_id in the channel state to cleanup on disconnect
|
148
|
+
write_subscription_id(channel, channel_uniq_id)
|
149
|
+
|
150
|
+
|
151
|
+
events.each do |event|
|
152
|
+
channel.stream_from(SUBSCRIPTIONS_PREFIX + event.fingerprint)
|
153
|
+
end
|
93
154
|
|
94
155
|
data = {
|
95
156
|
query_string: query.query_string,
|
96
157
|
variables: query.provided_variables.to_json,
|
97
158
|
context: @serializer.dump(context.to_h),
|
98
|
-
operation_name: query.operation_name
|
159
|
+
operation_name: query.operation_name,
|
160
|
+
events: events.map { |e| [e.topic, e.fingerprint] }.to_h.to_json,
|
99
161
|
}
|
100
162
|
|
101
163
|
redis.multi do
|
102
|
-
redis.sadd(CHANNEL_PREFIX +
|
164
|
+
redis.sadd(CHANNEL_PREFIX + channel_uniq_id, subscription_id)
|
103
165
|
redis.mapped_hmset(SUBSCRIPTION_PREFIX + subscription_id, data)
|
104
|
-
redis.sadd(SUBSCRIPTION_EVENTS_PREFIX + subscription_id, events.map(&:topic))
|
105
166
|
events.each do |event|
|
106
|
-
redis.
|
167
|
+
redis.zincrby(FINGERPRINTS_PREFIX + event.topic, 1, event.fingerprint)
|
168
|
+
redis.sadd(SUBSCRIPTIONS_PREFIX + event.fingerprint, subscription_id)
|
107
169
|
end
|
108
170
|
next unless config.subscription_expiration_seconds
|
109
|
-
redis.expire(CHANNEL_PREFIX +
|
171
|
+
redis.expire(CHANNEL_PREFIX + channel_uniq_id, config.subscription_expiration_seconds)
|
110
172
|
redis.expire(SUBSCRIPTION_PREFIX + subscription_id, config.subscription_expiration_seconds)
|
111
173
|
end
|
112
174
|
end
|
@@ -117,25 +179,52 @@ module GraphQL
|
|
117
179
|
"#{SUBSCRIPTION_PREFIX}#{subscription_id}",
|
118
180
|
:query_string, :variables, :context, :operation_name
|
119
181
|
).tap do |subscription|
|
182
|
+
return if subscription.values.all?(&:nil?) # Redis returns hash with all nils for missing key
|
183
|
+
|
120
184
|
subscription[:context] = @serializer.load(subscription[:context])
|
121
185
|
subscription[:variables] = JSON.parse(subscription[:variables])
|
122
186
|
subscription[:operation_name] = nil if subscription[:operation_name].strip == ""
|
123
187
|
end
|
124
188
|
end
|
125
189
|
|
126
|
-
# The channel was closed, forget about it.
|
127
190
|
def delete_subscription(subscription_id)
|
128
|
-
|
191
|
+
events = redis.hget(SUBSCRIPTION_PREFIX + subscription_id, :events)
|
192
|
+
events = events ? JSON.parse(events) : {}
|
193
|
+
fingerprint_subscriptions = {}
|
194
|
+
redis.pipelined do
|
195
|
+
events.each do |topic, fingerprint|
|
196
|
+
redis.srem(SUBSCRIPTIONS_PREFIX + fingerprint, subscription_id)
|
197
|
+
score = redis.zincrby(FINGERPRINTS_PREFIX + topic, -1, fingerprint)
|
198
|
+
fingerprint_subscriptions[FINGERPRINTS_PREFIX + topic] = score
|
199
|
+
end
|
200
|
+
# Delete subscription itself
|
201
|
+
redis.del(SUBSCRIPTION_PREFIX + subscription_id)
|
202
|
+
end
|
203
|
+
# Clean up fingerprints that doesn't have any subscriptions left
|
204
|
+
redis.pipelined do
|
205
|
+
fingerprint_subscriptions.each do |key, score|
|
206
|
+
redis.zremrangebyscore(key, '-inf', '0') if score.value.zero?
|
207
|
+
end
|
208
|
+
end
|
209
|
+
delete_legacy_subscription(subscription_id)
|
210
|
+
end
|
211
|
+
|
212
|
+
def delete_legacy_subscription(subscription_id)
|
213
|
+
return unless config.handle_legacy_subscriptions
|
214
|
+
|
129
215
|
events = redis.smembers(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
|
130
|
-
|
131
|
-
|
216
|
+
redis.pipelined do
|
217
|
+
events.each do |event_topic|
|
218
|
+
redis.srem(EVENT_PREFIX + event_topic, subscription_id)
|
219
|
+
end
|
220
|
+
redis.del(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
|
132
221
|
end
|
133
|
-
# Delete subscription itself
|
134
|
-
redis.del(SUBSCRIPTION_EVENTS_PREFIX + subscription_id)
|
135
|
-
redis.del(SUBSCRIPTION_PREFIX + subscription_id)
|
136
222
|
end
|
137
223
|
|
138
|
-
|
224
|
+
# The channel was closed, forget about it and its subscriptions
|
225
|
+
def delete_channel_subscriptions(channel_or_id)
|
226
|
+
# For backward compatibility
|
227
|
+
channel_id = channel_or_id.is_a?(String) ? channel_or_id : read_subscription_id(channel_or_id)
|
139
228
|
redis.smembers(CHANNEL_PREFIX + channel_id).each do |subscription_id|
|
140
229
|
delete_subscription(subscription_id)
|
141
230
|
end
|
@@ -147,6 +236,17 @@ module GraphQL
|
|
147
236
|
def anycable
|
148
237
|
@anycable ||= ::AnyCable.broadcast_adapter
|
149
238
|
end
|
239
|
+
|
240
|
+
def read_subscription_id(channel)
|
241
|
+
return channel.instance_variable_get(:@__sid__) if channel.instance_variable_defined?(:@__sid__)
|
242
|
+
|
243
|
+
channel.instance_variable_set(:@__sid__, channel.connection.socket.istate["sid"])
|
244
|
+
end
|
245
|
+
|
246
|
+
def write_subscription_id(channel, val)
|
247
|
+
channel.connection.socket.istate["sid"] = val
|
248
|
+
channel.instance_variable_set(:@__sid__, val)
|
249
|
+
end
|
150
250
|
end
|
151
251
|
end
|
152
252
|
end
|
data/lib/graphql-anycable.rb
CHANGED
@@ -9,7 +9,11 @@ require_relative "graphql/anycable/railtie" if defined?(Rails)
|
|
9
9
|
require_relative "graphql/subscriptions/anycable_subscriptions"
|
10
10
|
|
11
11
|
module GraphQL
|
12
|
-
module
|
12
|
+
module AnyCable
|
13
|
+
def self.use(schema, **options)
|
14
|
+
schema.use GraphQL::Subscriptions::AnyCableSubscriptions, **options
|
15
|
+
end
|
16
|
+
|
13
17
|
module_function
|
14
18
|
|
15
19
|
def redis
|
@@ -24,7 +28,7 @@ module GraphQL
|
|
24
28
|
end
|
25
29
|
|
26
30
|
def config
|
27
|
-
@config ||= GraphQL::
|
31
|
+
@config ||= GraphQL::AnyCable::Config.new
|
28
32
|
end
|
29
33
|
|
30
34
|
def configure
|
metadata
CHANGED
@@ -1,35 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-anycable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrey Novikov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-11-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: anycable
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 0.6.0
|
20
|
-
- - "<"
|
17
|
+
- - "~>"
|
21
18
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
19
|
+
version: '1.0'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
|
-
- - "
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 0.6.0
|
30
|
-
- - "<"
|
24
|
+
- - "~>"
|
31
25
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
26
|
+
version: '1.0'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: anyway_config
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,14 +50,14 @@ dependencies:
|
|
56
50
|
requirements:
|
57
51
|
- - "~>"
|
58
52
|
- !ruby/object:Gem::Version
|
59
|
-
version: '1.
|
53
|
+
version: '1.11'
|
60
54
|
type: :runtime
|
61
55
|
prerelease: false
|
62
56
|
version_requirements: !ruby/object:Gem::Requirement
|
63
57
|
requirements:
|
64
58
|
- - "~>"
|
65
59
|
- !ruby/object:Gem::Version
|
66
|
-
version: '1.
|
60
|
+
version: '1.11'
|
67
61
|
- !ruby/object:Gem::Dependency
|
68
62
|
name: redis
|
69
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -161,6 +155,7 @@ files:
|
|
161
155
|
- lib/graphql/anycable.rb
|
162
156
|
- lib/graphql/anycable/cleaner.rb
|
163
157
|
- lib/graphql/anycable/config.rb
|
158
|
+
- lib/graphql/anycable/errors.rb
|
164
159
|
- lib/graphql/anycable/railtie.rb
|
165
160
|
- lib/graphql/anycable/tasks/clean_expired_subscriptions.rake
|
166
161
|
- lib/graphql/anycable/version.rb
|
@@ -184,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
184
179
|
- !ruby/object:Gem::Version
|
185
180
|
version: '0'
|
186
181
|
requirements: []
|
187
|
-
rubygems_version: 3.1.
|
182
|
+
rubygems_version: 3.1.6
|
188
183
|
signing_key:
|
189
184
|
specification_version: 4
|
190
185
|
summary: A drop-in replacement for GraphQL ActionCable subscriptions for AnyCable.
|