anycable-rails 1.0.7 → 1.3.4
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 +82 -0
- data/MIT-LICENSE +1 -1
- data/README.md +11 -12
- data/lib/anycable/rails/action_cable_ext/channel.rb +49 -0
- data/lib/anycable/rails/action_cable_ext/connection.rb +90 -0
- data/lib/anycable/rails/{actioncable → action_cable_ext}/remote_connections.rb +3 -1
- data/lib/anycable/rails/channel_state.rb +60 -5
- data/lib/anycable/rails/config.rb +6 -1
- data/lib/anycable/rails/connection.rb +211 -0
- data/lib/anycable/rails/connection_factory.rb +44 -0
- data/lib/anycable/rails/connections/persistent_session.rb +40 -0
- data/lib/anycable/rails/connections/serializable_identification.rb +46 -0
- data/lib/anycable/rails/connections/session_proxy.rb +81 -0
- data/lib/anycable/rails/middlewares/log_tagging.rb +2 -2
- data/lib/anycable/rails/railtie.rb +22 -13
- data/lib/anycable/rails/version.rb +1 -1
- data/lib/anycable/rails.rb +11 -0
- data/lib/generators/anycable/setup/setup_generator.rb +21 -5
- data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +1 -1
- metadata +22 -35
- data/lib/anycable/rails/actioncable/channel.rb +0 -40
- data/lib/anycable/rails/actioncable/connection/persistent_session.rb +0 -34
- data/lib/anycable/rails/actioncable/connection/serializable_identification.rb +0 -42
- data/lib/anycable/rails/actioncable/connection.rb +0 -222
- data/lib/anycable/rails/actioncable/testing.rb +0 -33
- data/lib/anycable/rails/refinements/subscriptions.rb +0 -20
- data/lib/anycable/rails/session_proxy.rb +0 -79
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6200d2863868ed23ac3bcc19478e4310bc4097bfff5884eb5c427ef9c41ec6be
|
4
|
+
data.tar.gz: a33f42cd90e362301dc90f5957b77787cec29e28c3aeee9782d3d81c582301f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f89b8b32dca31e58df2ad57f926b6ef7f2619dd5286d630e393cd7e306fd247cba46e4aa1ce76304bb75486ba52127b0d819dae10093e0a835a5e11c9b02ece
|
7
|
+
data.tar.gz: 5fc0c51cb4eb86c5c8bbbbae0c896af8440ec765370ff0c4a69aaa146e3d2465f1b28993f8f213cd7f8d956d684251aecb7eeda8951db14b0dbbc4c035b45507
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,88 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 1.3.4 (2022-06-28)
|
6
|
+
|
7
|
+
- Add support and backport for Connection command callbacks. ([@palkan][])
|
8
|
+
|
9
|
+
## 1.3.3 (2022-04-20)
|
10
|
+
|
11
|
+
- Added `sid` (unique connection identifier) field to the `welcome` message if present. ([@palkan][])
|
12
|
+
|
13
|
+
- Fixed handling Ruby Logger incompatible loggers. ([@palkan][])
|
14
|
+
|
15
|
+
## 1.3.2 (2022-03-04)
|
16
|
+
|
17
|
+
- Allow Ruby 2.6.
|
18
|
+
|
19
|
+
## 1.3.1 (2022-02-28)
|
20
|
+
|
21
|
+
- Fix Action Cable Channel patch to not change methods signatures. ([@palkan][])
|
22
|
+
|
23
|
+
Otherwise it could lead to conflicts with other patches.
|
24
|
+
|
25
|
+
## 1.3.0 (2022-02-21)
|
26
|
+
|
27
|
+
- Introduce `AnyCable::Rails.extend_adapter!` to make any pubsub adapter AnyCable-compatible. ([@palkan][])
|
28
|
+
|
29
|
+
- Refactored Action Cable patching to preserve original functionality and avoid monkey-patching collisions. ([@palkan][])
|
30
|
+
|
31
|
+
## 1.2.1 (2022-01-31)
|
32
|
+
|
33
|
+
- Add a temporary fix to be compatible with `sentry-rails`. ([@palkan][])
|
34
|
+
|
35
|
+
See [#165](https://github.com/anycable/anycable-rails/issues/165).
|
36
|
+
|
37
|
+
- Run embedded RPC server only if `any_cable` adapter is used for Action Cable. ([@palkan][])
|
38
|
+
|
39
|
+
## 1.2.0 (2021-12-21) 🎄
|
40
|
+
|
41
|
+
- Drop Rails 5 support.
|
42
|
+
|
43
|
+
- Drop Ruby 2.6 support.
|
44
|
+
|
45
|
+
## 1.1.4 (2021-11-11)
|
46
|
+
|
47
|
+
- Added `Connection#state_attr_accessor`. ([@palkan][])
|
48
|
+
|
49
|
+
## 1.1.3 (2021-10-11)
|
50
|
+
|
51
|
+
- Relax Action Cable dependency. ([@palkan][])
|
52
|
+
|
53
|
+
Action Cable 5.1 is allowed (though not recommended).
|
54
|
+
|
55
|
+
## 1.1.2 (2021-06-23)
|
56
|
+
|
57
|
+
- Bring back dependency on `anycable` (instead of `anycable-core`). ([@palkan][])
|
58
|
+
|
59
|
+
Make it easier to get started by adding just a single gem.
|
60
|
+
|
61
|
+
## 1.1.1 (2021-06-08)
|
62
|
+
|
63
|
+
- Updated documentation links in the generator. ([@palkan][])
|
64
|
+
|
65
|
+
## 1.1.0 🚸 (2021-06-01)
|
66
|
+
|
67
|
+
- No changes since 1.1.0.rc1.1.
|
68
|
+
|
69
|
+
## 1.1.0.rc1.1 (2021-05-12)
|
70
|
+
|
71
|
+
- Fixed config loading regression introduced in 1.1.0.rc1.
|
72
|
+
|
73
|
+
## 1.1.0.rc1 (2021-05-12)
|
74
|
+
|
75
|
+
- Adding `anycable` or `grpc` gem as an explicit dependency is required.
|
76
|
+
|
77
|
+
Now, `anycable-rails` depends on `anycable-core`, which doesn't include gRPC server implementation.
|
78
|
+
You should either add `anycable` or `grpc` (>= 1.37) gem as an explicit dependency.
|
79
|
+
|
80
|
+
- Add option to embed AnyCable RPC into a Rails server process. ([@palkan][])
|
81
|
+
|
82
|
+
Set `embedded: true` in the configuration to launch RPC along with `rails s` (only for Rails 6.1+).
|
83
|
+
|
84
|
+
- **Ruby >= 2.6** is required.
|
85
|
+
- **Rails >= 6.0** is required.
|
86
|
+
|
5
87
|
## 1.0.7 (2021-03-05)
|
6
88
|
|
7
89
|
- Ruby 3 compatibility. ([@palkan][])
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
[](https://rubygems.org/gems/anycable-rails)
|
2
2
|
[](https://github.com/anycable/anycable-rails/actions)
|
3
|
-
[](https://docs.anycable.io/#/rails/getting_started)
|
3
|
+
[](https://docs.anycable.io/rails/getting_started)
|
5
4
|
|
6
5
|
# AnyCable Rails
|
7
6
|
|
@@ -11,19 +10,19 @@ With AnyCable you can use channels, client-side JS, broadcasting - (almost) all
|
|
11
10
|
|
12
11
|
You can even use Action Cable in development and not be afraid of [compatibility issues](#compatibility).
|
13
12
|
|
14
|
-
**Important** This is a readme for the upcoming v1.0 release. For v0.6.x see the readme from the [0-6-stable](https://github.com/anycable/anycable-rails/tree/0-6-stable) branch.
|
15
|
-
|
16
13
|
💾 [Example Application](https://github.com/anycable/anycable_rails_demo)
|
17
14
|
|
18
|
-
📑 [Documentation](https://docs.anycable.io
|
15
|
+
📑 [Documentation](https://docs.anycable.io/rails/getting_started).
|
16
|
+
|
17
|
+
> [AnyCable Pro](https://docs.anycable.io/pro) has been launched 🚀
|
19
18
|
|
20
19
|
<a href="https://evilmartians.com/">
|
21
20
|
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
22
21
|
|
23
22
|
## Requirements
|
24
23
|
|
25
|
-
- Ruby >= 2.
|
26
|
-
- Rails >= 5.
|
24
|
+
- Ruby >= 2.6
|
25
|
+
- Rails >= 6.0 (Rails 5.1 could work but we're no longer enforce compatibility on CI)
|
27
26
|
- Redis (see [other options](https://github.com/anycable/anycable/issues/2) for broadcasting)
|
28
27
|
|
29
28
|
## Usage
|
@@ -64,7 +63,7 @@ and specify AnyCable WebSocket server URL:
|
|
64
63
|
# For development it's likely the localhost
|
65
64
|
|
66
65
|
# config/environments/development.rb
|
67
|
-
config.action_cable.url = "ws://localhost:
|
66
|
+
config.action_cable.url = "ws://localhost:8080/cable"
|
68
67
|
|
69
68
|
# For production it's likely to have a sub-domain and secure connection
|
70
69
|
|
@@ -82,17 +81,17 @@ $ bundle exec anycable
|
|
82
81
|
$ RAILS_ENV=production bundle exec anycable
|
83
82
|
```
|
84
83
|
|
85
|
-
And, finally, run AnyCable WebSocket server, e.g. [anycable-go](https://docs.anycable.io
|
84
|
+
And, finally, run AnyCable WebSocket server, e.g. [anycable-go](https://docs.anycable.io/anycable-go/getting_started):
|
86
85
|
|
87
86
|
```sh
|
88
|
-
anycable-go --host=localhost --port=
|
87
|
+
anycable-go --host=localhost --port=8080
|
89
88
|
```
|
90
89
|
|
91
|
-
See [documentation](https://docs.anycable.io
|
90
|
+
See [documentation](https://docs.anycable.io/rails/getting_started) for more information on AnyCable + Rails usage.
|
92
91
|
|
93
92
|
## Action Cable Compatibility
|
94
93
|
|
95
|
-
See [documentation](https://docs.anycable.io
|
94
|
+
See [documentation](https://docs.anycable.io/rails/compatibility).
|
96
95
|
|
97
96
|
## Contributing
|
98
97
|
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable"
|
4
|
+
|
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
|
+
def start_periodic_timers
|
18
|
+
super unless anycabled?
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop_periodic_timers
|
22
|
+
super unless anycabled?
|
23
|
+
end
|
24
|
+
|
25
|
+
def stream_from(broadcasting, _callback = nil, **)
|
26
|
+
return super unless anycabled?
|
27
|
+
|
28
|
+
connection.anycable_socket.subscribe identifier, broadcasting
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop_stream_from(broadcasting)
|
32
|
+
return super unless anycabled?
|
33
|
+
|
34
|
+
connection.anycable_socket.unsubscribe identifier, broadcasting
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop_all_streams
|
38
|
+
return super unless anycabled?
|
39
|
+
|
40
|
+
connection.anycable_socket.unsubscribe_from_all identifier
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def anycabled?
|
46
|
+
# Use instance variable check here for testing compatibility
|
47
|
+
connection.instance_variable_defined?(:@anycable_socket)
|
48
|
+
end
|
49
|
+
end)
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable"
|
4
|
+
require "anycable/rails/connections/serializable_identification"
|
5
|
+
|
6
|
+
ActionCable::Connection::Base.include(AnyCable::Rails::Connections::SerializableIdentification)
|
7
|
+
ActionCable::Connection::Base.prepend(Module.new do
|
8
|
+
attr_reader :anycable_socket
|
9
|
+
attr_accessor :anycable_request_builder
|
10
|
+
|
11
|
+
# In AnyCable, we lazily populate env by passing it through the middleware chain,
|
12
|
+
# so we access it via #request
|
13
|
+
def env
|
14
|
+
return super unless anycabled?
|
15
|
+
|
16
|
+
request.env
|
17
|
+
end
|
18
|
+
|
19
|
+
def anycabled?
|
20
|
+
@anycable_socket
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def request
|
26
|
+
return super unless anycabled?
|
27
|
+
|
28
|
+
@request ||= anycable_request_builder.build_rack_request(@env)
|
29
|
+
end
|
30
|
+
end)
|
31
|
+
|
32
|
+
# Backport command callbacks: https://github.com/rails/rails/pull/44696
|
33
|
+
unless ActionCable::Connection::Base.respond_to?(:before_command)
|
34
|
+
ActionCable::Connection::Base.include ActiveSupport::Callbacks
|
35
|
+
ActionCable::Connection::Base.define_callbacks :command
|
36
|
+
ActionCable::Connection::Base.extend(Module.new do
|
37
|
+
def before_command(*methods, &block)
|
38
|
+
set_callback(:command, :before, *methods, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_command(*methods, &block)
|
42
|
+
set_callback(:command, :after, *methods, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def around_command(*methods, &block)
|
46
|
+
set_callback(:command, :around, *methods, &block)
|
47
|
+
end
|
48
|
+
end)
|
49
|
+
|
50
|
+
ActionCable::Connection::Base.prepend(Module.new do
|
51
|
+
def dispatch_websocket_message(websocket_message)
|
52
|
+
return super unless websocket.alive?
|
53
|
+
|
54
|
+
handle_channel_command(decode(websocket_message))
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_channel_command(payload)
|
58
|
+
run_callbacks :command do
|
59
|
+
subscriptions.execute_command payload
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Trigger autoload
|
66
|
+
test_case_defined = false
|
67
|
+
|
68
|
+
begin
|
69
|
+
ActionCable::Connection::TestCase # rubocop:disable Lint/Void
|
70
|
+
test_case_defined = true
|
71
|
+
rescue NameError
|
72
|
+
end
|
73
|
+
|
74
|
+
# Backport: https://github.com/rails/rails/pull/45445
|
75
|
+
if test_case_defined && !ActionCable::Connection::TestConnection.method_defined?(:transmissions)
|
76
|
+
ActionCable::Connection::TestConnection.prepend(Module.new do
|
77
|
+
attr_reader :transmissions
|
78
|
+
|
79
|
+
def initialize(*)
|
80
|
+
super
|
81
|
+
|
82
|
+
@transmissions = []
|
83
|
+
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def transmit(cable_message)
|
87
|
+
transmissions << cable_message.with_indifferent_access
|
88
|
+
end
|
89
|
+
end)
|
90
|
+
end
|
@@ -2,10 +2,12 @@
|
|
2
2
|
|
3
3
|
require "action_cable/remote_connections"
|
4
4
|
|
5
|
-
ActionCable::RemoteConnections::RemoteConnection.include(
|
5
|
+
ActionCable::RemoteConnections::RemoteConnection.include(AnyCable::Rails::Connections::SerializableIdentification)
|
6
6
|
|
7
7
|
ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
|
8
8
|
def disconnect(reconnect: true)
|
9
|
+
# Legacy Action Cable functionality if case we're not fully migrated yet
|
10
|
+
super() unless AnyCable::Rails.enabled?
|
9
11
|
::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
|
10
12
|
end
|
11
13
|
end)
|
@@ -10,11 +10,11 @@ module AnyCable
|
|
10
10
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
11
11
|
def #{name}
|
12
12
|
return @#{name} if instance_variable_defined?(:@#{name})
|
13
|
-
@#{name} = AnyCable::Rails.deserialize(__istate__["#{name}"], json: true) if
|
13
|
+
@#{name} = AnyCable::Rails.deserialize(__istate__["#{name}"], json: true) if anycabled?
|
14
14
|
end
|
15
15
|
|
16
16
|
def #{name}=(val)
|
17
|
-
__istate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if
|
17
|
+
__istate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
|
18
18
|
instance_variable_set(:@#{name}, val)
|
19
19
|
end
|
20
20
|
RUBY
|
@@ -41,13 +41,68 @@ module AnyCable
|
|
41
41
|
attr_writer :__istate__
|
42
42
|
|
43
43
|
def __istate__
|
44
|
-
@__istate__ ||= connection.
|
44
|
+
@__istate__ ||= connection.anycable_socket.istate
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module ConnectionState
|
49
|
+
module ClassMethods
|
50
|
+
def state_attr_accessor(*names)
|
51
|
+
names.each do |name|
|
52
|
+
connection_state_attributes << name
|
53
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
54
|
+
def #{name}
|
55
|
+
return @#{name} if instance_variable_defined?(:@#{name})
|
56
|
+
@#{name} = AnyCable::Rails.deserialize(__cstate__["#{name}"], json: true) if anycabled?
|
57
|
+
end
|
58
|
+
|
59
|
+
def #{name}=(val)
|
60
|
+
__cstate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
|
61
|
+
instance_variable_set(:@#{name}, val)
|
62
|
+
end
|
63
|
+
RUBY
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def connection_state_attributes
|
68
|
+
return @connection_state_attributes if instance_variable_defined?(:@connection_state_attributes)
|
69
|
+
|
70
|
+
@connection_state_attributes =
|
71
|
+
if superclass.respond_to?(:connection_state_attributes)
|
72
|
+
superclass.connection_state_attributes.dup
|
73
|
+
else
|
74
|
+
[]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.included(base)
|
80
|
+
base.extend ClassMethods
|
81
|
+
end
|
82
|
+
|
83
|
+
# Make it possible to provide istate explicitly for a connection instance
|
84
|
+
attr_writer :__cstate__
|
85
|
+
|
86
|
+
def __cstate__
|
87
|
+
@__cstate__ ||= anycable_socket.cstate
|
45
88
|
end
|
46
89
|
end
|
47
90
|
end
|
48
91
|
end
|
49
92
|
|
50
|
-
ActiveSupport
|
93
|
+
if ActiveSupport::VERSION::MAJOR < 6
|
94
|
+
# `state_attr_accessor` must be available in Action Cable
|
95
|
+
ActiveSupport.on_load(:action_cable) do
|
96
|
+
::ActionCable::Connection::Base.include(AnyCable::Rails::ConnectionState)
|
97
|
+
::ActionCable::Channel::Base.include(AnyCable::Rails::ChannelState)
|
98
|
+
end
|
99
|
+
else
|
51
100
|
# `state_attr_accessor` must be available in Action Cable
|
52
|
-
|
101
|
+
ActiveSupport.on_load(:action_cable_connection) do
|
102
|
+
::ActionCable::Connection::Base.include(AnyCable::Rails::ConnectionState)
|
103
|
+
end
|
104
|
+
|
105
|
+
ActiveSupport.on_load(:action_cable_channel) do
|
106
|
+
::ActionCable::Channel::Base.include(AnyCable::Rails::ChannelState)
|
107
|
+
end
|
53
108
|
end
|
@@ -1,12 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "anycable/config"
|
4
|
+
# Make sure Rails extensions for Anyway Config are loaded
|
5
|
+
# See https://github.com/anycable/anycable-rails/issues/63
|
6
|
+
require "anyway/rails"
|
4
7
|
|
5
8
|
# Extend AnyCable configuration with:
|
6
9
|
# - `access_logs_disabled` (defaults to true) — whether to print Started/Finished logs
|
7
10
|
# - `persistent_session_enabled` (defaults to false) — whether to store session changes in the connection state
|
11
|
+
# - `embedded` (defaults to false) — whether to run RPC server inside a Rails server process
|
8
12
|
AnyCable::Config.attr_config(
|
9
13
|
access_logs_disabled: true,
|
10
|
-
persistent_session_enabled: false
|
14
|
+
persistent_session_enabled: false,
|
15
|
+
embedded: false
|
11
16
|
)
|
12
17
|
AnyCable::Config.ignore_options :access_logs_disabled, :persistent_session_enabled
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rails
|
7
|
+
# Enhance Action Cable connection
|
8
|
+
using(Module.new do
|
9
|
+
refine ActionCable::Connection::Base do
|
10
|
+
attr_writer :env, :websocket, :logger, :coder,
|
11
|
+
:subscriptions, :serialized_ids, :cached_ids, :server,
|
12
|
+
:anycable_socket
|
13
|
+
|
14
|
+
# Using public :send_welcome_message causes stack level too deep 🤷🏻♂️
|
15
|
+
def send_welcome_message
|
16
|
+
transmit({
|
17
|
+
type: ActionCable::INTERNAL[:message_types][:welcome],
|
18
|
+
sid: env["anycable.sid"]
|
19
|
+
}.compact)
|
20
|
+
end
|
21
|
+
|
22
|
+
def public_request
|
23
|
+
request
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
refine ActionCable::Channel::Base do
|
28
|
+
def rejected?
|
29
|
+
subscription_rejected?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
refine ActionCable::Connection::Subscriptions do
|
34
|
+
# Find or add a subscription to the list
|
35
|
+
def fetch(identifier)
|
36
|
+
add("identifier" => identifier) unless subscriptions[identifier]
|
37
|
+
|
38
|
+
unless subscriptions[identifier]
|
39
|
+
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
|
40
|
+
end
|
41
|
+
|
42
|
+
subscriptions[identifier]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end)
|
46
|
+
|
47
|
+
class Connection
|
48
|
+
# We store logger tags in the connection state to be able
|
49
|
+
# to re-use them in the subsequent calls
|
50
|
+
LOG_TAGS_IDENTIFIER = "__ltags__"
|
51
|
+
|
52
|
+
delegate :identifiers_json, to: :conn
|
53
|
+
|
54
|
+
attr_reader :socket, :logger
|
55
|
+
|
56
|
+
def initialize(connection_class, socket, identifiers: nil, subscriptions: nil)
|
57
|
+
@socket = socket
|
58
|
+
|
59
|
+
logger_tags = fetch_logger_tags_from_state
|
60
|
+
@logger = ActionCable::Connection::TaggedLoggerProxy.new(AnyCable.logger, tags: logger_tags)
|
61
|
+
|
62
|
+
# Instead of calling #initialize,
|
63
|
+
# we allocate an instance and setup all the required components manually
|
64
|
+
@conn = connection_class.allocate
|
65
|
+
# Required to access config (for access origin checks)
|
66
|
+
conn.server = ActionCable.server
|
67
|
+
conn.logger = logger
|
68
|
+
conn.anycable_socket = conn.websocket = socket
|
69
|
+
conn.env = socket.env
|
70
|
+
conn.coder = ActiveSupport::JSON
|
71
|
+
conn.subscriptions = ActionCable::Connection::Subscriptions.new(conn)
|
72
|
+
conn.serialized_ids = {}
|
73
|
+
conn.serialized_ids = ActiveSupport::JSON.decode(identifiers) if identifiers
|
74
|
+
conn.cached_ids = {}
|
75
|
+
conn.anycable_request_builder = self
|
76
|
+
|
77
|
+
return unless subscriptions
|
78
|
+
|
79
|
+
# Pre-initialize channels (for disconnect)
|
80
|
+
subscriptions.each do |id|
|
81
|
+
channel = conn.subscriptions.fetch(id)
|
82
|
+
next unless socket.istate[id]
|
83
|
+
|
84
|
+
channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_open
|
89
|
+
logger.info started_request_message if access_logs?
|
90
|
+
|
91
|
+
verify_origin! || return
|
92
|
+
|
93
|
+
conn.connect if conn.respond_to?(:connect)
|
94
|
+
|
95
|
+
socket.cstate.write(LOG_TAGS_IDENTIFIER, logger.tags.to_json) unless logger.tags.empty?
|
96
|
+
|
97
|
+
conn.send_welcome_message
|
98
|
+
rescue ::ActionCable::Connection::Authorization::UnauthorizedError
|
99
|
+
reject_request(
|
100
|
+
ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_close
|
105
|
+
logger.info finished_request_message if access_logs?
|
106
|
+
|
107
|
+
conn.subscriptions.unsubscribe_from_all
|
108
|
+
conn.disconnect if conn.respond_to?(:disconnect)
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def handle_channel_command(identifier, command, data)
|
113
|
+
conn.run_callbacks :command do
|
114
|
+
# We cannot use subscriptions#execute_command here,
|
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
|
131
|
+
end
|
132
|
+
# Support rescue_from
|
133
|
+
# https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
|
134
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
135
|
+
rescue_with_handler(e) || raise
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_rack_request(env)
|
140
|
+
environment = ::Rails.application.env_config.merge(env) if defined?(::Rails.application) && ::Rails.application
|
141
|
+
AnyCable::Rails::Rack.app.call(environment) if environment
|
142
|
+
|
143
|
+
ActionDispatch::Request.new(environment || env)
|
144
|
+
end
|
145
|
+
|
146
|
+
def action_cable_connection
|
147
|
+
conn
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
attr_reader :conn
|
153
|
+
|
154
|
+
def reject_request(reason, reconnect = false)
|
155
|
+
logger.info finished_request_message("Rejected") if access_logs?
|
156
|
+
conn.close(
|
157
|
+
reason: reason,
|
158
|
+
reconnect: reconnect
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def fetch_logger_tags_from_state
|
163
|
+
socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
|
164
|
+
next [] unless raw_tags
|
165
|
+
ActiveSupport::JSON.decode(raw_tags)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def started_request_message
|
170
|
+
format(
|
171
|
+
'Started "%s"%s for %s at %s',
|
172
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
|
173
|
+
)
|
174
|
+
end
|
175
|
+
|
176
|
+
def finished_request_message(reason = "Closed")
|
177
|
+
format(
|
178
|
+
'Finished "%s"%s for %s at %s (%s)',
|
179
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
def verify_origin!
|
184
|
+
return true unless socket.env.key?("HTTP_ORIGIN")
|
185
|
+
|
186
|
+
return true if conn.send(:allow_request_origin?)
|
187
|
+
|
188
|
+
reject_request(
|
189
|
+
ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
|
190
|
+
)
|
191
|
+
false
|
192
|
+
end
|
193
|
+
|
194
|
+
def access_logs?
|
195
|
+
AnyCable.config.access_logs_disabled == false
|
196
|
+
end
|
197
|
+
|
198
|
+
def request
|
199
|
+
conn.public_request
|
200
|
+
end
|
201
|
+
|
202
|
+
def request_loaded?
|
203
|
+
conn.instance_variable_defined?(:@request)
|
204
|
+
end
|
205
|
+
|
206
|
+
def rescue_with_handler(e)
|
207
|
+
conn.rescue_with_handler(e) if conn.respond_to?(:rescue_with_handler)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/rails/connection"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rails
|
7
|
+
class ConnectionFactory
|
8
|
+
def initialize(&block)
|
9
|
+
@mappings = []
|
10
|
+
@use_router = false
|
11
|
+
instance_eval(&block) if block
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(socket, **options)
|
15
|
+
connection_class = use_router? ? resolve_connection_class(socket.env) :
|
16
|
+
ActionCable.server.config.connection_class.call
|
17
|
+
|
18
|
+
AnyCable::Rails::Connection.new(connection_class, socket, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def map(route, &block)
|
22
|
+
raise ArgumentError, "Block is required" unless block
|
23
|
+
|
24
|
+
@use_router = true
|
25
|
+
mappings << [route, block]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :mappings, :use_router
|
31
|
+
alias_method :use_router?, :use_router
|
32
|
+
|
33
|
+
def resolve_connection_class(env)
|
34
|
+
path = env["PATH_INFO"]
|
35
|
+
|
36
|
+
mappings.each do |(prefix, resolver)|
|
37
|
+
return resolver.call if path.starts_with?(prefix)
|
38
|
+
end
|
39
|
+
|
40
|
+
raise "No connection class found matching #{path}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|