actioncable 6.1.7.9 → 7.2.2.1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -160
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +5 -5
  5. data/app/assets/javascripts/action_cable.js +239 -302
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +114 -90
  9. data/lib/action_cable/channel/broadcasting.rb +25 -16
  10. data/lib/action_cable/channel/callbacks.rb +39 -0
  11. data/lib/action_cable/channel/naming.rb +10 -7
  12. data/lib/action_cable/channel/periodic_timers.rb +7 -7
  13. data/lib/action_cable/channel/streams.rb +81 -68
  14. data/lib/action_cable/channel/test_case.rb +133 -87
  15. data/lib/action_cable/connection/authorization.rb +4 -1
  16. data/lib/action_cable/connection/base.rb +71 -43
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +3 -1
  19. data/lib/action_cable/connection/identification.rb +10 -6
  20. data/lib/action_cable/connection/internal_channel.rb +7 -2
  21. data/lib/action_cable/connection/message_buffer.rb +4 -1
  22. data/lib/action_cable/connection/stream.rb +2 -2
  23. data/lib/action_cable/connection/stream_event_loop.rb +4 -4
  24. data/lib/action_cable/connection/subscriptions.rb +8 -3
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +14 -9
  26. data/lib/action_cable/connection/test_case.rb +67 -55
  27. data/lib/action_cable/connection/web_socket.rb +11 -7
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +28 -9
  30. data/lib/action_cable/gem_version.rb +7 -5
  31. data/lib/action_cable/helpers/action_cable_helper.rb +21 -18
  32. data/lib/action_cable/remote_connections.rb +25 -13
  33. data/lib/action_cable/server/base.rb +29 -14
  34. data/lib/action_cable/server/broadcasting.rb +24 -16
  35. data/lib/action_cable/server/configuration.rb +28 -14
  36. data/lib/action_cable/server/connections.rb +13 -5
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +4 -2
  38. data/lib/action_cable/server/worker.rb +7 -7
  39. data/lib/action_cable/subscription_adapter/async.rb +1 -1
  40. data/lib/action_cable/subscription_adapter/base.rb +2 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +2 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +2 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +6 -5
  44. data/lib/action_cable/subscription_adapter/redis.rb +101 -25
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +2 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +7 -6
  47. data/lib/action_cable/test_case.rb +2 -0
  48. data/lib/action_cable/test_helper.rb +89 -59
  49. data/lib/action_cable/version.rb +3 -1
  50. data/lib/action_cable.rb +30 -12
  51. data/lib/rails/generators/channel/USAGE +14 -8
  52. data/lib/rails/generators/channel/channel_generator.rb +95 -20
  53. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -5
  54. data/lib/rails/generators/test_unit/channel_generator.rb +2 -0
  55. metadata +29 -15
  56. data/lib/action_cable/channel.rb +0 -17
  57. data/lib/action_cable/connection.rb +0 -22
  58. data/lib/action_cable/server.rb +0 -16
  59. data/lib/action_cable/subscription_adapter.rb +0 -12
@@ -1,14 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
5
+ require "rack"
6
+
3
7
  module ActionCable
4
8
  module Server
5
- # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
6
- # in a Rails config initializer.
9
+ # # Action Cable Server Configuration
10
+ #
11
+ # An instance of this configuration object is available via
12
+ # ActionCable.server.config, which allows you to tweak Action Cable
13
+ # configuration in a Rails config initializer.
7
14
  class Configuration
8
15
  attr_accessor :logger, :log_tags
9
16
  attr_accessor :connection_class, :worker_pool_size
10
- attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
17
+ attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host, :filter_parameters
11
18
  attr_accessor :cable, :url, :mount_path
19
+ attr_accessor :precompile_assets
20
+ attr_accessor :health_check_path, :health_check_application
12
21
 
13
22
  def initialize
14
23
  @log_tags = []
@@ -18,30 +27,35 @@ module ActionCable
18
27
 
19
28
  @disable_request_forgery_protection = false
20
29
  @allow_same_origin_as_host = true
30
+ @filter_parameters = []
31
+
32
+ @health_check_application = ->(env) {
33
+ [200, { Rack::CONTENT_TYPE => "text/html", "date" => Time.now.httpdate }, []]
34
+ }
21
35
  end
22
36
 
23
- # Returns constant of subscription adapter specified in config/cable.yml.
24
- # If the adapter cannot be found, this will default to the Redis adapter.
25
- # Also makes sure proper dependencies are required.
37
+ # Returns constant of subscription adapter specified in config/cable.yml. If the
38
+ # adapter cannot be found, this will default to the Redis adapter. Also makes
39
+ # sure proper dependencies are required.
26
40
  def pubsub_adapter
