actioncable 7.0.8.6 → 7.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -166
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +4 -4
  5. data/app/assets/javascripts/action_cable.js +25 -4
  6. data/app/assets/javascripts/actioncable.esm.js +25 -4
  7. data/app/assets/javascripts/actioncable.js +25 -4
  8. data/lib/action_cable/channel/base.rb +17 -5
  9. data/lib/action_cable/channel/broadcasting.rb +3 -1
  10. data/lib/action_cable/channel/callbacks.rb +16 -0
  11. data/lib/action_cable/channel/naming.rb +4 -2
  12. data/lib/action_cable/channel/streams.rb +2 -0
  13. data/lib/action_cable/channel/test_case.rb +6 -1
  14. data/lib/action_cable/connection/authorization.rb +1 -1
  15. data/lib/action_cable/connection/base.rb +18 -5
  16. data/lib/action_cable/connection/callbacks.rb +51 -0
  17. data/lib/action_cable/connection/internal_channel.rb +3 -1
  18. data/lib/action_cable/connection/stream.rb +1 -3
  19. data/lib/action_cable/connection/stream_event_loop.rb +0 -1
  20. data/lib/action_cable/connection/subscriptions.rb +2 -0
  21. data/lib/action_cable/connection/tagged_logger_proxy.rb +6 -4
  22. data/lib/action_cable/connection/test_case.rb +3 -1
  23. data/lib/action_cable/connection/web_socket.rb +2 -0
  24. data/lib/action_cable/deprecator.rb +7 -0
  25. data/lib/action_cable/engine.rb +13 -5
  26. data/lib/action_cable/gem_version.rb +4 -4
  27. data/lib/action_cable/remote_connections.rb +11 -2
  28. data/lib/action_cable/server/base.rb +3 -0
  29. data/lib/action_cable/server/broadcasting.rb +2 -0
  30. data/lib/action_cable/server/configuration.rb +12 -2
  31. data/lib/action_cable/server/connections.rb +3 -1
  32. data/lib/action_cable/server/worker.rb +0 -1
  33. data/lib/action_cable/subscription_adapter/async.rb +0 -2
  34. data/lib/action_cable/subscription_adapter/postgresql.rb +0 -1
  35. data/lib/action_cable/subscription_adapter/redis.rb +3 -6
  36. data/lib/action_cable/subscription_adapter/test.rb +3 -5
  37. data/lib/action_cable/test_helper.rb +53 -22
  38. data/lib/action_cable/version.rb +1 -1
  39. data/lib/action_cable.rb +24 -12
  40. data/lib/rails/generators/channel/USAGE +14 -8
  41. data/lib/rails/generators/channel/channel_generator.rb +21 -7
  42. metadata +31 -19
  43. data/lib/action_cable/channel.rb +0 -17
  44. data/lib/action_cable/connection.rb +0 -22
  45. data/lib/action_cable/server.rb +0 -16
  46. data/lib/action_cable/subscription_adapter.rb +0 -12
@@ -5,7 +5,7 @@ module ActionCable
5
5
  module Authorization
6
6
  class UnauthorizedError < StandardError; end
7
7
 
8
- # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
8
+ # Closes the WebSocket connection if it is open and returns an "unauthorized" reason.
9
9
  def reject_unauthorized_connection
10
10
  logger.error "An unauthorized connection attempt was rejected"
11
11
  raise UnauthorizedError
@@ -5,6 +5,8 @@ require "active_support/rescuable"
5
5
 
6
6
  module ActionCable
7
7
  module Connection
8
+ # = Action Cable \Connection \Base
9
+ #
8
10
  # For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
9
11
  # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
10
12
  # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
@@ -47,10 +49,11 @@ module ActionCable
47
49
  include Identification
48
50
  include InternalChannel
49
51
  include Authorization
52
+ include Callbacks
50
53
  include ActiveSupport::Rescuable
51
54
 
52
55
  attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
53
- delegate :event_loop, :pubsub, to: :server
56
+ delegate :event_loop, :pubsub, :config, to: :server
54
57
 
55
58
  def initialize(server, env, coder: ActiveSupport::JSON)
56
59
  @server, @env, @coder = server, env, coder
