anycable-rails 1.4.0 → 1.4.2

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.
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