27
41
  adapter = (cable.fetch("adapter") { "redis" })
28
42
 
29
43
  # Require the adapter itself and give useful feedback about
30
- # 1. Missing adapter gems and
31
- # 2. Adapter gems' missing dependencies.
44
+ # 1. Missing adapter gems and
45
+ # 2. Adapter gems' missing dependencies.
32
46
  path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
33
47
  begin
34
48
  require path_to_adapter
35
49
  rescue LoadError => e
36
- # We couldn't require the adapter itself. Raise an exception that
37
- # points out config typos and missing gems.
50
+ # We couldn't require the adapter itself. Raise an exception that points out
51
+ # config typos and missing gems.
38
52
  if e.path == path_to_adapter
39
- # We can assume that a non-builtin adapter was specified, so it's
40
- # either misspelled or missing from Gemfile.
53
+ # We can assume that a non-builtin adapter was specified, so it's either
54
+ # misspelled or missing from Gemfile.
41
55
  raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
42
56
 
43
- # Bubbled up from the adapter require. Prefix the exception message
44
- # with some guidance about how to address it and reraise.
57
+ # Bubbled up from the adapter require. Prefix the exception message with some
58
+ # guidance about how to address it and reraise.
45
59
  else
46
60
  raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
47
61
  end
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module Server
5
- # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
6
- # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
7
+ # # Action Cable Server Connections
8
+ #
9
+ # Collection class for all the connections that have been established on this
10
+ # specific server. Remember, usually you'll run many Action Cable servers, so
11
+ # you can't use this collection as a full list of all of the connections
12
+ # established against your application. Instead, use RemoteConnections for that.
7
13
  module Connections # :nodoc:
8
14
  BEAT_INTERVAL = 3
9
15
 
@@ -19,12 +25,14 @@ module ActionCable
19
25
  connections.delete connection
20
26
  end
21
27
 
22
- # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
23
- # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
28
+ # WebSocket connection implementations differ on when they'll mark a connection
29
+ # as stale. We basically never want a connection to go stale, as you then can't
30
+ # rely on being able to communicate with the connection. To solve this, a 3
31
+ # second heartbeat runs on all connections. If the beat fails, we automatically
24
32
  # disconnect.
25
33
  def setup_heartbeat_timer
26
34
  @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
27
- event_loop.post { connections.map(&:beat) }
35
+ event_loop.post { connections.each(&:beat) }
28
36
  end
29
37
  end
30
38
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module Server
5
7
  class Worker
@@ -12,8 +14,8 @@ module ActionCable
12
14
  end
13
15
  end
14
16
 
15
- def with_database_connections
16
- connection.logger.tag(ActiveRecord::Base.logger) { yield }
17
+ def with_database_connections(&block)
18
+ connection.logger.tag(ActiveRecord::Base.logger, &block)
17
19
  end
18
20
  end
19
21
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/callbacks"
4
6
  require "active_support/core_ext/module/attribute_accessors_per_thread"
5
- require "action_cable/server/worker/active_record_connection_management"
6
7
  require "concurrent"
7
8
 
8
9
  module ActionCable
@@ -19,14 +20,15 @@ module ActionCable
19
20
 
20
21
  def initialize(max_size: 5)
21
22
  @executor = Concurrent::ThreadPoolExecutor.new(
23
+ name: "ActionCable",
22
24
  min_threads: 1,
23
25
  max_threads: max_size,
24
26
  max_queue: 0,
25
27
  )
26
28
  end
27
29
 
28
- # Stop processing work: any work that has not already started
29
- # running will be discarded from the queue
30
+ # Stop processing work: any work that has not already started running will be
31
+ # discarded from the queue
30
32
  def halt
31
33
  @executor.shutdown
32
34
  end
@@ -35,12 +37,10 @@ module ActionCable
35
37
  @executor.shuttingdown?
36
38
  end
37
39
 
38
- def work(connection)
40
+ def work(connection, &block)
39
41
  self.connection = connection
40
42
 
41
- run_callbacks :work do
42
- yield
43
- end
43
+ run_callbacks :work, &block
44
44
  ensure
45
45
  self.connection = nil
46
46
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_cable/subscription_adapter/inline"
3
+ # :markup: markdown
4
4
 
5
5
  module ActionCable
6
6
  module SubscriptionAdapter
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module SubscriptionAdapter
5
7
  class Base
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module SubscriptionAdapter
5
7
  module ChannelPrefix # :nodoc:
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module SubscriptionAdapter
5
7
  class Inline < Base # :nodoc:
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  gem "pg", "~> 1.1"
4
6
  require "pg"
5
- require "thread"
6
- require "digest/sha1"
7
+ require "openssl"
7
8
 
8
9
  module ActionCable