@@ -86,12 +89,18 @@ module ActionCable
86
89
 
87
90
  def dispatch_websocket_message(websocket_message) # :nodoc:
88
91
  if websocket.alive?
89
- subscriptions.execute_command decode(websocket_message)
92
+ handle_channel_command decode(websocket_message)
90
93
  else
91
94
  logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
92
95
  end
93
96
  end
94
97
 
98
+ def handle_channel_command(payload)
99
+ run_callbacks :command do
100
+ subscriptions.execute_command payload
101
+ end
102
+ end
103
+
95
104
  def transmit(cable_message) # :nodoc:
96
105
  websocket.transmit encode(cable_message)
97
106
  end
@@ -143,6 +152,10 @@ module ActionCable
143
152
  send_async :handle_close
144
153
  end
145
154
 
155
+ def inspect # :nodoc:
156
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
157
+ end
158
+
146
159
  private
147
160
  attr_reader :websocket
148
161
  attr_reader :message_buffer
@@ -222,7 +235,7 @@ module ActionCable
222
235
 
223
236
  logger.error invalid_request_message
224
237
  logger.info finished_request_message
225
- [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
238
+ [ 404, { Rack::CONTENT_TYPE => "text/plain; charset=utf-8" }, [ "Page not found" ] ]
226
239
  end
227
240
 
228
241
  # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
@@ -237,7 +250,7 @@ module ActionCable
237
250
  request.filtered_path,
238
251
  websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
239
252
  request.ip,
240
- Time.now.to_default_s ]
253
+ Time.now.to_s ]
241
254
  end
242
255
 
243
256
  def finished_request_message
@@ -245,7 +258,7 @@ module ActionCable
245
258
  request.filtered_path,
246
259
  websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
247
260
  request.ip,
248
- Time.now.to_default_s ]
261
+ Time.now.to_s ]
249
262
  end
250
263
 
251
264
  def invalid_request_message
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+
5
+ module ActionCable
6
+ module Connection
7
+ # = Action Cable \Connection \Callbacks
8
+ #
9
+ # There are <tt>before_command</tt>, <tt>after_command</tt>, and <tt>around_command</tt> callbacks
10
+ # available to be invoked before, after or around every command received by a client respectively.
11
+ # The term "command" here refers to any interaction received by a client (subscribing, unsubscribing or performing actions):
12
+ #
13
+ # module ApplicationCable
14
+ # class Connection < ActionCable::Connection::Base
15
+ # identified_by :user
16
+ #
17
+ # around_command :set_current_account
18
+ #
19
+ # private
20
+ #
21
+ # def set_current_account
22
+ # # Now all channels could use Current.account
23
+ # Current.set(account: user.account) { yield }
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ module Callbacks
29
+ extend ActiveSupport::Concern
30
+ include ActiveSupport::Callbacks
31
+
32
+ included do
33
+ define_callbacks :command
34
+ end
35
+
36
+ module ClassMethods
37
+ def before_command(*methods, &block)
38
+ set_callback(:command, :before, *methods, &block)
39
+ end
40
+
41
+ def after_command(*methods, &block)
42
+ set_callback(:command, :after, *methods, &block)
43
+ end
44
+
45
+ def around_command(*methods, &block)
46
+ set_callback(:command, :around, *methods, &block)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Connection
5
+ # = Action Cable \InternalChannel
6
+ #
5
7
  # Makes it possible for the RemoteConnection to disconnect a specific connection.
6
8
  module InternalChannel
7
9
  extend ActiveSupport::Concern
@@ -32,7 +34,7 @@ module ActionCable
32
34
  case message["type"]
33
35
  when "disconnect"
34
36
  logger.info "Removing connection (#{connection_identifier})"
35
- websocket.close
37
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:remote], reconnect: message.fetch("reconnect", true))
36
38
  end
37
39
  rescue Exception => e
38
40
  logger.error "There was an exception - #{e.class}(#{e.message})"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
-
5
3
  module ActionCable
6
4
  module Connection
7
5
  #--
@@ -100,7 +98,7 @@ module ActionCable
100
98
 
101
99
  # This should return the underlying io according to the SPEC:
