anycable-rails-core 1.5.3 → 1.6.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +10 -75
  4. data/lib/anycable/rails/action_cable_ext/channel.rb +20 -11
  5. data/lib/anycable/rails/action_cable_ext/remote_connections.rb +4 -14
  6. data/lib/anycable/rails/channel/presence.rb +36 -0
  7. data/lib/anycable/rails/channel.rb +9 -0
  8. data/lib/anycable/rails/compatibility/rubocop.rb +2 -25
  9. data/lib/anycable/rails/compatibility.rb +2 -0
  10. data/lib/anycable/rails/config.rb +2 -0
  11. data/lib/anycable/rails/connection.rb +62 -33
  12. data/lib/anycable/rails/connection_factory.rb +20 -1
  13. data/lib/anycable/rails/connections/persistent_session.rb +5 -0
  14. data/lib/anycable/rails/connections/serializable_identification.rb +19 -0
  15. data/lib/anycable/rails/connections/session_proxy.rb +2 -2
  16. data/lib/anycable/rails/ext/jwt.rb +1 -1
  17. data/lib/anycable/rails/ext/signed_streams.rb +21 -0
  18. data/lib/anycable/rails/ext/whisper.rb +34 -0
  19. data/lib/anycable/rails/ext.rb +4 -1
  20. data/lib/anycable/rails/helper.rb +2 -2
  21. data/lib/anycable/rails/next/action_cable_ext/channel.rb +36 -0
  22. data/lib/anycable/rails/next/action_cable_ext/connection.rb +29 -0
  23. data/lib/anycable/rails/next/connection/persistent_session.rb +39 -0
  24. data/lib/anycable/rails/next/connection.rb +226 -0
  25. data/lib/anycable/rails/railtie.rb +4 -8
  26. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/instance_vars.rb +1 -1
  27. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/periodical_timers.rb +3 -8
  28. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/stream_from.rb +3 -5
  29. data/lib/anycable/rails/rubocop.rb +27 -0
  30. data/lib/anycable/rails/version.rb +1 -1
  31. data/lib/anycable/rails.rb +2 -1
  32. data/lib/generators/anycable/bin/templates/bin/anycable-go.tt +5 -0
  33. data/lib/generators/anycable/download/download_generator.rb +16 -5
  34. data/lib/generators/anycable/setup/setup_generator.rb +280 -38
  35. data/lib/generators/anycable/setup/templates/Procfile.dev.tt +2 -2
  36. data/lib/generators/anycable/setup/templates/anycable.toml.tt +81 -0
  37. data/lib/generators/anycable/setup/templates/bin/anycable-go.tt +5 -0
  38. data/lib/generators/anycable/setup/templates/bin/dev +17 -0
  39. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +20 -33
  40. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +2 -5
  41. metadata +27 -16
  42. data/lib/anycable/rails/action_cable_ext/signed_streams.rb +0 -31
  43. /data/lib/anycable/rails/{compatibility/rubocop → rubocop}/config/default.yml +0 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+ require "anycable/rails/connections/serializable_identification"