9
10
  module SubscriptionAdapter
@@ -35,8 +36,8 @@ module ActionCable
35
36
 
36
37
  def with_subscriptions_connection(&block) # :nodoc:
37
38
  ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
38
- # Action Cable is taking ownership over this database connection, and
39
- # will perform the necessary cleanup tasks
39
+ # Action Cable is taking ownership over this database connection, and will
40
+ # perform the necessary cleanup tasks
40
41
  ActiveRecord::Base.connection_pool.remove(conn)
41
42
  end
42
43
  pg_conn = ar_conn.raw_connection
@@ -58,7 +59,7 @@ module ActionCable
58
59
 
59
60
  private
60
61
  def channel_identifier(channel)
61
- channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
62
+ channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
62
63
  end
63
64
 
64
65
  def listener
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
3
+ # :markup: markdown
4
4
 
5
- gem "redis", ">= 3", "< 5"
5
+ gem "redis", ">= 4", "< 6"
6
6
  require "redis"
7
7
 
8
8
  require "active_support/core_ext/hash/except"
@@ -12,8 +12,9 @@ module ActionCable
12
12
  class Redis < Base # :nodoc:
13
13
  prepend ChannelPrefix
14
14
 
15
- # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
16
- # This is needed, for example, when using Makara proxies for distributed Redis.
15
+ # Overwrite this factory method for Redis connections if you want to use a
16
+ # different Redis library than the redis gem. This is needed, for example, when
17
+ # using Makara proxies for distributed Redis.
17
18
  cattr_accessor :redis_connector, default: ->(config) do
18
19
  ::Redis.new(config.except(:adapter, :channel_prefix))
19
20
  end
@@ -46,7 +47,7 @@ module ActionCable
46
47
 
47
48
  private
48
49
  def listener
49
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
50
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, config_options, @server.event_loop) }
50
51
  end
51
52
 
52
53
  def redis_connection_for_broadcasts
@@ -56,11 +57,15 @@ module ActionCable
56
57
  end
57
58
 
58
59
  def redis_connection
59
- self.class.redis_connector.call(@server.config.cable.merge(id: identifier))
60
+ self.class.redis_connector.call(config_options)
61
+ end
62
+
63
+ def config_options
64
+ @config_options ||= @server.config.cable.deep_symbolize_keys.merge(id: identifier)
60
65
  end
61
66
 
62
67
  class Listener < SubscriberMap
63
- def initialize(adapter, event_loop)
68
+ def initialize(adapter, config_options, event_loop)
64
69
  super()
65
70
 
66
71
  @adapter = adapter
@@ -69,7 +74,12 @@ module ActionCable
69
74
  @subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
70
75
  @subscription_lock = Mutex.new
71
76
 
72
- @raw_client = nil
77
+ @reconnect_attempt = 0
78
+ # Use the same config as used by Redis conn
79
+ @reconnect_attempts = config_options.fetch(:reconnect_attempts, 1)
80
+ @reconnect_attempts = Array.new(@reconnect_attempts, 0) if @reconnect_attempts.is_a?(Integer)
81
+
82
+ @subscribed_client = nil
73
83
 
74
84
  @when_connected = []
75
85
 
@@ -78,13 +88,14 @@ module ActionCable
78
88
 
79
89
  def listen(conn)
80
90
  conn.without_reconnect do
81
- original_client = conn.respond_to?(:_client) ? conn._client : conn.client
91
+ original_client = extract_subscribed_client(conn)
82
92
 
83
93
  conn.subscribe("_action_cable_internal") do |on|
84
94
  on.subscribe do |chan, count|
85
95
  @subscription_lock.synchronize do
86
96
  if count == 1
87
- @raw_client = original_client
97
+ @reconnect_attempt = 0
98
+ @subscribed_client = original_client
88
99
 
89
100
  until @when_connected.empty?
90
101
  @when_connected.shift.call
@@ -106,7 +117,7 @@ module ActionCable
106
117
  on.unsubscribe do |chan, count|
107
118
  if count == 0
108
119
  @subscription_lock.synchronize do
109
- @raw_client = nil
120
+ @subscribed_client = nil
110
121
  end
111
122
  end
112
123
  end
@@ -119,8 +130,8 @@ module ActionCable
119
130
  return if @thread.nil?
120
131
 
121
132
  when_connected do
122
- send_command("unsubscribe")
123
- @raw_client = nil
133
+ @subscribed_client.unsubscribe
134
+ @subscribed_client = nil
124
135
  end
125
136
  end
126
137
 
@@ -131,13 +142,13 @@ module ActionCable
131
142
  @subscription_lock.synchronize do
132
143
  ensure_listener_running
133
144
  @subscribe_callbacks[channel] << on_success