102
100
  @rack_hijack_io = @socket_object.env["rack.hijack"].call
103
- # Retain existing behaviour if required:
101
+ # Retain existing behavior if required:
104
102
  @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
105
103
 
106
104
  @event_loop.attach(@rack_hijack_io, self)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nio"
4
- require "thread"
5
4
 
6
5
  module ActionCable
7
6
  module Connection
@@ -4,6 +4,8 @@ require "active_support/core_ext/hash/indifferent_access"
4
4
 
5
5
  module ActionCable
6
6
  module Connection
7
+ # = Action Cable \Connection \Subscriptions
8
+ #
7
9
  # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
8
10
  # the connection to the proper channel.
9
11
  class Subscriptions # :nodoc:
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Connection
5
+ # = Action Cable \Connection \TaggedLoggerProxy
6
+ #
5
7
  # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
6
8
  # ActiveSupport::TaggedLogging enhanced Rails.logger, as that logger will reset the tags between requests.
7
9
  # The connection is long-lived, so it needs its own set of tags for its independent duration.
@@ -28,14 +30,14 @@ module ActionCable
28
30
  end
29
31
 
30
32
  %i( debug info warn error fatal unknown ).each do |severity|
31
- define_method(severity) do |message|
32
- log severity, message
33
+ define_method(severity) do |message = nil, &block|
34
+ log severity, message, &block
33
35
  end
34
36
  end
35
37
 
36
38
  private
37
- def log(type, message) # :doc:
38
- tag(@logger) { @logger.send type, message }
39
+ def log(type, message, &block) # :doc:
40
+ tag(@logger) { @logger.send type, message, &block }
39
41
  end
40
42
  end
41
43
  end
@@ -56,6 +56,8 @@ module ActionCable
56
56
  end
57
57
  end
58
58
 
59
+ # = Action Cable \Connection \TestCase
60
+ #
59
61
  # Unit test Action Cable connections.
60
62
  #
61
63
  # Useful to check whether a connection's +identified_by+ gets assigned properly
@@ -116,7 +118,7 @@ module ActionCable
116
118
  # assert_equal "1", connection.user_id
117
119
  # end
118
120
  #
119
- # == Connection is automatically inferred
121
+ # == \Connection is automatically inferred
120
122
  #
121
123
  # ActionCable::Connection::TestCase will automatically infer the connection under test
122
124
  # from the test class name. If the channel cannot be inferred from the test
@@ -4,6 +4,8 @@ require "websocket/driver"
4
4
 
5
5
  module ActionCable
6
6
  module Connection
7
+ # = Action Cable \Connection \WebSocket
8
+ #
7
9
  # Wrap the real socket to minimize the externally-presented API
8
10
  class WebSocket # :nodoc:
9
11
  def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "rails"
4
4
  require "action_cable"
5
- require "action_cable/helpers/action_cable_helper"
6
5
  require "active_support/core_ext/hash/indifferent_access"
7
6
 
8
7
  module ActionCable
@@ -11,7 +10,9 @@ module ActionCable
11
10
  config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
12
11
  config.action_cable.precompile_assets = true
13
12
 
14
- config.eager_load_namespaces << ActionCable
13
+ initializer "action_cable.deprecator", before: :load_environment_config do |app|
14
+ app.deprecators[:action_cable] = ActionCable.deprecator
15
+ end
15
16
 
16
17
  initializer "action_cable.helpers" do
17
18
  ActiveSupport.on_load(:action_view) do
@@ -23,6 +24,12 @@ module ActionCable
23
24
  ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
24
25
  end
25
26
 
27
+ initializer "action_cable.health_check_application" do
28
+ ActiveSupport.on_load(:action_cable) {
29
+ self.health_check_application = ->(env) { Rails::HealthController.action(:show).call(env) }
30
+ }
31
+ end
32
+
26
33
  initializer "action_cable.asset" do
27
34
  config.after_initialize do |app|
28
35
  if app.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
@@ -39,11 +46,12 @@ module ActionCable
39
46
 
40
47
  ActiveSupport.on_load(:action_cable) do
41
48
  if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