5
+
6
+ ActionCable::Connection::Base.include(AnyCable::Rails::Connections::SerializableIdentification)
7
+ ActionCable::Connection::Base.prepend(Module.new do
8
+ def anycabled?
9
+ anycable_socket
10
+ end
11
+
12
+ # Allow overriding #subscriptions to use a custom implementation
13
+ attr_writer :subscriptions
14
+
15
+ # Alias for the #socket which is only set within AnyCable RPC context
16
+ attr_accessor :anycable_socket
17
+
18
+ # Enhance #send_welcome_message to include sid if present
19
+ def send_welcome_message
20
+ transmit({
21
+ type: ActionCable::INTERNAL[:message_types][:welcome],
22
+ sid: env["anycable.sid"]
23
+ }.compact)
24
+ end
25
+
26
+ def subscribe_to_internal_channel
27
+ super unless anycabled?
28
+ end
29
+ end)
@@ -0,0 +1,39 @@
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 request
18
+ @request ||= super.tap do |req|
19
+ next unless socket.session
20
+ req.env[::Rack::RACK_SESSION] =
21
+ SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def commit_session!
28
+ return unless defined?(@request) && request.session.respond_to?(:loaded?) && request.session.loaded?
29
+
30
+ socket.session = request.session.to_json
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ AnyCable::Rails::Connection.prepend(
38
+ AnyCable::Rails::Connections::PersistentSession
39
+ )
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ module AnyCable
6
+ module Rails
7
+ class Current < ActiveSupport::CurrentAttributes
8
+ attribute :identifier
9
+ end
10
+
11
+ # Wrap ActionCable.server to provide a custom executor
12
+ # and a pubsub adapter
13
+ class Server < SimpleDelegator
14
+ # Implements an executor inteface
15
+ class Executor
16
+ class NoopTimer
17
+ def shutdown = nil
18
+ end
19
+
20
+ NOOP_TIMER = NoopTimer.new.freeze
21
+
22
+ def post(...)
23
+ raise NonImplementedError, "Executor#post is not implemented in AnyCable context"
24
+ end
25
+
26
+ def timer(...) = NOOP_TIMER
27
+ end
28
+
29
+ # A signleton executor for all connections
30
+ EXECUTOR = Executor.new.freeze
31
+
32
+ # PubSub adapter to manage streams configuration
33
+ # for the underlying socket
34
+ class PubSub
35
+ private attr_reader :socket
36
+
37
+ ALL_STREAMS = Data.define(:to_str).new("all")
38
+
39
+ def initialize(socket) = @socket = socket
40
+
41
+ def subscribe(channel, _message_callback, success_callback = nil)
42
+ socket.subscribe identifier, channel
43
+ success_callback&.call
44
+ end
45
+
46
+ def unsubscribe(channel, _message_callback)
47
+ if channel == ALL_STREAMS
48
+ socket.unsubscribe_from_all identifier
49
+ else
50
+ socket.unsubscribe identifier, channel
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def identifier = Current.identifier
57
+ end
58
+
59
+ attr_accessor :pubsub, :executor
60
+
61
+ def self.for(server, socket)
62
+ new(server).tap do |srv|
63
+ srv.executor = EXECUTOR
64
+ srv.pubsub = PubSub.new(socket)
65
+ end
66
+ end
67
+ end
68
+
69
+ class Connection
70
+ class Subscriptions < ::ActionCable::Connection::Subscriptions
71
+ # Wrap the original #execute_command to pre-initialize the channel for unsubscribe/message and
72
+ # return true/false to indicate successful/unsuccessful subscription.
73
+ def execute_command(data)
74
+ cmd = data["command"]
75
+
76
+ # We need the current channel identifier in pub/sub
77
+ Current.identifier = data["identifier"]
78
+
79
+ load(data["identifier"]) unless cmd == "subscribe"
80
+
81
+ super
82
+
83
+ return true unless cmd == "subscribe"
84
+
85
+ subscription = subscriptions[data["identifier"]]
86
+ !(subscription.nil? || subscription.rejected?)
87
+ end
88
+
89
+ # Restore channels from the list of identifiers and the state
90
+ def restore(subscriptions, istate)
91
+ subscriptions.each do |id|
92
+ channel = load(id)
93
+ channel.__istate__ = ActiveSupport::JSON.decode(istate[id]) if istate[id]
94
+ end
95
+ end
96
+
97
+ # Find or create a channel for a given identifier
98
+ def load(identifier)
99
+ return subscriptions[identifier] if subscriptions[identifier]
100
+
101
+ subscription = subscription_from_identifier(identifier)
102
+ raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}" unless subscription
103
+
104
+ subscriptions[identifier] = subscription
105
+ end
106
+ end
107
+
108
+ # We store logger tags in the connection state to be able
109
+ # to re-use them in the subsequent calls
110
+ LOG_TAGS_IDENTIFIER = "__ltags__"
111
+
112
+ attr_reader :socket, :server
113
+
114
+ delegate :identifiers_json, to: :conn
115
+ delegate :cstate, :istate, to: :socket
116
+
117
+ def initialize(connection_class, socket, identifiers: nil, subscriptions: nil, server: ::ActionCable.server)
118
+ server = Server.for(server, socket)
119
+
120
+ @socket = socket
121
+ @server = server
122
+ # TODO: Move protocol to socket.env as "anycable.protocol"
123
+ @protocol = "actioncable-v1-json"
124
+
125
+ logger_tags = fetch_logger_tags_from_state
126
+ @logger = ActionCable::Server::TaggedLoggerProxy.new(AnyCable.logger, tags: logger_tags)
127
+
128
+ @conn = connection_class.new(server, self)
129
+ conn.subscriptions = Subscriptions.new(conn)
130
+ conn.identifiers_json = identifiers
131
+ conn.anycable_socket = socket
132
+ conn.subscriptions.restore(subscriptions, socket.istate) if subscriptions
133
+ end
134
+
135
+ # == AnyCable RPC interface [BEGIN] ==
136
+ def handle_open
137
+ logger.info started_request_message if access_logs?
138
+
139
+ return close unless allow_request_origin?
140
+
141
+ conn.handle_open
142
+
143
+ # Commit log tags to the connection state
144
+ socket.cstate.write(LOG_TAGS_IDENTIFIER, logger.tags.to_json) unless logger.tags.empty?
145
+
146
+ socket.closed?
147
+ end
148
+
149
+ def handle_close
150
+ conn.handle_close
151
+ close
152
+ true
153
+ end
154
+
155
+ def handle_channel_command(identifier, command, data)
156
+ conn.handle_incoming({"command" => command, "identifier" => identifier, "data" => data})
157
+ end
158
+ # == AnyCable RPC interface [END] ==
159
+
160
+ # == Action Cable socket interface [BEGIN]
161
+ attr_reader :protocol, :logger
162
+
163
+ def request
164
+ @request ||= begin
165
+ env = socket.env
166
+ environment = ::Rails.application.env_config.merge(env) if defined?(::Rails.application) && ::Rails.application
167
+ AnyCable::Rails::Rack.app.call(environment) if environment
168
+
169
+ ActionDispatch::Request.new(environment || env)
170
+ end
171
+ end
172
+
173
+ delegate :env, to: :request
174
+
175
+ def transmit(data)
176
+ socket.transmit ActiveSupport::JSON.encode(data)
177
+ end
178
+
179
+ def close(...)
180
+ return if socket.closed?
181
+ logger.info finished_request_message if access_logs?
182
+ socket.close(...)
183
+ end
184
+
185
+ def perform_work(receiver, method_name, *args)
186
+ raise ArgumentError, "Performing work is not supported within AnyCable"
187
+ end
188
+ # == Action Cable socket interface [END]
189
+
190
+ private
191
+
192
+ attr_reader :conn
193
+
194
+ def fetch_logger_tags_from_state
195
+ socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
196
+ next [] unless raw_tags
197
+ ActiveSupport::JSON.decode(raw_tags)
198
+ end
199
+ end
200
+
201
+ def started_request_message
202
+ format(
203
+ 'Started "%s"%s for %s at %s',
204
+ request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
205
+ )
206
+ end
207
+
208
+ def finished_request_message
209
+ format(
210
+ 'Finished "%s"%s for %s at %s',
211
+ request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
212
+ )
213
+ end
214
+
215
+ def allow_request_origin?
216
+ return true unless socket.env.key?("HTTP_ORIGIN")
217
+
218
+ server.allow_request_origin?(socket.env)
219
+ end
220
+
221
+ def access_logs?
222
+ AnyCable.config.access_logs_disabled == false
223
+ end
224
+ end
225
+ end
226
+ end
@@ -1,11 +1,5 @@
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
- require "anycable/rails/action_cable_ext/broadcast_options"
7
- require "anycable/rails/action_cable_ext/signed_streams"
8
-
9
3
  require "anycable/rails/channel_state"
