anycable-rails 1.2.1 → 1.3.0

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: d21ee870a0caaeed44d71d571303e0e9ab835457360ee1b205279f38379716e2
4
- data.tar.gz: 0e1f4c986782474601d40228ae272ab795bd038ad837420b9a689f74c91edfcf
3
+ metadata.gz: 37be6474db2d3efc3493ec6fdf5b3826fbdfd84f9c4bc5f4574f1e0497457ec1
4
+ data.tar.gz: bafe87b642e1aaf15c5b09650ed271081d0ba53738507077ed1075b2de5e0ef2
5
5
  SHA512:
6
- metadata.gz: 7cd248fd53e2f09632a9fec2e3439c788f70b96b40270480b0b519ac70738740df783afc7bf7d107bf718dfe08cf31e8880796beace87cc87b20b5aa69cf90e2
7
- data.tar.gz: 6c8e6cedcdd61b58e0dcfd82dfa72a4f3f1e0591bb90e27ef7b8b5c10bbe51a37163f83b12daba720f64f9519cbeadb9fbcb24e9d9af5ec84e988faa1cc54ad0
6
+ metadata.gz: b656f5a17009a19aacef7c1aac587299b5bdca24079e3e6a1ce2c9e652e26a7d65f08e34f0022e1ed4a9e42d8783af9d7d9e85a9059577ec224a523b7c12378e
7
+ data.tar.gz: 383c128d5404a7ed42bba43a426cb68bd10be23607557bce7a8ef46cd1ea1cf2777c918e44a4c71f307a0f973ff863aef36f547290ce821d65251e7f9a62a3fd
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.3.0 (2021-02-21)
6
+
7
+ - Introduce `AnyCable::Rails.extend_adapter!` to make any pubsub adapter AnyCable-compatible. ([@palkan][])
8
+
9
+ - Refactored Action Cable patching to preserve original functionality and avoid monkey-patching collisions. ([@palkan][])
10
+
5
11
  ## 1.2.1 (2021-01-31)
6
12
 
7
13
  - Add a temporary fix to be compatible with `sentry-rails`. ([@palkan][])
data/README.md CHANGED
@@ -1,6 +1,5 @@
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
3
  [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io/rails/getting_started)
5
4
 
6
5
  # AnyCable Rails
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/channel"
4
+
5
+ ActionCable::Channel::Base.prepend(Module.new do
6
+ def subscribe_to_channel(force: false)
7
+ return if anycabled? && !force
8
+ super()
9
+ end
10
+
11
+ def handle_subscribe
12
+ subscribe_to_channel(force: true)
13
+ end
14
+
15
+ def start_periodic_timers
16
+ super unless anycabled?
17
+ end
18
+
19
+ def stop_periodic_timers
20
+ super unless anycabled?
21
+ end
22
+
23
+ def stream_from(broadcasting, _callback = nil, **)
24
+ return super unless anycabled?
25
+
26
+ connection.anycable_socket.subscribe identifier, broadcasting
27
+ end
28
+
29
+ def stop_stream_from(broadcasting)
30
+ return super unless anycabled?
31
+
32
+ connection.anycable_socket.unsubscribe identifier, broadcasting
33
+ end
34
+
35
+ def stop_all_streams
36
+ return super unless anycabled?
37
+
38
+ connection.anycable_socket.unsubscribe_from_all identifier
39
+ end
40
+
41
+ private
42
+
43
+ def anycabled?
44
+ # Use instance variable check here for testing compatibility
45
+ connection.instance_variable_defined?(:@anycable_socket)
46
+ end
47
+ end)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/connection"
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)
@@ -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,7 +41,7 @@ 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
45
  end
46
46
  end
47
47
 
@@ -53,11 +53,11 @@ module AnyCable
53
53
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
54
54
  def #{name}
55
55
  return @#{name} if instance_variable_defined?(:@#{name})
56
- @#{name} = AnyCable::Rails.deserialize(__cstate__["#{name}"], json: true) if anycable_socket
56
+ @#{name} = AnyCable::Rails.deserialize(__cstate__["#{name}"], json: true) if anycabled?
57
57
  end
58
58
 
59
59
  def #{name}=(val)
