anycable-rails-core 1.5.3 → 1.6.2
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 +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +10 -75
- data/lib/anycable/rails/action_cable_ext/channel.rb +20 -11
- data/lib/anycable/rails/action_cable_ext/remote_connections.rb +4 -14
- data/lib/anycable/rails/channel/presence.rb +36 -0
- data/lib/anycable/rails/channel.rb +9 -0
- data/lib/anycable/rails/compatibility/rubocop.rb +2 -25
- data/lib/anycable/rails/compatibility.rb +2 -0
- data/lib/anycable/rails/config.rb +2 -0
- data/lib/anycable/rails/connection.rb +62 -33
- data/lib/anycable/rails/connection_factory.rb +20 -1
- data/lib/anycable/rails/connections/persistent_session.rb +5 -0
- data/lib/anycable/rails/connections/serializable_identification.rb +19 -0
- data/lib/anycable/rails/connections/session_proxy.rb +2 -2
- data/lib/anycable/rails/ext/jwt.rb +1 -1
- data/lib/anycable/rails/ext/signed_streams.rb +21 -0
- data/lib/anycable/rails/ext/whisper.rb +34 -0
- data/lib/anycable/rails/ext.rb +4 -1
- data/lib/anycable/rails/helper.rb +2 -2
- data/lib/anycable/rails/next/action_cable_ext/channel.rb +36 -0
- data/lib/anycable/rails/next/action_cable_ext/connection.rb +29 -0
- data/lib/anycable/rails/next/connection/persistent_session.rb +39 -0
- data/lib/anycable/rails/next/connection.rb +226 -0
- data/lib/anycable/rails/railtie.rb +4 -8
- data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/instance_vars.rb +1 -1
- data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/periodical_timers.rb +3 -8
- data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/stream_from.rb +3 -5
- data/lib/anycable/rails/rubocop.rb +27 -0
- data/lib/anycable/rails/version.rb +1 -1
- data/lib/anycable/rails.rb +2 -1
- data/lib/generators/anycable/bin/templates/bin/anycable-go.tt +5 -0
- data/lib/generators/anycable/download/download_generator.rb +16 -5
- data/lib/generators/anycable/setup/setup_generator.rb +280 -38
- data/lib/generators/anycable/setup/templates/Procfile.dev.tt +2 -2
- data/lib/generators/anycable/setup/templates/anycable.toml.tt +81 -0
- data/lib/generators/anycable/setup/templates/bin/anycable-go.tt +5 -0
- data/lib/generators/anycable/setup/templates/bin/dev +17 -0
- data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +20 -33
- data/lib/generators/anycable/setup/templates/config/cable.yml.tt +2 -5
- metadata +27 -16
- data/lib/anycable/rails/action_cable_ext/signed_streams.rb +0 -31
- /data/lib/anycable/rails/{compatibility/rubocop → rubocop}/config/default.yml +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae5f5e00ae334a317de5b59685ff57de668c2f102125f48bc8309a7330787950
|
|
4
|
+
data.tar.gz: f15f057412a3c0a0dbf759bd8c8d47abee1bee60a9b68ca62983ced577ea9397
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b780c7ca124e8f84d708e94ec78a27b50e9b36cbf524264cea3582bb171b037e40f4f92d8292f115810b988dfe93d45a942c779550943484bfad9810c1865f3
|
|
7
|
+
data.tar.gz: 916571c822234a92ec9bbb1839c06c8a0cc9a6f434c370c0b9bccd6486a450c2dc2ec88e27069d3b14bff093e245d0a83e6aed71fc7bcd1f20b298ae479edde7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## main
|
|
4
|
+
|
|
5
|
+
## 1.6.2 (2026-04-02)
|
|
6
|
+
|
|
7
|
+
- Upgrade setup generator. ([@palkan][])
|
|
8
|
+
|
|
9
|
+
## 1.6.1 (2025-12-18)
|
|
10
|
+
|
|
11
|
+
- Ensure `bin/dist/anycable-go` has execution permission as part of `bin/anycable-go` ([@dmorgan-fa][])
|
|
12
|
+
|
|
13
|
+
## 1.6.0 (2025-03-18)
|
|
14
|
+
|
|
15
|
+
- Update `anycable:download` generator to support v1.6+. ([@palkan][])
|
|
16
|
+
|
|
17
|
+
- Add `#anycable_token_meta_tag` helper. ([@palkan][])
|
|
18
|
+
|
|
19
|
+
It generates a meta tag with the AnyCable JWT token (w/o the URL).
|
|
20
|
+
|
|
21
|
+
- Add Presence API. ([@palkan][])
|
|
22
|
+
|
|
23
|
+
## 1.5.5 (2024-12-12)
|
|
24
|
+
|
|
25
|
+
- Publish RuboCop cops as a separate gem (`rubocop-anycable-rails`). ([@palkan][])
|
|
26
|
+
|
|
27
|
+
- Upgrade RuboCop cops. ([@palkan][])
|
|
28
|
+
|
|
29
|
+
## 1.5.4 (2024-10-08)
|
|
30
|
+
|
|
31
|
+
- Add [actioncable-next](https://github.com/anycable/actioncable-next) support. ([@palkan][])
|
|
32
|
+
|
|
33
|
+
- Generate `anycable.toml` in `anycable:setup` generator. ([@palkan][])
|
|
4
34
|
|
|
5
35
|
## 1.5.3 (2024-09-12)
|
|
6
36
|
|
data/README.md
CHANGED
|
@@ -1,97 +1,32 @@
|
|
|
1
|
-
[](https://rubygems.org/gems/anycable)
|
|
2
2
|
[](https://github.com/anycable/anycable-rails/actions)
|
|
3
3
|
[](https://docs.anycable.io/rails/getting_started)
|
|
4
4
|
|
|
5
|
-
# AnyCable Rails
|
|
5
|
+
# AnyCable SDK for Ruby on Rails
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<img align="right" height="150" width="129"
|
|
8
|
+
title="AnyCable logo" src="https://docs.anycable.io/assets/images/logo.svg">
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
[AnyCable](https://github.com/anycable/anycable) is an open-source language-agnostic realtime server for reliable two-way communication over WebSockets and SSE.
|
|
11
|
+
This repository contains code for AnyCable Rails SKD that allows you to use AnyCable as a drop-in replacement for Action Cable.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
🌐 [Website](https://anycable.io) · 📚 [Documentation](https://docs.anycable.io/rails/getting_started) · 💾 [Example Rails Application](https://github.com/anycable/anycable_rails_demo)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
📑 [Documentation](https://docs.anycable.io/rails/getting_started).
|
|
16
|
-
|
|
17
|
-
> [AnyCable Pro](https://docs.anycable.io/pro) has been launched 🚀
|
|
18
|
-
|
|
19
|
-
<a href="https://evilmartians.com/">
|
|
20
|
-
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
|
21
|
-
|
|
22
|
-
## Requirements
|
|
23
|
-
|
|
24
|
-
- Ruby >= 2.6
|
|
25
|
-
- Rails >= 6.0 (Rails 5.1 could work but we're no longer enforce compatibility on CI)
|
|
26
|
-
- Redis (see [other options](https://github.com/anycable/anycable/issues/2) for broadcasting)
|
|
27
|
-
|
|
28
|
-
## Usage
|
|
15
|
+
## Quick Start
|
|
29
16
|
|
|
30
17
|
Add `anycable-rails` gem to your Gemfile:
|
|
31
18
|
|
|
32
19
|
```ruby
|
|
33
20
|
gem "anycable-rails"
|
|
34
|
-
|
|
35
|
-
# when using Redis broadcast adapter
|
|
36
|
-
gem "redis", ">= 4.0"
|
|
37
21
|
```
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
After the gem was installed, you can run an interactive wizard to configure your Rails application for using with AnyCable by running a generator:
|
|
23
|
+
Then run our interactive setup command:
|
|
42
24
|
|
|
43
25
|
```sh
|
|
44
26
|
bundle exec rails g anycable:setup
|
|
45
27
|
```
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Specify AnyCable subscription adapter for Action Cable:
|
|
50
|
-
|
|
51
|
-
```yml
|
|
52
|
-
# config/cable.yml
|
|
53
|
-
development:
|
|
54
|
-
adapter: any_cable # or anycable
|
|
55
|
-
|
|
56
|
-
production:
|
|
57
|
-
adapter: any_cable
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
and specify AnyCable WebSocket server URL:
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
# For development it's likely the localhost
|
|
64
|
-
|
|
65
|
-
# config/environments/development.rb
|
|
66
|
-
config.action_cable.url = "ws://localhost:8080/cable"
|
|
67
|
-
|
|
68
|
-
# For production it's likely to have a sub-domain and secure connection
|
|
69
|
-
|
|
70
|
-
# config/environments/production.rb
|
|
71
|
-
config.action_cable.url = "wss://ws.example.com/cable"
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Then, run AnyCable RPC server:
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
$ bundle exec anycable
|
|
78
|
-
|
|
79
|
-
# don't forget to provide Rails env
|
|
80
|
-
|
|
81
|
-
$ RAILS_ENV=production bundle exec anycable
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
And, finally, run AnyCable WebSocket server, e.g. [anycable-go](https://docs.anycable.io/anycable-go/getting_started):
|
|
85
|
-
|
|
86
|
-
```sh
|
|
87
|
-
anycable-go --host=localhost --port=8080
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
See [documentation](https://docs.anycable.io/rails/getting_started) for more information on AnyCable + Rails usage.
|
|
91
|
-
|
|
92
|
-
## Action Cable Compatibility
|
|
93
|
-
|
|
94
|
-
See [documentation](https://docs.anycable.io/rails/compatibility).
|
|
29
|
+
Learn more [here](https://docs.anycable.io/rails/getting_started).
|
|
95
30
|
|
|
96
31
|
## Contributing
|
|
97
32
|
|
|
@@ -3,17 +3,6 @@
|
|
|
3
3
|
require "action_cable"
|
|
4
4
|
|
|
5
5
|
ActionCable::Channel::Base.prepend(Module.new do
|
|
6
|
-
def subscribe_to_channel
|
|
7
|
-
super unless anycabled? && !@__anycable_subscribing__
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def handle_subscribe
|
|
11
|
-
@__anycable_subscribing__ = true
|
|
12
|
-
subscribe_to_channel
|
|
13
|
-
ensure
|
|
14
|
-
@__anycable_subscribing__ = false
|
|
15
|
-
end
|
|
16
|
-
|
|
17
6
|
def start_periodic_timers
|
|
18
7
|
super unless anycabled?
|
|
19
8
|
end
|
|
@@ -57,3 +46,23 @@ ActionCable::Channel::Base.prepend(Module.new do
|
|
|
57
46
|
connection.instance_variable_defined?(:@anycable_socket)
|
|
58
47
|
end
|
|
59
48
|
end)
|
|
49
|
+
|
|
50
|
+
# Handle $pubsub channel in Subscriptions
|
|
51
|
+
ActionCable::Connection::Subscriptions.prepend(Module.new do
|
|
52
|
+
# The contents are mostly copied from the original,
|
|
53
|
+
# there is no good way to configure channels mapping due to #safe_constantize
|
|
54
|
+
# and the layers of JSON
|
|
55
|
+
# https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/connection/subscriptions.rb
|
|
56
|
+
def add(data)
|
|
57
|
+
id_key = data["identifier"]
|
|
58
|
+
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
|
59
|
+
|
|
60
|
+
return if subscriptions.key?(id_key)
|
|
61
|
+
|
|
62
|
+
return super unless id_options[:channel] == "$pubsub"
|
|
63
|
+
|
|
64
|
+
subscription = AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
|
|
65
|
+
subscriptions[id_key] = subscription
|
|
66
|
+
subscription.subscribe_to_channel
|
|
67
|
+
end
|
|
68
|
+
end)
|
|
@@ -3,20 +3,10 @@
|
|
|
3
3
|
require "action_cable/remote_connections"
|
|
4
4
|
|
|
5
5
|
ActionCable::RemoteConnections::RemoteConnection.include(AnyCable::Rails::Connections::SerializableIdentification)
|
|
6
|
-
|
|
7
6
|
ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
super() unless AnyCable::Rails.enabled?
|
|
13
|
-
::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
|
|
14
|
-
end
|
|
15
|
-
else
|
|
16
|
-
def disconnect(reconnect: true)
|
|
17
|
-
# Legacy Action Cable functionality if case we're not fully migrated yet
|
|
18
|
-
super unless AnyCable::Rails.enabled?
|
|
19
|
-
::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
|
|
20
|
-
end
|
|
7
|
+
def disconnect(reconnect: true)
|
|
8
|
+
# Legacy Action Cable functionality if case we're not fully migrated yet
|
|
9
|
+
return super unless AnyCable::Rails.enabled?
|
|
10
|
+
::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
|
|
21
11
|
end
|
|
22
12
|
end)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyCable
|
|
4
|
+
module Rails
|
|
5
|
+
module Channel
|
|
6
|
+
# Presence API for Action Cable channels (backed by AnyCable)
|
|
7
|
+
module Presence
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
def join_presence(stream = nil, id: user_presence_id, info: user_presence_info)
|
|
11
|
+
return unless anycabled?
|
|
12
|
+
|
|
13
|
+
stream ||= connection.anycable_socket.streams[:start].first || raise(ArgumentError, "Provide a stream name for presence updates")
|
|
14
|
+
|
|
15
|
+
connection.anycable_socket.presence_join(stream, id.to_s, info)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def leave_presence(id = user_presence_id)
|
|
19
|
+
return unless anycabled?
|
|
20
|
+
|
|
21
|
+
connection.anycable_socket.presence_leave(id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def user_presence_id
|
|
27
|
+
connection.connection_identifier
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def user_presence_info
|
|
31
|
+
# nothing
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require_relative "rubocop/cops/anycable/stream_from"
|
|
7
|
-
require_relative "rubocop/cops/anycable/periodical_timers"
|
|
8
|
-
require_relative "rubocop/cops/anycable/instance_vars"
|
|
9
|
-
|
|
10
|
-
module RuboCop
|
|
11
|
-
module AnyCable # :nodoc:
|
|
12
|
-
CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
|
|
13
|
-
|
|
14
|
-
# Merge anycable config into default configuration
|
|
15
|
-
# See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
|
|
16
|
-
def self.inject!
|
|
17
|
-
path = CONFIG_DEFAULT.to_s
|
|
18
|
-
puts "configuration from #{path}" if ConfigLoader.debug?
|
|
19
|
-
hash = ConfigLoader.send(:load_yaml_configuration, path)
|
|
20
|
-
config = Config.new(hash, path)
|
|
21
|
-
config = ConfigLoader.merge_with_default(config, path)
|
|
22
|
-
ConfigLoader.instance_variable_set(:@default_configuration, config)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
RuboCop::AnyCable.inject!
|
|
3
|
+
# For backwards compatibility
|
|
4
|
+
require_relative "../rubocop"
|
|
@@ -9,6 +9,7 @@ require "anyway/rails"
|
|
|
9
9
|
# - `access_logs_disabled` (defaults to true) — whether to print Started/Finished logs
|
|
10
10
|
# - `persistent_session_enabled` (defaults to false) — whether to store session changes in the connection state
|
|
11
11
|
# - `embedded` (defaults to false) — whether to run RPC server inside a Rails server process
|
|
12
|
+
# - `http_rpc` (default to false) - whether to mount HTTP RPC server or not
|
|
12
13
|
# - `http_rpc_mount_path` (defaults to nil) — path to mount HTTP RPC server
|
|
13
14
|
# - `batch_broadcasts` (defaults to false) — whether to batch broadcasts automatically for code wrapped with Rails executor
|
|
14
15
|
# - `jwt_param` (defaults to 'jid') — the name of the JWT authentication query paramter or header
|
|
@@ -18,6 +19,7 @@ AnyCable::Config.attr_config(
|
|
|
18
19
|
persistent_session_enabled: false,
|
|
19
20
|
embedded: false,
|
|
20
21
|
jwt_param: "jid",
|
|
22
|
+
http_rpc: false,
|
|
21
23
|
http_rpc_mount_path: nil,
|
|
22
24
|
batch_broadcasts: false,
|
|
23
25
|
socket_id_header: "X-Socket-ID",
|
|
@@ -31,15 +31,69 @@ module AnyCable
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
refine ActionCable::Connection::Subscriptions do
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
# Override the original #execute_command to pre-initialize the channel for unsubscribe/message and
|
|
35
|
+
# return true/false to indicate successful/unsuccessful subscription.
|
|
36
|
+
# We also must not lose any exceptions raised in the process.
|
|
37
|
+
def execute_rpc_command(data)
|
|
38
|
+
# First, verify the channel name
|
|
39
|
+
raise "Channel not found: #{ActiveSupport::JSON.decode(data["identifier"]).fetch("channel")}" unless subscription_class_from_identifier(data["identifier"])
|
|
40
|
+
|
|
41
|
+
if data["command"] == "subscribe"
|
|
42
|
+
add data
|
|
43
|
+
subscription = subscriptions[data["identifier"]]
|
|
44
|
+
return !(subscription.nil? || subscription.rejected?)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
load(data["identifier"])
|
|
48
|
+
|
|
49
|
+
case data["command"]
|
|
50
|
+
when "unsubscribe"
|
|
51
|
+
remove data
|
|
52
|
+
when "message"
|
|
53
|
+
perform_action data
|
|
54
|
+
when "whisper"
|
|
55
|
+
whisper data
|
|
56
|
+
else
|
|
57
|
+
raise UnknownCommandError, data["command"]
|
|
58
|
+
end
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Restore channels from the list of identifiers and the state
|
|
64
|
+
def restore(subscriptions, istate)
|
|
65
|
+
subscriptions.each do |id|
|
|
66
|
+
channel = load(id)
|
|
67
|
+
channel.__istate__ = ActiveSupport::JSON.decode(istate[id]) if istate[id]
|
|
40
68
|
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Find or create a channel for a given identifier
|
|
72
|
+
def load(identifier)
|
|
73
|
+
return subscriptions[identifier] if subscriptions[identifier]
|
|
41
74
|
|
|
42
|
-
|
|
75
|
+
subscription = subscription_from_identifier(identifier)
|
|
76
|
+
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}" unless subscription
|
|
77
|
+
|
|
78
|
+
subscriptions[identifier] = subscription
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def subscription_class_from_identifier(id_key)
|
|
82
|
+
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
|
83
|
+
if id_options[:channel] == "$pubsub"
|
|
84
|
+
PubSubChannel
|
|
85
|
+
else
|
|
86
|
+
id_options[:channel].safe_constantize
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def subscription_from_identifier(id_key)
|
|
91
|
+
subscription_klass = subscription_class_from_identifier(id_key)
|
|
92
|
+
|
|
93
|
+
if subscription_klass && subscription_klass < ActionCable::Channel::Base
|
|
94
|
+
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
|
95
|
+
subscription_klass.new(connection, id_key, id_options)
|
|
96
|
+
end
|
|
43
97
|
end
|
|
44
98
|
end
|
|
45
99
|
end)
|
|
@@ -74,15 +128,8 @@ module AnyCable
|
|
|
74
128
|
conn.cached_ids = {}
|
|
75
129
|
conn.anycable_request_builder = self
|
|
76
130
|
|
|
77
|
-
return unless subscriptions
|
|
78
|
-
|
|
79
131
|
# Pre-initialize channels (for disconnect)
|
|
80
|
-
subscriptions.
|
|
81
|
-
channel = conn.subscriptions.fetch(id)
|
|
82
|
-
next unless socket.istate[id]
|
|
83
|
-
|
|
84
|
-
channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
|
|
85
|
-
end
|
|
132
|
+
conn.subscriptions.restore(subscriptions, socket.istate) if subscriptions
|
|
86
133
|
end
|
|
87
134
|
|
|
88
135
|
def handle_open
|
|
@@ -111,26 +158,8 @@ module AnyCable
|
|
|
111
158
|
|
|
112
159
|
def handle_channel_command(identifier, command, data)
|
|
113
160
|
conn.run_callbacks :command do
|
|
114
|
-
|
|
115
|
-
# since we MUST return true of false, depending on the status
|
|
116
|
-
# of execution
|
|
117
|
-
channel = conn.subscriptions.fetch(identifier)
|
|
118
|
-
case command
|
|
119
|
-
when "subscribe"
|
|
120
|
-
channel.handle_subscribe
|
|
121
|
-
!channel.rejected?
|
|
122
|
-
when "unsubscribe"
|
|
123
|
-
conn.subscriptions.remove_subscription(channel)
|
|
124
|
-
true
|
|
125
|
-
when "message"
|
|
126
|
-
channel.perform_action ActiveSupport::JSON.decode(data)
|
|
127
|
-
true
|
|
128
|
-
else
|
|
129
|
-
false
|
|
130
|
-
end
|
|
161
|
+
conn.subscriptions.execute_rpc_command({"command" => command, "identifier" => identifier, "data" => data})
|
|
131
162
|
end
|
|
132
|
-
# Support rescue_from
|
|
133
|
-
# https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
|
|
134
163
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
135
164
|
rescue_with_handler(e) || raise
|
|
136
165
|
false
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "action_cable"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
ActionCable::Server::Socket
|
|
7
|
+
rescue
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
if defined?(ActionCable::Server::Socket)
|
|
11
|
+
require "anycable/rails/next/connection"
|
|
12
|
+
require "anycable/rails/next/action_cable_ext/connection"
|
|
13
|
+
require "anycable/rails/next/action_cable_ext/channel"
|
|
14
|
+
else
|
|
15
|
+
require "anycable/rails/connection"
|
|
16
|
+
|
|
17
|
+
require "anycable/rails/action_cable_ext/connection"
|
|
18
|
+
require "anycable/rails/action_cable_ext/channel"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require "anycable/rails/action_cable_ext/remote_connections"
|
|
22
|
+
require "anycable/rails/action_cable_ext/broadcast_options"
|
|
4
23
|
|
|
5
24
|
module AnyCable
|
|
6
25
|
module Rails
|
|
@@ -6,6 +6,20 @@ module AnyCable
|
|
|
6
6
|
module SerializableIdentification
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
|
+
module ConnectionGID
|
|
10
|
+
def connection_identifier
|
|
11
|
+
unless defined? @connection_identifier
|
|
12
|
+
@connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get(:"@#{id}") || __send__(id) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@connection_identifier
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
prepend ConnectionGID
|
|
21
|
+
end
|
|
22
|
+
|
|
9
23
|
class_methods do
|
|
10
24
|
def identified_by(*identifiers)
|
|
11
25
|
super
|
|
@@ -32,6 +46,11 @@ module AnyCable
|
|
|
32
46
|
identifiers_hash.to_json
|
|
33
47
|
end
|
|
34
48
|
|
|
49
|
+
def identifiers_json=(val)
|
|
50
|
+
@cached_ids = {}
|
|
51
|
+
@serialized_ids = val ? ActiveSupport::JSON.decode(val) : {}
|
|
52
|
+
end
|
|
53
|
+
|
|
35
54
|
# Fetch identifier and deserialize if neccessary
|
|
36
55
|
def fetch_identifier(name)
|
|
37
56
|
return unless @cached_ids
|
|
@@ -46,9 +46,9 @@ module AnyCable
|
|
|
46
46
|
rack_session.respond_to?(name, include_private) || super
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def method_missing(method,
|
|
49
|
+
def method_missing(method, ...)
|
|
50
50
|
if rack_session.respond_to?(method, true)
|
|
51
|
-
rack_session.send(method,
|
|
51
|
+
rack_session.send(method, ...)
|
|
52
52
|
else
|
|
53
53
|
super
|
|
54
54
|
end
|
|
@@ -10,7 +10,7 @@ module AnyCable
|
|
|
10
10
|
super
|
|
11
11
|
rescue AnyCable::JWT::ExpiredSignature
|
|
12
12
|
logger.error "An expired JWT token was rejected"
|
|
13
|
-
close(reason: "token_expired", reconnect: false)
|
|
13
|
+
close(reason: "token_expired", reconnect: false)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def anycable_jwt_present?
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_cable"
|
|
4
|
+
|
|
5
|
+
ActionCable::Connection::Base.include(Module.new do
|
|
6
|
+
# This method is assumed to be overriden in the connection class to enable public
|
|
7
|
+
# streams
|
|
8
|
+
def allow_public_streams?
|
|
9
|
+
false
|
|
10
|
+
end
|
|
11
|
+
end)
|
|
12
|
+
|
|
13
|
+
# Handle $pubsub channel in Subscriptions
|
|
14
|
+
ActionCable::Connection::Subscriptions.prepend(Module.new do
|
|
15
|
+
def subscription_from_identifier(id_key)
|
|
16
|
+
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
|
17
|
+
return super unless id_options[:channel] == "$pubsub"
|
|
18
|
+
|
|
19
|
+
AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
|
|
20
|
+
end
|
|
21
|
+
end)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_cable"
|
|
4
|
+
|
|
5
|
+
ActionCable::Channel::Base.prepend(Module.new do
|
|
6
|
+
attr_accessor :whisper_stream
|
|
7
|
+
|
|
8
|
+
def stream_from(broadcasting, _callback = nil, **opts)
|
|
9
|
+
whispering = opts.delete(:whisper)
|
|
10
|
+
whispers_to(broadcasting) if whispering
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def whispers_to(broadcasting)
|
|
15
|
+
logger.debug "#{self.class.name} whispers to #{broadcasting}"
|
|
16
|
+
self.whisper_stream = broadcasting
|
|
17
|
+
end
|
|
18
|
+
end)
|
|
19
|
+
|
|
20
|
+
ActionCable::Connection::Subscriptions.prepend(Module.new do
|
|
21
|
+
def execute_command(data)
|
|
22
|
+
return whisper(data) if data["command"] == "whisper"
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def whisper(data)
|
|
28
|
+
subscription = find(data)
|
|
29
|
+
stream = subscription.whisper_stream
|
|
30
|
+
raise "Whispering stream is not set" unless stream
|
|
31
|
+
|
|
32
|
+
::ActionCable.server.broadcast stream, data["data"]
|
|
33
|
+
end
|
|
34
|
+
end)
|
data/lib/anycable/rails/ext.rb
CHANGED
|
@@ -4,7 +4,10 @@ module AnyCable
|
|
|
4
4
|
module Rails
|
|
5
5
|
module Ext
|
|
6
6
|
autoload :JWT, "anycable/rails/ext/jwt"
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
# These features are included by default
|
|
9
|
+
require "anycable/rails/ext/signed_streams"
|
|
10
|
+
require "anycable/rails/ext/whisper"
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -22,10 +22,10 @@ module AnyCable
|
|
|
22
22
|
tag "meta", name: "action-cable-url", content: url
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def anycable_token_meta_tag(**identifiers)
|
|
26
26
|
token = JWT.encode(identifiers)
|
|
27
27
|
|
|
28
|
-
tag "meta", name: "
|
|
28
|
+
tag "meta", name: "cable-token", content: token
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def signed_stream_name(streamables)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_cable"
|
|
4
|
+
|
|
5
|
+
ActionCable::Channel::Base.prepend(Module.new do
|
|
6
|
+
# Whispering support
|
|
7
|
+
def whispers_to(broadcasting)
|
|
8
|
+
return super unless anycabled?
|
|
9
|
+
|
|
10
|
+
connection.anycable_socket.whisper identifier, broadcasting
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Unsubscribing relies on the channel state (which is not persistent in AnyCable).
|
|
14
|
+
# Thus, we pretend that the stream is registered to make Action Cable do its unsubscribing job.
|
|
15
|
+
def stop_stream_from(broadcasting)
|
|
16
|
+
streams[broadcasting] = true if anycabled?
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# For AnyCable, unsubscribing from all streams is a separate operation,
|
|
21
|
+
# so we use a special constant to indicate it.
|
|
22
|
+
def stop_all_streams
|
|
23
|
+
if anycabled?
|
|
24
|
+
streams.clear
|
|
25
|
+
streams[AnyCable::Rails::Server::PubSub::ALL_STREAMS] = true
|
|
26
|
+
end
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Make rejected status accessible from outside
|
|
31
|
+
def rejected? = subscription_rejected?
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def anycabled? = connection.anycabled?
|
|
36
|
+
end)
|