42
- self.cable = Rails.application.config_for(config_path).to_h.with_indifferent_access
49
+ self.cable = app.config_for(config_path).to_h.with_indifferent_access
43
50
  end
44
51
 
45
52
  previous_connection_class = connection_class
46
53
  self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
54
+ self.filter_parameters += app.config.filter_parameters
47
55
 
48
56
  options.each { |k, v| send("#{k}=", v) }
49
57
  end
@@ -63,7 +71,7 @@ module ActionCable
63
71
  initializer "action_cable.set_work_hooks" do |app|
64
72
  ActiveSupport.on_load(:action_cable) do
65
73
  ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
66
- app.executor.wrap do
74
+ app.executor.wrap(source: "application.action_cable") do
67
75
  # If we took a while to get the lock, we may have been halted
68
76
  # in the meantime. As we haven't started doing any real work
69
77
  # yet, we should pretend that we never made it off the queue.
@@ -74,7 +82,7 @@ module ActionCable
74
82
  end
75
83
 
76
84
  wrap = lambda do |_, inner|
77
- app.executor.wrap(&inner)
85
+ app.executor.wrap(source: "application.action_cable", &inner)
78
86
  end
79
87
  ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
80
88
  ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionCable
4
- # Returns the currently loaded version of Action Cable as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of Action Cable as a +Gem::Version+.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 7
11
- MINOR = 0
12
- TINY = 8
13
- PRE = "6"
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -3,6 +3,8 @@
3
3
  require "active_support/core_ext/module/redefine_method"
4
4
 
5
5
  module ActionCable
6
+ # = Action Cable Remote Connections
7
+ #
6
8
  # If you need to disconnect a given connection, you can go through the
7
9
  # RemoteConnections. You can find the connections you're looking for by
8
10
  # searching for the identifier declared on the connection. For example:
@@ -19,6 +21,11 @@ module ActionCable
19
21
  # This will disconnect all the connections established for
20
22
  # <tt>User.find(1)</tt>, across all servers running on all machines, because
21
23
  # it uses the internal channel that all of these servers are subscribed to.
24
+ #
25
+ # By default, server sends a "disconnect" message with "reconnect" flag set to true.
26
+ # You can override it by specifying the +reconnect+ option:
27
+ #
28
+ # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false)
22
29
  class RemoteConnections
23
30
  attr_reader :server
24
31
 
@@ -31,6 +38,8 @@ module ActionCable
31
38
  end
32
39
 
33
40
  private
41
+ # = Action Cable Remote \Connection
42
+ #
34
43
  # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
35
44
  # Exists solely for the purpose of calling #disconnect on that connection.
36
45
  class RemoteConnection
@@ -44,8 +53,8 @@ module ActionCable
44
53
  end
45
54
 
46
55
  # Uses the internal channel to disconnect the connection.
47
- def disconnect
48
- server.broadcast internal_channel, { type: "disconnect" }
56
+ def disconnect(reconnect: true)
57
+ server.broadcast internal_channel, { type: "disconnect", reconnect: reconnect }
49
58
  end
50
59
 
51
60
  # Returns all the identifiers that were applied to this connection.
@@ -4,6 +4,8 @@ require "monitor"
4
4
 
5
5
  module ActionCable
6
6
  module Server
7
+ # = Action Cable \Server \Base
8
+ #
7
9
  # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
8
10
  # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
9
11
  #
@@ -29,6 +31,7 @@ module ActionCable
29
31
 
30
32
  # Called by Rack to set up the server.
31
33
  def call(env)
34
+ return config.health_check_application.call(env) if env["PATH_INFO"] == config.health_check_path
32
35
  setup_heartbeat_timer
33
36
  config.connection_class.call.new(self, env).process
34
37
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Server
5
+ # = Action Cable \Server \Broadcasting
6
+ #
5
7
  # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
6
8
  # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
7
9
  #
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rack"
4
+
3
5
  module ActionCable
4
6
  module Server
7
+ # = Action Cable \Server \Configuration
8
+ #
5
9
  # 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.
10
+ # in a \Rails config initializer.
7
11
  class Configuration
8
12
  attr_accessor :logger, :log_tags
9
13
  attr_accessor :connection_class, :worker_pool_size
