anycable-rails 1.4.0 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. metadata +10 -58
  4. data/lib/action_cable/subscription_adapter/any_cable.rb +0 -40
  5. data/lib/action_cable/subscription_adapter/anycable.rb +0 -10
  6. data/lib/anycable/rails/action_cable_ext/channel.rb +0 -51
  7. data/lib/anycable/rails/action_cable_ext/connection.rb +0 -90
  8. data/lib/anycable/rails/action_cable_ext/remote_connections.rb +0 -13
  9. data/lib/anycable/rails/channel_state.rb +0 -108
  10. data/lib/anycable/rails/compatibility/rubocop/config/default.yml +0 -14
  11. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +0 -50
  12. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb +0 -29
  13. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +0 -100
  14. data/lib/anycable/rails/compatibility/rubocop.rb +0 -27
  15. data/lib/anycable/rails/compatibility.rb +0 -63
  16. data/lib/anycable/rails/config.rb +0 -19
  17. data/lib/anycable/rails/connection.rb +0 -211
  18. data/lib/anycable/rails/connection_factory.rb +0 -44
  19. data/lib/anycable/rails/connections/persistent_session.rb +0 -40
  20. data/lib/anycable/rails/connections/serializable_identification.rb +0 -46
  21. data/lib/anycable/rails/connections/session_proxy.rb +0 -81
  22. data/lib/anycable/rails/middlewares/executor.rb +0 -31
  23. data/lib/anycable/rails/middlewares/log_tagging.rb +0 -21
  24. data/lib/anycable/rails/rack.rb +0 -56
  25. data/lib/anycable/rails/railtie.rb +0 -92
  26. data/lib/anycable/rails/version.rb +0 -7
  27. data/lib/anycable/rails.rb +0 -76
  28. data/lib/anycable-rails.rb +0 -3
  29. data/lib/generators/anycable/download/USAGE +0 -14
  30. data/lib/generators/anycable/download/download_generator.rb +0 -85
  31. data/lib/generators/anycable/setup/USAGE +0 -2
  32. data/lib/generators/anycable/setup/setup_generator.rb +0 -300
  33. data/lib/generators/anycable/setup/templates/Procfile.dev.tt +0 -6
  34. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +0 -48
  35. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +0 -11
  36. data/lib/generators/anycable/setup/templates/config/initializers/anycable.rb.tt +0 -9
  37. data/lib/generators/anycable/with_os_helpers.rb +0 -55
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnyCable
4
- class CompatibilityError < StandardError; end
5
-
6
- module Compatibility # :nodoc:
7
- IGNORE_INSTANCE_VARS = %i[
8
- @active_periodic_timers
9
- @_streams
10
- @parameter_filter
11
- ]
12
-
13
- ActionCable::Channel::Base.prepend(Module.new do
14
- def stream_from(broadcasting, callback = nil, coder: nil)
15
- if coder.present? && coder != ActiveSupport::JSON
16
- raise AnyCable::CompatibilityError, "Custom coders are not supported by AnyCable"
17
- end
18
-
19
- if callback.present? || block_given?
20
- raise AnyCable::CompatibilityError,
21
- "Custom stream callbacks are not supported by AnyCable"
22
- end
23
-
24
- super
25
- end
26
-
27
- # Do not prepend `subscribe_to_channel` 'cause we make it no-op
28
- # when AnyCable is running (see anycable/rails/actioncable/channel.rb)
29
- %w[run_callbacks perform_action].each do |mid|
30
- module_eval <<~CODE, __FILE__, __LINE__ + 1
31
- def #{mid}(*)
32
- __anycable_check_ivars__ { super }
33
- end
34
- CODE
35
- end
36
-
37
- def __anycable_check_ivars__
38
- was_ivars = instance_variables
39
- res = yield
40
- diff = instance_variables - was_ivars - IGNORE_INSTANCE_VARS
41
-
42
- if self.class.respond_to?(:channel_state_attributes)
43
- diff.delete(:@__istate__)
44
- diff.delete_if { |ivar| self.class.channel_state_attributes.include?(:"#{ivar.to_s.sub(/^@/, "")}") }
45
- end
46
-
47
- unless diff.empty?
48
- raise AnyCable::CompatibilityError,
49
- "Channel instance variables are not supported by AnyCable, " \
50
- "but were set: #{diff.join(", ")}"
51
- end
52
-
53
- res
54
- end
55
- end)
56
-
57
- ActionCable::Channel::Base.singleton_class.prepend(Module.new do
58
- def periodically(*)
59
- raise AnyCable::CompatibilityError, "Periodical timers are not supported by AnyCable"
60
- end
61
- end)
62
- end
63
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
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"
7
-
8
- # Extend AnyCable configuration with:
9
- # - `access_logs_disabled` (defaults to true) — whether to print Started/Finished logs
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
12
- # - `http_rpc_mount_path` (default to nil) — path to mount HTTP RPC server
13
- AnyCable::Config.attr_config(
14
- access_logs_disabled: true,
15
- persistent_session_enabled: false,
16
- embedded: false,
17
- http_rpc_mount_path: nil
18
- )
19
- AnyCable::Config.ignore_options :access_logs_disabled, :persistent_session_enabled
@@ -1,211 +0,0 @@
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
@@ -1,44 +0,0 @@
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
@@ -1,40 +0,0 @@
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
- )
@@ -1,46 +0,0 @@
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
@@ -1,81 +0,0 @@
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,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnyCable
4
- module Rails
5
- module Middlewares
6
- # Executor runs Rails executor for each call
7
- # See https://guides.rubyonrails.org/v5.2.0/threading_and_code_execution.html#framework-behavior
8
- class Executor < AnyCable::Middleware
9
- attr_reader :executor
10
-
11
- def initialize(executor)
12
- @executor = executor
13
- end
14
-
15
- def call(method, message, metadata)
16
- if ::Rails.respond_to?(:error)
17
- executor.wrap do
18
- sid = metadata["sid"]
19
-
20
- ::Rails.error.record(context: {method: method, payload: message.to_h, sid: sid}) do
21
- yield
22
- end
23
- end
24
- else
25
- executor.wrap { yield }
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnyCable
4
- module Rails
5
- module Middlewares
6
- # Middleware to add `sid` (session ID) tag to logs.
7
- #
8
- # Session ID could be provided through gRPC metadata `sid` key.
9
- #
10
- # See https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-metadata.md
11
- class LogTagging < AnyCable::Middleware
12
- def call(_method, _request, metadata)
13
- sid = metadata["sid"]
14
- return yield unless sid
15
-
16
- AnyCable.logger.tagged("AnyCable sid=#{sid}") { yield }
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/configuration"
4
- require "action_dispatch/middleware/stack"
5
-
6
- module AnyCable
7
- module Rails
8
- # Rack middleware stack to modify the HTTP request object.
9
- #
10
- # AnyCable Websocket server does not use Rack middleware processing mechanism (which Rails uses
11
- # when Action Cable is mounted into the main app).
12
- #
13
- # Some middlewares could enhance request env with useful information.
14
- #
15
- # For instance, consider the Rails session middleware: it's responsible for restoring the
16
- # session data from cookies.
17
- #
18
- # AnyCable adds session middelware by default to its own stack.
19
- #
20
- # You can also use any Rack/Rails middleware you want. For example, to enable Devise/Warden
21
- # you can add the following code to an initializer or any other configuration file:
22
- #
23
- # AnyCable::Rails::Rack.middleware.use Warden::Manager do |config|
24
- # Devise.warden_config = config
25
- # end
26
- module Rack
27
- def self.app_build_lock
28
- @app_build_lock
29
- end
30
-
31
- @app_build_lock = Mutex.new
32
-
33
- def self.middleware
34
- @middleware ||= ::Rails::Configuration::MiddlewareStackProxy.new
35
- end
36
-
37
- def self.default_middleware_stack
38
- config = ::Rails.application.config
39
-
40
- ActionDispatch::MiddlewareStack.new do |middleware|
41
- middleware.use(config.session_store, config.session_options) if config.session_store
42
- end
43
- end
44
-
45
- def self.app
46
- @rack_app || app_build_lock.synchronize do
47
- @rack_app ||= default_middleware_stack.yield_self do |stack|
48
- middleware.merge_into(stack)
49
- end.yield_self do |stack|
50
- stack.build { [-1, {}, []] }
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end