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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 474cfd55dac8443341c7717845af042cd80889e3d865cdef3a6e41d089f02348
4
- data.tar.gz: e744ee509ebc1ee82cb112150f346970fe52b55dfb134745b927f94c5670198e
3
+ metadata.gz: 6200d2863868ed23ac3bcc19478e4310bc4097bfff5884eb5c427ef9c41ec6be
4
+ data.tar.gz: a33f42cd90e362301dc90f5957b77787cec29e28c3aeee9782d3d81c582301f4
5
5
  SHA512:
6
- metadata.gz: 4092bd45059bbb6062df44dc78b4ba2ff1bf8a2f3ef65448f7baf3886c35b91b4e0feb315e86e92cf746735f945b007d7550bd01c5bdd1a0dc6b32178d1a1c68
7
- data.tar.gz: e63aa37233adf6f31eff249c2b9f1beeb1ea41d7f50152061cb8b3769cefc9be863c380df710dcf6715c4b2070fda26a569f3a2b7d8bca63cecc5fc8c3b0c97f
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
@@ -1,4 +1,4 @@
1
- Copyright 2017-2021 palkan
1
+ Copyright 2017-2022 palkan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/anycable-rails.svg)](https://rubygems.org/gems/anycable-rails)
2
2
  [![Build](https://github.com/anycable/anycable-rails/workflows/Build/badge.svg)](https://github.com/anycable/anycable-rails/actions)
3
- [![Gitter](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/anycable/Lobby)
4
- [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io/#/rails/getting_started)
3
+ [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](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/#/rails/getting_started).
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.5
26
- - Rails >= 5.2
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:3334/cable"
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/#/v1/anycable-go/getting_started):
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=3334
87
+ anycable-go --host=localhost --port=8080
89
88
  ```
90
89
 
91
- See [documentation](https://docs.anycable.io/#/rails/getting_started) for more information on AnyCable + Rails usage.
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/#/rails/compatibility).
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(ActionCable::Connection::SerializableIdentification)
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 connection.anycable_socket
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 connection.anycable_socket
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.socket.istate
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.on_load(:action_cable) do
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
- ::ActionCable::Channel::Base.include(AnyCable::Rails::ChannelState)
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