60
- __cstate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycable_socket
60
+ __cstate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
61
61
  instance_variable_set(:@#{name}, val)
62
62
  end
63
63
  RUBY
@@ -84,7 +84,7 @@ module AnyCable
84
84
  attr_writer :__cstate__
85
85
 
86
86
  def __cstate__
87
- @__cstate__ ||= socket.cstate
87
+ @__cstate__ ||= anycable_socket.cstate
88
88
  end
89
89
  end
90
90
  end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/connection"
4
+ require "action_cable/channel"
5
+
6
+ module AnyCable
7
+ module Rails
8
+ # Enhance Action Cable connection
9
+ using(Module.new do
10
+ refine ActionCable::Connection::Base do
11
+ attr_writer :env, :websocket, :logger, :coder,
12
+ :subscriptions, :serialized_ids, :cached_ids, :server,
13
+ :anycable_socket
14
+
15
+ # Using public :send_welcome_message causes stack level too deep 🤷🏻‍♂️
16
+ def public_send_welcome_message
17
+ send_welcome_message
18
+ end
19
+
20
+ def public_request
21
+ request
22
+ end
23
+ end
24
+
25
+ refine ActionCable::Channel::Base do
26
+ def rejected?
27
+ subscription_rejected?
28
+ end
29
+ end
30
+
31
+ refine ActionCable::Connection::Subscriptions do
32
+ # Find or add a subscription to the list
33
+ def fetch(identifier)
34
+ add("identifier" => identifier) unless subscriptions[identifier]
35
+
36
+ unless subscriptions[identifier]
37
+ raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
38
+ end
39
+
40
+ subscriptions[identifier]
41
+ end
42
+ end
43
+ end)
44
+
45
+ class Connection
46
+ # We store logger tags in the connection state to be able
47
+ # to re-use them in the subsequent calls
48
+ LOG_TAGS_IDENTIFIER = "__ltags__"
49
+
50
+ delegate :identifiers_json, to: :conn
51
+
52
+ attr_reader :socket, :logger
53
+
54
+ def initialize(connection_class, socket, identifiers: nil, subscriptions: nil)
55
+ @socket = socket
56
+
57
+ logger_tags = fetch_logger_tags_from_state
58
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(AnyCable.logger, tags: logger_tags)
59
+
60
+ # Instead of calling #initialize,
61
+ # we allocate an instance and setup all the required components manually
62
+ @conn = connection_class.allocate
63
+ # Required to access config (for access origin checks)
64
+ conn.server = ActionCable.server
65
+ conn.logger = logger
66
+ conn.anycable_socket = conn.websocket = socket
67
+ conn.env = socket.env
68
+ conn.coder = ActiveSupport::JSON
69
+ conn.subscriptions = ActionCable::Connection::Subscriptions.new(conn)
70
+ conn.serialized_ids = {}
71
+ conn.serialized_ids = ActiveSupport::JSON.decode(identifiers) if identifiers
72
+ conn.cached_ids = {}
73
+ conn.anycable_request_builder = self
74
+
75
+ return unless subscriptions
76
+
77
+ # Pre-initialize channels (for disconnect)
78
+ subscriptions.each do |id|
79
+ channel = conn.subscriptions.fetch(id)
80
+ next unless socket.istate[id]
81
+
82
+ channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
83
+ end
84
+ end
85
+
86
+ def handle_open
87
+ logger.info started_request_message if access_logs?
88
+
89
+ verify_origin! || return
90
+
91
+ conn.connect if conn.respond_to?(:connect)
92
+
93
+ socket.cstate.write(LOG_TAGS_IDENTIFIER, logger.tags.to_json) unless logger.tags.empty?
94
+
95
+ conn.public_send_welcome_message
96
+ rescue ::ActionCable::Connection::Authorization::UnauthorizedError
97
+ reject_request(
98
+ ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
99
+ )
100
+ end
101
+
102
+ def handle_close
103
+ logger.info finished_request_message if access_logs?
104
+
105
+ conn.subscriptions.unsubscribe_from_all
106
+ conn.disconnect if conn.respond_to?(:disconnect)
107
+ true
108
+ end
109
+
110
+ def handle_channel_command(identifier, command, data)
111
+ # We cannot use subscriptions#execute_command here,
112
+ # since we MUST return true of false, depending on the status
113
+ # of execution
114
+ channel = conn.subscriptions.fetch(identifier)
115
+ case command
116
+ when "subscribe"
117
+ channel.handle_subscribe
118
+ !channel.rejected?
119
+ when "unsubscribe"
120
+ conn.subscriptions.remove_subscription(channel)
121
+ true
122
+ when "message"
123
+ channel.perform_action ActiveSupport::JSON.decode(data)
124
+ true
125
+ else
126
+ false
127
+ end
128
+ # Support rescue_from
129
+ # https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
130
+ rescue Exception => e # rubocop:disable Lint/RescueException
131
+ rescue_with_handler(e) || raise
132
+ false
133
+ end
134
+
135
+ def build_rack_request(env)
136
+ environment = ::Rails.application.env_config.merge(env) if defined?(::Rails.application) && ::Rails.application
137
+ AnyCable::Rails::Rack.app.call(environment) if environment
138
+
139
+ ActionDispatch::Request.new(environment || env)
140
+ end
141
+
142
+ def action_cable_connection
143
+ conn
144
+ end
145
+
146
+ private
147
+
148
+ attr_reader :conn
149
+
150
+ def reject_request(reason, reconnect = false)
151
+ logger.info finished_request_message("Rejected") if access_logs?
152
+ conn.close(
153
+ reason: reason,
154
+ reconnect: reconnect
155
+ )
156
+ end
157
+
158
+ def fetch_logger_tags_from_state
159
+ socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
160
+ next [] unless raw_tags
161
+ ActiveSupport::JSON.decode(raw_tags)
162
+ end
163
+ end
164
+
165
+ def started_request_message
166
+ format(
167
+ 'Started "%s"%s for %s at %s',
168
+ request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
169
+ )
170
+ end
171
+
172
+ def finished_request_message(reason = "Closed")
173
+ format(
174
+ 'Finished "%s"%s for %s at %s (%s)',
175
+ request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
176
+ )
177
+ end
178
+
179
+ def verify_origin!
180
+ return true unless socket.env.key?("HTTP_ORIGIN")
181
+
182
+ return true if conn.send(:allow_request_origin?)
183
+
184
+ reject_request(
185
+ ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
186
+ )
187
+ false
188
+ end
189
+
190
+ def access_logs?
191
+ AnyCable.config.access_logs_disabled == false
192
+ end
193
+
194
+ def request
195
+ conn.public_request
196
+ end
197
+
198
+ def request_loaded?
199
+ conn.instance_variable_defined?(:@request)
200
+ end
201
+
202
+ def rescue_with_handler(e)
203
+ conn.rescue_with_handler(e) if conn.respond_to?(:rescue_with_handler)
204
+ end
205
+ end
206
+ end
207
+ 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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rails/connections/session_proxy"
4
+
5
+ module AnyCable
6
+ module Rails
7
+ module Connections
8
+ module PersistentSession
9
+ def handle_open
10
+ super.tap { commit_session! }
11
+ end
12
+
13
+ def handle_channel_command(*)
14
+ super.tap { commit_session! }
15
+ end
16
+
17
+ def build_rack_request(env)
18
+ return super unless socket.session
19
+
20
+ super.tap do |req|
21
+ req.env[::Rack::RACK_SESSION] =
22
+ SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def commit_session!
29
+ return unless request_loaded? && request.session.respond_to?(:loaded?) && request.session.loaded?
30
+
31
+ socket.session = request.session.to_json
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ AnyCable::Rails::Connection.prepend(
39
+ AnyCable::Rails::Connections::PersistentSession
40
+ )
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Connections
6
+ module SerializableIdentification
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def identified_by(*identifiers)
11
+ super
12
+ Array(identifiers).each do |identifier|
13
+ define_method(identifier) do
14
+ instance_variable_get(:"@#{identifier}") || fetch_identifier(identifier)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # Generate identifiers info.
21
+ # Converts GlobalID compatible vars to corresponding global IDs params.
22
+ def identifiers_hash
23
+ identifiers.each_with_object({}) do |id, acc|
24
+ obj = instance_variable_get("@#{id}")
25
+ next unless obj
26
+
27
+ acc[id] = AnyCable::Rails.serialize(obj)
28
+ end.compact
29
+ end
30
+
31
+ def identifiers_json
32
+ identifiers_hash.to_json
33
+ end
34
+
35
+ # Fetch identifier and deserialize if neccessary
36
+ def fetch_identifier(name)
37
+ return unless @cached_ids
38
+
39
+ @cached_ids[name] ||= @cached_ids.fetch(name) do
40
+ AnyCable::Rails.deserialize(@serialized_ids[name.to_s])
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Connections
6
+ # Wrap `request.session` to lazily load values provided
7
+ # in the RPC call (set by the previous calls)
8
+ class SessionProxy
9
+ attr_reader :rack_session, :socket_session
10
+
11
+ def initialize(rack_session, socket_session)
12
+ @rack_session = rack_session
13
+ @socket_session = JSON.parse(socket_session).with_indifferent_access
14
+ end
15
+
16
+ %i[has_key? [] []= fetch delete dig].each do |mid|
17
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
18
+ def #{mid}(*args, **kwargs, &block)
19
+ restore_key! args.first
20
+ rack_session.#{mid}(*args, **kwargs, &block)
21
+ end
22
+ CODE
23
+ end
24
+
25
+ alias_method :include?, :has_key?
26
+ alias_method :key?, :has_key?
27
+
28
+ %i[update merge! to_hash].each do |mid|
29
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
30
+ def #{mid}(*args, **kwargs, &block)
31
+ restore!
32
+ rack_session.#{mid}(*args, **kwargs, &block)
33
+ end
34
+ CODE
35
+ end
36
+
37
+ alias_method :to_h, :to_hash
38
+
39
+ def keys
40
+ rack_session.keys + socket_session.keys
41
+ end
42
+
43
+ # Delegate both publuc and private methods to rack_session
44
+ def respond_to_missing?(name, include_private = false)
45
+ return false if name == :marshal_dump || name == :_dump
46
+ rack_session.respond_to?(name, include_private) || super
47
+ end
48
+
49
+ def method_missing(method, *args, &block)
50
+ if rack_session.respond_to?(method, true)
51
+ rack_session.send(method, *args, &block)
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ # This method is used by StimulusReflex to obtain `@by`
58
+ def instance_variable_get(name)
59
+ super || rack_session.instance_variable_get(name)
60
+ end
61
+
62
+ private
63
+
64
+ def restore!
65
+ socket_session.keys.each(&method(:restore_key!))
66
+ end
67
+
68
+ def restore_key!(key)
69
+ return unless socket_session.key?(key)
70
+ val = socket_session.delete(key)
71
+ rack_session[key] =
72
+ if val.is_a?(String)
73
+ GlobalID::Locator.locate(val) || val
74
+ else
75
+ val
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "anycable/rails/action_cable_ext/connection"
4
+ require "anycable/rails/action_cable_ext/channel"
5
+ require "anycable/rails/action_cable_ext/remote_connections"
6
+
3
7
  require "anycable/rails/channel_state"
8
+ require "anycable/rails/connection_factory"
4
9
 
5
10
  module AnyCable
6
11
  module Rails
@@ -43,30 +48,16 @@ module AnyCable
43
48
 
44
49
  initializer "anycable.connection_factory", after: "action_cable.set_configs" do |app|
45
50
  ActiveSupport.on_load(:action_cable) do
46
- # Add AnyCable patch method stub (we use it in ChannelState to distinguish between Action Cable and AnyCable)
47
- # NOTE: Method could be already defined if patch was loaded manually
48
- ActionCable::Connection::Base.attr_reader(:anycable_socket) unless ActionCable::Connection::Base.method_defined?(:anycable_socket)
49
-
50
51
  app.config.to_prepare do
51
- AnyCable.connection_factory = ActionCable.server.config.connection_class.call
52
+ AnyCable.connection_factory = AnyCable::Rails::ConnectionFactory.new
52
53
  end
53
54
 
54
- if AnyCable::Rails.enabled?
55
- require "anycable/rails/actioncable/connection"
56
- if AnyCable.config.persistent_session_enabled
57
- require "anycable/rails/actioncable/connection/persistent_session"
58
- end
55
+ if AnyCable.config.persistent_session_enabled?
56
+ require "anycable/rails/connections/persistent_session"
59
57
  end
60
58
  end
61
59
  end
62
60
 
63
- # Temp hack to fix Sentry vs AnyCable incompatibility
64
- # See https://github.com/anycable/anycable-rails/issues/165
65
- initializer "anycable.sentry_hack", after: :"sentry.extend_action_cable" do
66
- next unless defined?(::Sentry::Rails::ActionCableExtensions::Connection)
67
- Sentry::Rails::ActionCableExtensions::Connection.send :public, :handle_open, :handle_close
68
- end
69
-
70
61
  # Since Rails 6.1
71
62
  if respond_to?(:server)
72
63
  server do
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "1.2.1"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -48,6 +48,17 @@ module AnyCable
48
48
  val
49
49
  end
50
50
  end
51
+
52
+ module Extension
53
+ def broadcast(channel, payload)
54
+ super
55
+ ::AnyCable.broadcast(channel, payload)
56
+ end
57
+ end
58
+
59
+ def extend_adapter!(adapter)
60
+ adapter.extend(Extension)
61
+ end
51
62
  end
52
63
  end
53
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-31 00:00:00.000000000 Z
11
+ date: 2022-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable
@@ -150,12 +150,9 @@ files:
150
150
  - lib/action_cable/subscription_adapter/anycable.rb
151
151
  - lib/anycable-rails.rb
152
152
  - lib/anycable/rails.rb
153
- - lib/anycable/rails/actioncable/channel.rb
154
- - lib/anycable/rails/actioncable/connection.rb
155
- - lib/anycable/rails/actioncable/connection/persistent_session.rb
156
- - lib/anycable/rails/actioncable/connection/serializable_identification.rb
157
- - lib/anycable/rails/actioncable/remote_connections.rb
158
- - lib/anycable/rails/actioncable/testing.rb
153
+ - lib/anycable/rails/action_cable_ext/channel.rb
154
+ - lib/anycable/rails/action_cable_ext/connection.rb
155
+ - lib/anycable/rails/action_cable_ext/remote_connections.rb
159
156
  - lib/anycable/rails/channel_state.rb
160
157
  - lib/anycable/rails/compatibility.rb
161
158
  - lib/anycable/rails/compatibility/rubocop.rb
@@ -164,12 +161,15 @@ files:
164
161
  - lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb
165
162
  - lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb
166
163
  - lib/anycable/rails/config.rb
164
+ - lib/anycable/rails/connection.rb
165
+ - lib/anycable/rails/connection_factory.rb
166
+ - lib/anycable/rails/connections/persistent_session.rb
167
+ - lib/anycable/rails/connections/serializable_identification.rb
168
+ - lib/anycable/rails/connections/session_proxy.rb
167
169
  - lib/anycable/rails/middlewares/executor.rb
168
170
  - lib/anycable/rails/middlewares/log_tagging.rb
169
171
  - lib/anycable/rails/rack.rb
170
172
  - lib/anycable/rails/railtie.rb
171
- - lib/anycable/rails/refinements/subscriptions.rb
172
- - lib/anycable/rails/session_proxy.rb
173
173
  - lib/anycable/rails/version.rb
174
174
  - lib/generators/anycable/download/USAGE
175
175
  - lib/generators/anycable/download/download_generator.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_cable/channel"
4
-
5
- module ActionCable
6
- module Channel
7
- class Base # :nodoc:
8
- alias_method :handle_subscribe, :subscribe_to_channel
9
-
10
- public :handle_subscribe, :subscription_rejected?
11
-
12
- # Action Cable calls this method from inside the Subscriptions#add
13
- # method, which we use to initialize the channel instance.
14
- # We don't want to invoke `subscribed` callbacks every time we do that.
15
- def subscribe_to_channel
16
- # noop
17
- end
18
-
19
- def start_periodic_timers
20
- # noop
21
- end
22
-
23
- def stop_periodic_timers
24
- # noop
25
- end
26
-
27
- def stream_from(broadcasting, _callback = nil, _options = {})
28
- connection.socket.subscribe identifier, broadcasting
29
- end
30
-
31
- def stop_stream_from(broadcasting)
32
- connection.socket.unsubscribe identifier, broadcasting
33
- end
34
-
35
- def stop_all_streams
36
- connection.socket.unsubscribe_from_all identifier
37
- end
38
- end
39
- end
40
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionCable
4
- module Connection
5
- module PersistentSession
6
- def handle_open
7
- super.tap { commit_session! }
8
- end
9
-
10
- def handle_channel_command(*)
11
- super.tap { commit_session! }
12
- end
13
-
14
- def build_rack_request
15
- return super unless socket.session
16
-
17
- super.tap do |req|
18
- req.env[::Rack::RACK_SESSION] =
19
- AnyCable::Rails::SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
20
- end
21
- end
22
-
23
- def commit_session!
24
- return unless request_loaded? && request.session.respond_to?(:loaded?) && request.session.loaded?
25
-
26
- socket.session = request.session.to_json
27
- end
28
- end
29
- end
30
- end
31
-
32
- ::ActionCable::Connection::Base.prepend(
33
- ::ActionCable::Connection::PersistentSession
34
- )
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionCable
4
- module Connection
5
- module SerializableIdentification
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- def identified_by(*identifiers)
10
- super
11
- Array(identifiers).each do |identifier|
12
- define_method(identifier) do
13
- instance_variable_get(:"@#{identifier}") || fetch_identifier(identifier)
14
- end
15
- end
16
- end
17
- end
18
-
19
- # Generate identifiers info.
20
- # Converts GlobalID compatible vars to corresponding global IDs params.
21
- def identifiers_hash
22
- identifiers.each_with_object({}) do |id, acc|
23
- obj = instance_variable_get("@#{id}")
24
- next unless obj
25
-
26
- acc[id] = AnyCable::Rails.serialize(obj)
27
- end.compact
28
- end
29
-
30
- def identifiers_json
31
- identifiers_hash.to_json
32
- end
33
-
34
- # Fetch identifier and deserialize if neccessary
35
- def fetch_identifier(name)
36
- @cached_ids[name] ||= @cached_ids.fetch(name) do
37
- AnyCable::Rails.deserialize(ids[name.to_s])
38
- end
39
- end
40
- end
41
- end
42
- end
@@ -1,220 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_cable/connection"
4
- require "anycable/rails/actioncable/connection/serializable_identification"
5
- require "anycable/rails/refinements/subscriptions"
6
- require "anycable/rails/actioncable/channel"
7
- require "anycable/rails/actioncable/remote_connections"
8
- require "anycable/rails/session_proxy"
9
-
10
- module ActionCable
11
- module Connection
12
- class Base # :nodoc:
13
- # We store logger tags in the connection state to be able
14
- # to re-use them in the subsequent calls
15
- LOG_TAGS_IDENTIFIER = "__ltags__"
16
-
17
- using AnyCable::Refinements::Subscriptions
18
-
19
- include SerializableIdentification
20
-
21
- attr_reader :socket
22
-
23
- alias_method :anycable_socket, :socket
24
-
25
- delegate :env, :session, to: :request
26
-
27
- class << self
28
- def call(socket, **options)
29
- new(socket, nil, **options)
30
- end
31
- end
32
-
33
- def initialize(socket, env, identifiers: "{}", subscriptions: nil)
34
- if env
35
- # If env is set, then somehow we're in the context of Action Cable
36
- # Return and print a warning in #process
37
- @request = ActionDispatch::Request.new(env)
38
- return
39
- end
40
-
41
- @ids = ActiveSupport::JSON.decode(identifiers)
42
-
43
- @ltags = socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
44
- next unless raw_tags
45
- ActiveSupport::JSON.decode(raw_tags)
46
- end
47
-
48
- @cached_ids = {}
49
- @coder = ActiveSupport::JSON
50
- @socket = socket
51
- @subscriptions = ActionCable::Connection::Subscriptions.new(self)
52
-
53
- return unless subscriptions
54
-
55
- # Initialize channels (for disconnect)
56
- subscriptions.each do |id|
57
- channel = @subscriptions.fetch(id)
58
- next unless socket.istate[id]
59
-
60
- channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
61
- end
62
- end
63
-
64
- def process
65
- # Use Rails logger here to print to stdout in development
66
- logger.error invalid_request_message
67
- logger.info finished_request_message
68
- [404, {"Content-Type" => "text/plain"}, ["Page not found"]]
69
- end
70
-
71
- def invalid_request_message
72
- "You're trying to connect to Action Cable server while using AnyCable. " \
73
- "See https://docs.anycable.io/troubleshooting?id=server-raises-an-argumenterror-exception-when-client-tries-to-connect"
74
- end
75
-
76
- def handle_open
77
- logger.info started_request_message if access_logs?
78
-
79
- verify_origin! || return
80
-
81
- connect if respond_to?(:connect)
82
-
83
- socket.cstate.write(LOG_TAGS_IDENTIFIER, fetch_ltags.to_json)
84
-
85
- send_welcome_message
86
- rescue ActionCable::Connection::Authorization::UnauthorizedError
87
- reject_request(
88
- ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
89
- )
90
- end
91
-
92
- def handle_close
93
- logger.info finished_request_message if access_logs?
94
-
95
- subscriptions.unsubscribe_from_all
96
- disconnect if respond_to?(:disconnect)
97
- true
98
- end
99
-
100
- def handle_channel_command(identifier, command, data)
101
- channel = subscriptions.fetch(identifier)
102
- case command
103
- when "subscribe"
104
- channel.handle_subscribe
105
- !channel.subscription_rejected?
106
- when "unsubscribe"
107
- subscriptions.remove_subscription(channel)
108
- true
109
- when "message"
110
- channel.perform_action ActiveSupport::JSON.decode(data)
111
- true
112
- else
113
- false
114
- end
115
- end
116
- # rubocop:enable Metrics/MethodLength
117
-
118
- def close(reason: nil, reconnect: nil)
119
- transmit(
120
- type: ActionCable::INTERNAL[:message_types].fetch(:disconnect, "disconnect"),
121
- reason: reason,
122
- reconnect: reconnect
123
- )
124
- socket.close
125
- end
126
-
127
- def transmit(cable_message)
128
- socket.transmit encode(cable_message)
129
- end
130
-
131
- def logger
132
- @logger ||= TaggedLoggerProxy.new(AnyCable.logger, tags: ltags || [])
133
- end
134
-
135
- def request
136
- @request ||= build_rack_request
137
- end
138
-
139
- private
140
-
141
- attr_reader :ids, :ltags
142
-
143
- def started_request_message
144
- format(
145
- 'Started "%s"%s for %s at %s',
146
- request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
147
- )
148
- end
149
-
150
- def finished_request_message(reason = "Closed")
151
- format(
152
- 'Finished "%s"%s for %s at %s (%s)',
153
- request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
154
- )
155
- end
156
-
157
- def access_logs?
158
- AnyCable.config.access_logs_disabled == false
159
- end
160
-
161
- def fetch_ltags
162
- if instance_variable_defined?(:@logger)
163
- logger.tags
164
- else
165
- ltags
166
- end
167
- end
168
-
169
- def server
170
- ActionCable.server
171
- end
172
-
173
- def verify_origin!
174
- return true unless socket.env.key?("HTTP_ORIGIN")
175
-
176
- return true if allow_request_origin?
177
-
178
- reject_request(
179
- ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
180
- )
181
- false
182
- end
183
-
184
- def reject_request(reason, reconnect = false)
185
- logger.info finished_request_message("Rejected") if access_logs?
186
- close(
187
- reason: reason,
188
- reconnect: reconnect
189
- )
190
- end
191
-
192
- def build_rack_request
193
- environment = Rails.application.env_config.merge(socket.env)
194
- AnyCable::Rails::Rack.app.call(environment)
195
-
196
- ActionDispatch::Request.new(environment)
197
- end
198
-
199
- def request_loaded?
200
- instance_variable_defined?(:@request)
201
- end
202
- end
203
- # rubocop:enable Metrics/ClassLength
204
- end
205
- end
206
-
207
- # Support rescue_from
208
- # https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
209
- if ActionCable::Connection::Base.respond_to?(:rescue_from)
210
- ActionCable::Connection::Base.prepend(Module.new do
211
- def handle_channel_command(*)
212
- super
213
- rescue Exception => e # rubocop:disable Lint/RescueException
214
- rescue_with_handler(e) || raise
215
- false
216
- end
217
- end)
218
- end
219
-
220
- require "anycable/rails/actioncable/testing" if ::Rails.env.test?
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file contains patches to Action Cable testing modules
4
-
5
- # Trigger autoload (if constant is defined)
6
- begin
7
- ActionCable::Channel::TestCase # rubocop:disable Lint/Void
8
- ActionCable::Connection::TestCase
9
- rescue NameError
10
- return
11
- end
12
-
13
- ActionCable::Channel::ChannelStub.prepend(Module.new do
14
- def subscribe_to_channel
15
- handle_subscribe
16
- end
17
- end)
18
-
19
- ActionCable::Channel::ConnectionStub.prepend(Module.new do
20
- def socket
21
- @socket ||= AnyCable::Socket.new(env: AnyCable::Env.new(url: "http://test.host", headers: {}))
22
- end
23
-
24
- alias_method :anycable_socket, :socket
25
- end)
26
-
27
- ActionCable::Connection::TestConnection.prepend(Module.new do
28
- def initialize(request)
29
- @request = request
30
- @cached_ids = {}
31
- super
32
- end
33
- end)
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnyCable
4
- module Refinements
5
- module Subscriptions # :nodoc:
6
- refine ActionCable::Connection::Subscriptions do
7
- # Find or add a subscription to the list
8
- def fetch(identifier)
9
- add("identifier" => identifier) unless subscriptions[identifier]
10
-
11
- unless subscriptions[identifier]
12
- raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
13
- end
14
-
15
- subscriptions[identifier]
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnyCable
4
- module Rails
5
- # Wrap `request.session` to lazily load values provided
6
- # in the RPC call (set by the previous calls)
7
- class SessionProxy
8
- attr_reader :rack_session, :socket_session
9
-
10
- def initialize(rack_session, socket_session)
11
- @rack_session = rack_session
12
- @socket_session = JSON.parse(socket_session).with_indifferent_access
13
- end
14
-
15
- %i[has_key? [] []= fetch delete dig].each do |mid|
16
- class_eval <<~CODE, __FILE__, __LINE__ + 1
17
- def #{mid}(*args, **kwargs, &block)
18
- restore_key! args.first
19
- rack_session.#{mid}(*args, **kwargs, &block)
20
- end
21
- CODE
22
- end
23
-
24
- alias_method :include?, :has_key?
25
- alias_method :key?, :has_key?
26
-
27
- %i[update merge! to_hash].each do |mid|
28
- class_eval <<~CODE, __FILE__, __LINE__ + 1
29
- def #{mid}(*args, **kwargs, &block)
30
- restore!
31
- rack_session.#{mid}(*args, **kwargs, &block)
32
- end
33
- CODE
34
- end
35
-
36
- alias_method :to_h, :to_hash
37
-
38
- def keys
39
- rack_session.keys + socket_session.keys
40
- end
41
-
42
- # Delegate both publuc and private methods to rack_session
43
- def respond_to_missing?(name, include_private = false)
44
- return false if name == :marshal_dump || name == :_dump
45
- rack_session.respond_to?(name, include_private) || super
46
- end
47
-
48
- def method_missing(method, *args, &block)
49
- if rack_session.respond_to?(method, true)
50
- rack_session.send(method, *args, &block)
51
- else
52
- super
53
- end
54
- end
55
-
56
- # This method is used by StimulusReflex to obtain `@by`
57
- def instance_variable_get(name)
58
- super || rack_session.instance_variable_get(name)
59
- end
60
-
61
- private
62
-
63
- def restore!
64
- socket_session.keys.each(&method(:restore_key!))
65
- end
66
-
67
- def restore_key!(key)
68
- return unless socket_session.key?(key)
69
- val = socket_session.delete(key)
70
- rack_session[key] =
71
- if val.is_a?(String)
72
- GlobalID::Locator.locate(val) || val
73
- else
74
- val
75
- end
76
- end
77
- end
78
- end
79
- end