10
4
  require "anycable/rails/connection_factory"
11
5
 
@@ -119,6 +113,7 @@ module AnyCable
119
113
  initializer "anycable.routes" do
120
114
  config.after_initialize do |app|
121
115
  config = AnyCable.config
116
+ config.http_rpc_mount_path ||= "/_anycable" if config.http_rpc?
122
117
  unless config.http_rpc_mount_path.nil?
123
118
  app.routes.prepend do
124
119
  mount AnyCable::HTTRPC::Server.new => config.http_rpc_mount_path, :internal => true
@@ -129,11 +124,12 @@ module AnyCable
129
124
 
130
125
  initializer "anycable.verify_pool_sizes" do
131
126
  next if AnyCable.config.disable_rpc_pool_size_warning?
132
- # Skip if non-gRPC server is used
133
- next unless AnyCable.config.respond_to?(:rpc_pool_size)
134
127
 
135
128
  # Log current db vs. gRPC pool sizes
136
129
  AnyCable.configure_server do
130
+ # Skip if non-gRPC server is used
131
+ next unless AnyCable.config.respond_to?(:rpc_pool_size)
132
+
137
133
  ActiveSupport.on_load(:active_record) do
138
134
  db_pool_size = ::ActiveRecord::Base.connection_pool.size
139
135
  rpc_pool_size = AnyCable.config.rpc_pool_size
