anycable-rails 1.0.7 → 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
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