anycable-rails 1.2.0 → 1.3.1

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: 6502d9587bfab38ed0e12416741885869551926e2d8fae5282cca66ca59e97cf
4
- data.tar.gz: 7c16519ba1a470e68fbcf0d5baaa261df245d64618277f8b4b2992821d543d1c
3
+ metadata.gz: e0b6b1675986dcb501b40474c041eb7605d10c20a0d3cad24137e212858fdd37
4
+ data.tar.gz: 9550331c49b15c2728dbe67a0a4d2238b2f02c7a43fe3a66e519eee87a5277f7
5
5
  SHA512:
6
- metadata.gz: dbfe72bae1b477034e32f7bb85e3c936bef3caf03db7d54cfe126e2cccbd7cc24494ec8cd7718887f9a56a41d3dab10ec24cf30ca6c7de0bd162e1658aa143f9
7
- data.tar.gz: ba01f329e4388d4257952592f214842214432e9c211261362bf530913ace6eb3b9bc74bf2bab3e8a9446b6d7e48b9dd54a82bc44f21276506b9c743403e0afae
6
+ metadata.gz: 93f254e60bfc30049050757a9603285a07b8ceefcb3a878670df220dc4b375ca6eed1ac7beb0dd758f8ff67ad879f60045ac273281e0a6a4dfa0d7bd6bdd573f
7
+ data.tar.gz: 95cb2de61f41a461818abdd49d453b7f678a47a6200d2f9caca271addb563e43fd74928bcf538ee1e5d06f40c491e99e236d0c88e8dd75223ddb2d46d502c494
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.3.1 (202-02-28)
6
+
7
+ - Fix Action Cable Channel patch to not change methods signatures. ([@palkan][])
8
+
9
+ Otherwise it could lead to conflicts with other patches.
10
+
11
+ ## 1.3.0 (2022-02-21)
12
+
13
+ - Introduce `AnyCable::Rails.extend_adapter!` to make any pubsub adapter AnyCable-compatible. ([@palkan][])
14
+
15
+ - Refactored Action Cable patching to preserve original functionality and avoid monkey-patching collisions. ([@palkan][])
16
+
17
+ ## 1.2.1 (2022-01-31)
18
+
19
+ - Add a temporary fix to be compatible with `sentry-rails`. ([@palkan][])
20
+
21
+ See [#165](https://github.com/anycable/anycable-rails/issues/165).
22
+
23
+ - Run embedded RPC server only if `any_cable` adapter is used for Action Cable. ([@palkan][])
24
+
5
25
  ## 1.2.0 (2021-12-21) 🎄
6
26
 
7
27
  - Drop Rails 5 support.
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,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,49 @@
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
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,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,19 +48,12 @@ 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
@@ -63,7 +61,7 @@ module AnyCable
63
61
  # Since Rails 6.1
64
62
  if respond_to?(:server)
65
63
  server do
66
- next unless AnyCable.config.embedded?
64
+ next unless AnyCable.config.embedded? && AnyCable::Rails.enabled?
67
65
 
68
66
  require "anycable/cli"
69
67
  AnyCable::CLI.embed!
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "1.2.0"
5
+ VERSION = "1.3.1"
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.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-21 00:00:00.000000000 Z
11
+ date: 2022-02-28 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