10
- attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
14
+ attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host, :filter_parameters
11
15
  attr_accessor :cable, :url, :mount_path
12
16
  attr_accessor :precompile_assets
17
+ attr_accessor :health_check_path, :health_check_application
13
18
 
14
19
  def initialize
15
20
  @log_tags = []
@@ -19,6 +24,11 @@ module ActionCable
19
24
 
20
25
  @disable_request_forgery_protection = false
21
26
  @allow_same_origin_as_host = true
27
+ @filter_parameters = []
28
+
29
+ @health_check_application = ->(env) {
30
+ [200, { Rack::CONTENT_TYPE => "text/html", "date" => Time.now.httpdate }, []]
31
+ }
22
32
  end
23
33
 
24
34
  # Returns constant of subscription adapter specified in config/cable.yml.
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Server
5
+ # = Action Cable \Server \Connections
6
+ #
5
7
  # 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
8
  # 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
9
  module Connections # :nodoc:
@@ -24,7 +26,7 @@ module ActionCable
24
26
  # disconnect.
25
27
  def setup_heartbeat_timer
26
28
  @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
27
- event_loop.post { connections.map(&:beat) }
29
+ event_loop.post { connections.each(&:beat) }
28
30
  end
29
31
  end
30
32
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "active_support/callbacks"
4
4
  require "active_support/core_ext/module/attribute_accessors_per_thread"
5
- require "action_cable/server/worker/active_record_connection_management"
6
5
  require "concurrent"
7
6
 
8
7
  module ActionCable
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_cable/subscription_adapter/inline"
4
-
5
3
  module ActionCable
6
4
  module SubscriptionAdapter
7
5
  class Async < Inline # :nodoc:
@@ -2,7 +2,6 @@
2
2
 
3
3
  gem "pg", "~> 1.1"
4
4
  require "pg"
5
- require "thread"
6
5
  require "openssl"
7
6
 
8
7
  module ActionCable
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
-
5
- gem "redis", ">= 3", "< 6"
3
+ gem "redis", ">= 4", "< 6"
6
4
  require "redis"
7
5
 
8
6
  require "active_support/core_ext/hash/except"
@@ -209,7 +207,7 @@ module ActionCable
209
207
  end
210
208
 
211
209
  if ::Redis::VERSION < "5"
212
- ConnectionError = ::Redis::ConnectionError
210
+ ConnectionError = ::Redis::BaseConnectionError
213
211
 
214
212
  class SubscribedClient
215
213
  def initialize(raw_client)
@@ -240,8 +238,7 @@ module ActionCable
240
238
  end
241
239
 
242
240
  def extract_subscribed_client(conn)
243
- raw_client = conn.respond_to?(:_client) ? conn._client : conn.client
244
- SubscribedClient.new(raw_client)
241
+ SubscribedClient.new(conn._client)
245
242
  end
246
243
  else
247
244
  ConnectionError = RedisClient::ConnectionError
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "async"
4
-
5
3
  module ActionCable
6
4
  module SubscriptionAdapter
7
- # == Test adapter for Action Cable
5
+ # == \Test adapter for Action Cable
8
6
  #
9
7
  # The test adapter should be used only in testing. Along with
10
- # ActionCable::TestHelper it makes a great tool to test your Rails application.
8
+ # ActionCable::TestHelper it makes a great tool to test your \Rails application.
11
9
  #
12
10
  # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
13
11
  #
14
- # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
12
+ # NOTE: +Test+ adapter extends the +ActionCable::SubscriptionAdapter::Async+ adapter,
15
13
  # so it could be used in system tests too.
16
14
  class Test < Async
17
15
  def broadcast(channel, payload)
@@ -29,30 +29,33 @@ module ActionCable
29
29
  # end
30
30
  #
31
31
  # If a block is passed, that block should cause the specified number of
32
- # messages to be broadcasted.
32
+ # messages to be broadcasted. It returns the messages that were broadcasted.
33
33
  #
34
34
  # def test_broadcasts_again
35
- # assert_broadcasts('messages', 1) do
35
+ # message = assert_broadcasts('messages', 1) do
36
36
  # ActionCable.server.broadcast 'messages', { text: 'hello' }
37
37
  # end