@@ -24,7 +24,7 @@ module RuboCop
24
24
  # end
25
25
  # end
26
26
  #
27
- class InstanceVars < RuboCop::Cop::Cop
27
+ class InstanceVars < RuboCop::Cop::Base
28
28
  MSG = "Channel instance variables are not supported in AnyCable. Use `state_attr_accessor` instead"
29
29
 
30
30
  def on_class(node)
@@ -13,16 +13,11 @@ module RuboCop
13
13
  # periodically(:do_something, every: 2.seconds)
14
14
  # end
15
15
  #
16
- class PeriodicalTimers < RuboCop::Cop::Cop
16
+ class PeriodicalTimers < RuboCop::Cop::Base
17
17
  MSG = "Periodical Timers are not supported in AnyCable"
18
+ RESTRICT_ON_SEND = %i[periodically].freeze
18
19
 
19
- def_node_matcher :calls_periodically?, <<-PATTERN
20
- (send _ :periodically ...)
21
- PATTERN
22
-
23
- def on_send(node)
24
- add_offense(node) if calls_periodically?(node)
25
- end
20
+ alias_method :on_send, :add_offense
26
21
  end
27
22
  end
28
23
  end
@@ -34,7 +34,7 @@ module RuboCop
34
34
  # end
35
35
  # end
36
36
  #
37
- class StreamFrom < RuboCop::Cop::Cop
37
+ class StreamFrom < RuboCop::Cop::Base
38
38
  def_node_matcher :stream_from_with_block?, <<-PATTERN
39
39
  (block {(send _ :stream_from ...) (send _ :stream_for ...)} ...)
40
40
  PATTERN
@@ -81,16 +81,14 @@ module RuboCop
81
81
 
82
82
  def add_callback_offense(node)
83
83
  add_offense(
84
- node,
85
- location: :expression,
84
+ node.loc.expression,
86
85
  message: "Custom stream callbacks are not supported in AnyCable"
87
86
  )
88
87
  end
89
88
 
90
89
  def add_custom_coder_offense(node)
91
90
  add_offense(
92
- node,
93
- location: :expression,
91
+ node.loc.expression,
94
92
  message: "Custom coders are not supported in AnyCable"
95
93
  )