134
- when_connected { send_command("subscribe", channel) }
145
+ when_connected { @subscribed_client.subscribe(channel) }
135
146
  end
136
147
  end
137
148
 
138
149
  def remove_channel(channel)
139
150
  @subscription_lock.synchronize do
140
- when_connected { send_command("unsubscribe", channel) }
151
+ when_connected { @subscribed_client.unsubscribe(channel) }
141
152
  end
142
153
  end
143
154
 
@@ -150,28 +161,93 @@ module ActionCable
150
161
  @thread ||= Thread.new do
151
162
  Thread.current.abort_on_exception = true
152
163
 
153
- conn = @adapter.redis_connection_for_subscriptions
154
- listen conn
164
+ begin
165
+ conn = @adapter.redis_connection_for_subscriptions
166
+ listen conn
167
+ rescue ConnectionError
168
+ reset
169
+ if retry_connecting?
170
+ when_connected { resubscribe }
171
+ retry
172
+ end
173
+ end
155
174
  end
156
175
  end
157
176
 
158
177
  def when_connected(&block)
159
- if @raw_client
178
+ if @subscribed_client
160
179
  block.call
161
180
  else
162
181
  @when_connected << block
163
182
  end
164
183
  end
165
184
 
166
- def send_command(*command)
167
- @raw_client.write(command)
185
+ def retry_connecting?
186
+ @reconnect_attempt += 1
187
+
188
+ return false if @reconnect_attempt > @reconnect_attempts.size
189
+
190
+ sleep_t = @reconnect_attempts[@reconnect_attempt - 1]
191
+
192
+ sleep(sleep_t) if sleep_t > 0
193
+
194
+ true
195
+ end
196
+
197
+ def resubscribe
198
+ channels = @sync.synchronize do
199
+ @subscribers.keys
200
+ end
201
+ @subscribed_client.subscribe(*channels) unless channels.empty?
202
+ end
203
+
204
+ def reset
205
+ @subscription_lock.synchronize do
206
+ @subscribed_client = nil
207
+ @subscribe_callbacks.clear
208
+ @when_connected.clear
209
+ end
210
+ end
211
+
212
+ if ::Redis::VERSION < "5"
213
+ ConnectionError = ::Redis::BaseConnectionError
214
+
215
+ class SubscribedClient
216
+ def initialize(raw_client)
217
+ @raw_client = raw_client
218
+ end
219
+
220
+ def subscribe(*channel)
221
+ send_command("subscribe", *channel)
222
+ end
223
+
224
+ def unsubscribe(*channel)
225
+ send_command("unsubscribe", *channel)
226
+ end
168
227
 
169
- very_raw_connection =
170
- @raw_client.connection.instance_variable_defined?(:@connection) &&
171
- @raw_client.connection.instance_variable_get(:@connection)
228
+ private
229
+ def send_command(*command)
230
+ @raw_client.write(command)
231
+
232
+ very_raw_connection =
233
+ @raw_client.connection.instance_variable_defined?(:@connection) &&
234
+ @raw_client.connection.instance_variable_get(:@connection)
235
+
236
+ if very_raw_connection && very_raw_connection.respond_to?(:flush)
237
+ very_raw_connection.flush
238
+ end
239
+ nil
240
+ end
241
+ end
242
+
243
+ def extract_subscribed_client(conn)
244
+ SubscribedClient.new(conn._client)
245
+ end
246
+ else
247
+ ConnectionError = RedisClient::ConnectionError
172
248
 
173
- if very_raw_connection && very_raw_connection.respond_to?(:flush)
174
- very_raw_connection.flush
249
+ def extract_subscribed_client(conn)
250
+ conn
175
251
  end
176
252
  end
177
253
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module SubscriptionAdapter
5
7
  class SubscriberMap
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "async"
3
+ # :markup: markdown
4
4
 
5
5
  module ActionCable
6
6
  module SubscriptionAdapter
7
- # == Test adapter for Action Cable
7
+ # ## Test adapter for Action Cable
8
8
  #
9
9
  # The test adapter should be used only in testing. Along with
10
- # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
10
+ # ActionCable::TestHelper it makes a great tool to test your Rails application.
11
11
  #
12
- # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
12
+ # To use the test adapter set `adapter` value to `test` in your
13
+ # `config/cable.yml` file.
13
14
  #
14
- # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
15
- # so it could be used in system tests too.
15
+ # NOTE: `Test` adapter extends the `ActionCable::SubscriptionAdapter::Async`
16
+ # adapter, so it could be used in system tests too.
16
17
  class Test < Async
17
18
  def broadcast(channel, payload)
18
19
  broadcasts(channel) << payload
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/test_case"
4
6
 
5
7
  module ActionCable