38
+ # assert_equal({ text: 'hello' }, message)
38
39
  #
39
- # assert_broadcasts('messages', 2) do
40
+ # messages = assert_broadcasts('messages', 2) do
40
41
  # ActionCable.server.broadcast 'messages', { text: 'hi' }
41
42
  # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
42
43
  # end
44
+ # assert_equal 2, messages.length
45
+ # assert_equal({ text: 'hi' }, messages.first)
46
+ # assert_equal({ text: 'how are you?' }, messages.last)
43
47
  # end
44
48
  #
45
49
  def assert_broadcasts(stream, number, &block)
46
50
  if block_given?
47
- original_count = broadcasts_size(stream)
48
- _assert_nothing_raised_or_warn("assert_broadcasts", &block)
49
- new_count = broadcasts_size(stream)
50
- actual_count = new_count - original_count
51
+ new_messages = new_broadcasts_from(broadcasts(stream), stream, "assert_broadcasts", &block)
52
+
53
+ actual_count = new_messages.size
54
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
51
55
  else
52
- actual_count = broadcasts_size(stream)
56
+ actual_count = broadcasts(stream).size
57
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
53
58
  end
54
-
55
- assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
56
59
  end
57
60
 
58
61
  # Asserts that no messages have been sent to the stream.
@@ -79,6 +82,22 @@ module ActionCable
79
82
  assert_broadcasts stream, 0, &block
80
83
  end
81
84
 
85
+ # Returns the messages that are broadcasted in the block.
86
+ #
87
+ # def test_broadcasts
88
+ # messages = capture_broadcasts('messages') do
89
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
90
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
91
+ # end
92
+ # assert_equal 2, messages.length
93
+ # assert_equal({ text: 'hi' }, messages.first)
94
+ # assert_equal({ text: 'how are you?' }, messages.last)
95
+ # end
96
+ #
97
+ def capture_broadcasts(stream, &block)
98
+ new_broadcasts_from(broadcasts(stream), stream, "capture_broadcasts", &block).map { |m| ActiveSupport::JSON.decode(m) }
99
+ end
100
+
82
101
  # Asserts that the specified message has been sent to the stream.
83
102
  #
84
103
  # def test_assert_transmitted_message
@@ -103,20 +122,22 @@ module ActionCable
103
122
 
104
123
  new_messages = broadcasts(stream)
105
124
  if block_given?
106
- old_messages = new_messages
107
- clear_messages(stream)
108
-
109
- _assert_nothing_raised_or_warn("assert_broadcast_on", &block)
110
- new_messages = broadcasts(stream)
111
- clear_messages(stream)
112
-
113
- # Restore all sent messages
114
- (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
125
+ new_messages = new_broadcasts_from(new_messages, stream, "assert_broadcast_on", &block)
115
126
  end
116
127
 
117
128
  message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
118
129
 
119
- assert message, "No messages sent with #{data} to #{stream}"
130
+ error_message = "No messages sent with #{data} to #{stream}"
131
+
132
+ if new_messages.any?
133
+ error_message = new_messages.inject("#{error_message}\nMessage(s) found:\n") do |error_message, new_message|
134
+ error_message + "#{ActiveSupport::JSON.decode(new_message)}\n"
135
+ end
136
+ else
137
+ error_message = "#{error_message}\nNo message found for #{stream}"
138
+ end
139
+
140
+ assert message, error_message
120
141
  end
121
142
 
122
143
  def pubsub_adapter # :nodoc:
@@ -126,8 +147,18 @@ module ActionCable
126
147
  delegate :broadcasts, :clear_messages, to: :pubsub_adapter
127
148
 
128
149
  private
129
- def broadcasts_size(channel)
130
- broadcasts(channel).size
150
+ def new_broadcasts_from(current_messages, stream, assertion, &block)
151
+ old_messages = current_messages
152
+ clear_messages(stream)
153
+
154
+ _assert_nothing_raised_or_warn(assertion, &block)
155
+ new_messages = broadcasts(stream)
156
+ clear_messages(stream)
157
+
158
+ # Restore all sent messages
159
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
160
+
161
+ new_messages
131
162
  end
132
163
  end
133
164
  end