96
94
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "pathname"
5
+
6
+ require_relative "rubocop/cops/anycable/stream_from"
7
+ require_relative "rubocop/cops/anycable/periodical_timers"
8
+ require_relative "rubocop/cops/anycable/instance_vars"
9
+
10
+ module RuboCop
11
+ module AnyCable # :nodoc:
12
+ CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
13
+
14
+ # Merge anycable config into default configuration
15
+ # See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
16
+ def self.inject!
17
+ path = CONFIG_DEFAULT.to_s
18
+ puts "configuration from #{path}" if ConfigLoader.debug?
19
+ hash = ConfigLoader.send(:load_yaml_configuration, path)
20
+ config = Config.new(hash, path)
21
+ config = ConfigLoader.merge_with_default(config, path)
22
+ ConfigLoader.instance_variable_set(:@default_configuration, config)
23
+ end
24
+ end
25
+ end
26
+
27
+ RuboCop::AnyCable.inject!
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "1.5.3"
5
+ VERSION = "1.6.2"
6
6
  end
7
7
  end
@@ -9,6 +9,7 @@ require "globalid"
9
9
  require "active_support/core_ext/module/attribute_accessors_per_thread"
10
10
 
11
11
  require "anycable/rails/ext"
12
+ require "anycable/rails/channel"
12
13
 
13
14
  module AnyCable
14
15
  # Rails handler for AnyCable
@@ -84,7 +85,7 @@ end
84
85
 
85
86
  # Warn if application has been already initialized.
86
87
  # AnyCable should be loaded before initialization in order to work correctly.
87
- if defined?(::Rails) && ::Rails.application && ::Rails.application.initialized?
88
+ if defined?(::Rails) && ::Rails.application&.initialized?
88
89
  puts("\n**************************************************")
89
90
  puts(
90
91
  "⛔️ WARNING: AnyCable loaded after application initialization. Might not work correctly.\n" \
@@ -10,6 +10,11 @@ if [ ! -f ./bin/dist/anycable-go ]; then
10
10
  ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist
11
11
  fi
12
12
 
13
+ if [ -x ./bin/dist/anycable-go ]; then
14
+ echo "Setting execution permission for AnyCable server"
15
+ chmod +x ./bin/dist/anycable-go
16
+ fi
17
+
13
18
  curVersion=$(./bin/dist/anycable-go -v)
14
19
 
15
20
  if [[ "$version" != "latest" ]]; then
@@ -37,24 +37,35 @@ module AnyCableRailsGenerators
37
37
  return latest_release_url(version) if version == "latest"
38
38
 
39
39
  if Gem::Version.new(version).segments.first >= 1
40
- new_release_url("v#{version}")
40
+ new_release_url(version)
41
41
  else
42
- legacy_release_url("v#{version}")
42
+ legacy_release_url(version)
43
+ end
44
+ end
45
+
46
+ def repository_name(version)
47
+ return "anycable/anycable" if version == "latest"
48
+
49
+ major, minor, = Gem::Version.new(version).segments
50
+ if (major >= 1 && minor >= 6) || (major > 1)
51
+ "anycable/anycable"
52
+ else
53
+ "anycable/anycable-go"
43
54
  end
44
55
  end
45
56
 
46
57
  def legacy_release_url(version)
47
- "https://github.com/anycable/anycable-go/releases/download/#{version}/" \
58
+ "https://github.com/#{repository_name(version)}/releases/download/v#{version}/" \
48
59
  "anycable-go-v#{version}-#{os_name}-#{cpu_name}"
49
60
  end
50
61
 
51
62
  def new_release_url(version)
52
- "https://github.com/anycable/anycable-go/releases/download/#{version}/" \
63
+ "https://github.com/#{repository_name(version)}/releases/download/v#{version}/" \
53
64
  "anycable-go-#{os_name}-#{cpu_name}"
54
65
  end
55
66
 
56
67
  def latest_release_url(version)
57
- "https://github.com/anycable/anycable-go/releases/latest/download/" \
68
+ "https://github.com/#{repository_name(version)}/releases/latest/download/" \
58
69
  "anycable-go-#{os_name}-#{cpu_name}"
59
70
  end
60
71