actioncable 5.2.6.3 → 6.1.5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -39
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +3 -546
  5. data/app/assets/javascripts/action_cable.js +574 -0
  6. data/lib/action_cable/channel/base.rb +10 -4
  7. data/lib/action_cable/channel/broadcasting.rb +18 -8
  8. data/lib/action_cable/channel/naming.rb +1 -1
  9. data/lib/action_cable/channel/streams.rb +30 -4
  10. data/lib/action_cable/channel/test_case.rb +310 -0
  11. data/lib/action_cable/channel.rb +1 -0
  12. data/lib/action_cable/connection/authorization.rb +1 -1
  13. data/lib/action_cable/connection/base.rb +13 -7
  14. data/lib/action_cable/connection/message_buffer.rb +1 -4
  15. data/lib/action_cable/connection/stream.rb +4 -2
  16. data/lib/action_cable/connection/subscriptions.rb +2 -5
  17. data/lib/action_cable/connection/test_case.rb +234 -0
  18. data/lib/action_cable/connection/web_socket.rb +1 -3
  19. data/lib/action_cable/connection.rb +1 -0
  20. data/lib/action_cable/engine.rb +1 -1
  21. data/lib/action_cable/gem_version.rb +4 -4
  22. data/lib/action_cable/helpers/action_cable_helper.rb +3 -3
  23. data/lib/action_cable/remote_connections.rb +1 -1
  24. data/lib/action_cable/server/base.rb +9 -4
  25. data/lib/action_cable/server/broadcasting.rb +1 -1
  26. data/lib/action_cable/server/worker.rb +6 -8
  27. data/lib/action_cable/server.rb +0 -1
  28. data/lib/action_cable/subscription_adapter/base.rb +4 -0
  29. data/lib/action_cable/subscription_adapter/postgresql.rb +28 -9
  30. data/lib/action_cable/subscription_adapter/redis.rb +4 -2
  31. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  32. data/lib/action_cable/subscription_adapter.rb +1 -0
  33. data/lib/action_cable/test_case.rb +11 -0
  34. data/lib/action_cable/test_helper.rb +133 -0
  35. data/lib/action_cable.rb +15 -7
  36. data/lib/rails/generators/channel/USAGE +5 -6
  37. data/lib/rails/generators/channel/channel_generator.rb +6 -3
  38. data/lib/rails/generators/channel/templates/{assets → javascript}/channel.js.tt +6 -4
  39. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  40. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  41. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  42. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  43. metadata +41 -16
  44. data/lib/assets/compiled/action_cable.js +0 -601
  45. data/lib/rails/generators/channel/templates/assets/cable.js.tt +0 -13
  46. data/lib/rails/generators/channel/templates/assets/channel.coffee.tt +0 -14
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/test_case"
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+ require "action_dispatch"
7
+ require "action_dispatch/http/headers"
8
+ require "action_dispatch/testing/test_request"
9
+
10
+ module ActionCable
11
+ module Connection
12
+ class NonInferrableConnectionError < ::StandardError
13
+ def initialize(name)
14
+ super "Unable to determine the connection to test from #{name}. " +
15
+ "You'll need to specify it using `tests YourConnection` in your " +
16
+ "test case definition."
17
+ end
18
+ end
19
+
20
+ module Assertions
21
+ # Asserts that the connection is rejected (via +reject_unauthorized_connection+).
22
+ #
23
+ # # Asserts that connection without user_id fails
24
+ # assert_reject_connection { connect params: { user_id: '' } }
25
+ def assert_reject_connection(&block)
26
+ assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
27
+ end
28
+ end
29
+
30
+ # We don't want to use the whole "encryption stack" for connection
31
+ # unit-tests, but we want to make sure that users test against the correct types
32
+ # of cookies (i.e. signed or encrypted or plain)
33
+ class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
34
+ def signed
35
+ self[:signed] ||= {}.with_indifferent_access
36
+ end
37
+
38
+ def encrypted
39
+ self[:encrypted] ||= {}.with_indifferent_access
40
+ end
41
+ end
42
+
43
+ class TestRequest < ActionDispatch::TestRequest
44
+ attr_accessor :session, :cookie_jar
45
+ end
46
+
47
+ module TestConnection
48
+ attr_reader :logger, :request
49
+
50
+ def initialize(request)
51
+ inner_logger = ActiveSupport::Logger.new(StringIO.new)
52
+ tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
53
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
54
+ @request = request
55
+ @env = request.env
56
+ end
57
+ end
58
+
59
+ # Unit test Action Cable connections.
60
+ #
61
+ # Useful to check whether a connection's +identified_by+ gets assigned properly
62
+ # and that any improper connection requests are rejected.
63
+ #
64
+ # == Basic example
65
+ #
66
+ # Unit tests are written as follows:
67
+ #
68
+ # 1. Simulate a connection attempt by calling +connect+.
69
+ # 2. Assert state, e.g. identifiers, has been assigned.
70
+ #
71
+ #
72
+ # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
73
+ # def test_connects_with_proper_cookie
74
+ # # Simulate the connection request with a cookie.
75
+ # cookies["user_id"] = users(:john).id
76
+ #
77
+ # connect
78
+ #
79
+ # # Assert the connection identifier matches the fixture.
80
+ # assert_equal users(:john).id, connection.user.id
81
+ # end
82
+ #
83
+ # def test_rejects_connection_without_proper_cookie
84
+ # assert_reject_connection { connect }
85
+ # end
86
+ # end
87
+ #
88
+ # +connect+ accepts additional information about the HTTP request with the
89
+ # +params+, +headers+, +session+ and Rack +env+ options.
90
+ #
91
+ # def test_connect_with_headers_and_query_string
92
+ # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
93
+ #
94
+ # assert_equal "1", connection.user.id
95
+ # assert_equal "secret-my", connection.token
96
+ # end
97
+ #
98
+ # def test_connect_with_params
99
+ # connect params: { user_id: 1 }
100
+ #
101
+ # assert_equal "1", connection.user.id
102
+ # end
103
+ #
104
+ # You can also set up the correct cookies before the connection request:
105
+ #
106
+ # def test_connect_with_cookies
107
+ # # Plain cookies:
108
+ # cookies["user_id"] = 1
109
+ #
110
+ # # Or signed/encrypted:
111
+ # # cookies.signed["user_id"] = 1
112
+ # # cookies.encrypted["user_id"] = 1
113
+ #
114
+ # connect
115
+ #
116
+ # assert_equal "1", connection.user_id
117
+ # end
118
+ #
119
+ # == Connection is automatically inferred
120
+ #
121
+ # ActionCable::Connection::TestCase will automatically infer the connection under test
122
+ # from the test class name. If the channel cannot be inferred from the test
123
+ # class name, you can explicitly set it with +tests+.
124
+ #
125
+ # class ConnectionTest < ActionCable::Connection::TestCase
126
+ # tests ApplicationCable::Connection
127
+ # end
128
+ #
129
+ class TestCase < ActiveSupport::TestCase
130
+ module Behavior
131
+ extend ActiveSupport::Concern
132
+
133
+ DEFAULT_PATH = "/cable"
134
+
135
+ include ActiveSupport::Testing::ConstantLookup
136
+ include Assertions
137
+
138
+ included do
139
+ class_attribute :_connection_class
140
+
141
+ attr_reader :connection
142
+
143
+ ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
144
+ end
145
+
146
+ module ClassMethods
147
+ def tests(connection)
148
+ case connection
149
+ when String, Symbol
150
+ self._connection_class = connection.to_s.camelize.constantize
151
+ when Module
152
+ self._connection_class = connection
153
+ else
154
+ raise NonInferrableConnectionError.new(connection)
155
+ end
156
+ end
157
+
158
+ def connection_class
159
+ if connection = self._connection_class
160
+ connection
161
+ else
162
+ tests determine_default_connection(name)
163
+ end
164
+ end
165
+
166
+ def determine_default_connection(name)
167
+ connection = determine_constant_from_test_name(name) do |constant|
168
+ Class === constant && constant < ActionCable::Connection::Base
169
+ end
170
+ raise NonInferrableConnectionError.new(name) if connection.nil?
171
+ connection
172
+ end
173
+ end
174
+
175
+ # Performs connection attempt to exert #connect on the connection under test.
176
+ #
177
+ # Accepts request path as the first argument and the following request options:
178
+ #
179
+ # - params – URL parameters (Hash)
180
+ # - headers – request headers (Hash)
181
+ # - session – session data (Hash)
182
+ # - env – additional Rack env configuration (Hash)
183
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
184
+ path ||= DEFAULT_PATH
185
+
186
+ connection = self.class.connection_class.allocate
187
+ connection.singleton_class.include(TestConnection)
188
+ connection.send(:initialize, build_test_request(path, **request_params))
189
+ connection.connect if connection.respond_to?(:connect)
190
+
191
+ # Only set instance variable if connected successfully
192
+ @connection = connection
193
+ end
194
+
195
+ # Exert #disconnect on the connection under test.
196
+ def disconnect
197
+ raise "Must be connected!" if connection.nil?
198
+
199
+ connection.disconnect if connection.respond_to?(:disconnect)
200
+ @connection = nil
201
+ end
202
+
203
+ def cookies
204
+ @cookie_jar ||= TestCookieJar.new
205
+ end
206
+
207
+ private
208
+ def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
209
+ wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
210
+
211
+ uri = URI.parse(path)
212
+
213
+ query_string = params.nil? ? uri.query : params.to_query
214
+
215
+ request_env = {
216
+ "QUERY_STRING" => query_string,
217
+ "PATH_INFO" => uri.path
218
+ }.merge(env)
219
+
220
+ if wrapped_headers.present?
221
+ ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
222
+ end
223
+
224
+ TestRequest.create(request_env).tap do |request|
225
+ request.session = session.with_indifferent_access
226
+ request.cookie_jar = cookies
227
+ end
228
+ end
229
+ end
230
+
231
+ include Behavior
232
+ end
233
+ end
234
+ end
@@ -34,9 +34,7 @@ module ActionCable
34
34
  websocket.rack_response
35
35
  end
36
36
 
37
- # TODO Change this to private once we've dropped Ruby 2.2 support.
38
- # Workaround for Ruby 2.2 "private attribute?" warning.
39
- protected
37
+ private
40
38
  attr_reader :websocket
41
39
  end
42
40
  end
@@ -15,6 +15,7 @@ module ActionCable
15
15
  autoload :StreamEventLoop
16
16
  autoload :Subscriptions
17
17
  autoload :TaggedLoggerProxy
18
+ autoload :TestCase
18
19
  autoload :WebSocket
19
20
  end
20
21
  end
@@ -30,7 +30,7 @@ module ActionCable
30
30
 
31
31
  ActiveSupport.on_load(:action_cable) do
32
32
  if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
33
- self.cable = Rails.application.config_for(config_path).with_indifferent_access
33
+ self.cable = Rails.application.config_for(config_path).to_h.with_indifferent_access
34
34
  end
35
35
 
36
36
  previous_connection_class = connection_class
@@ -7,10 +7,10 @@ module ActionCable
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 6
13
- PRE = "3"
10
+ MAJOR = 6
11
+ MINOR = 1
12
+ TINY = 5
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -12,11 +12,11 @@ module ActionCable
12
12
  # </head>
13
13
  #
14
14
  # This is then used by Action Cable to determine the URL of your WebSocket server.
15
- # Your CoffeeScript can then connect to the server without needing to specify the
15
+ # Your JavaScript can then connect to the server without needing to specify the
16
16
  # URL directly:
17
17
  #
18
- # #= require cable
19
- # @App = {}
18
+ # window.Cable = require("@rails/actioncable")
19
+ # window.App = {}
20
20
  # App.cable = Cable.createConsumer()
21
21
  #
22
22
  # Make sure to specify the correct server location in each of your environment
@@ -45,7 +45,7 @@ module ActionCable
45
45
 
46
46
  # Uses the internal channel to disconnect the connection.
47
47
  def disconnect
48
- server.broadcast internal_channel, type: "disconnect"
48
+ server.broadcast internal_channel, { type: "disconnect" }
49
49
  end
50
50
 
51
51
  # Returns all the identifiers that were applied to this connection.
@@ -12,19 +12,22 @@ module ActionCable
12
12
  include ActionCable::Server::Broadcasting
13
13
  include ActionCable::Server::Connections
14
14
 
15
- cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new
15
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
16
+
17
+ attr_reader :config
16
18
 
17
19
  def self.logger; config.logger; end
18
20
  delegate :logger, to: :config
19
21
 
20
22
  attr_reader :mutex
21
23
 
22
- def initialize
24
+ def initialize(config: self.class.config)
25
+ @config = config
23
26
  @mutex = Monitor.new
24
27
  @remote_connections = @event_loop = @worker_pool = @pubsub = nil
25
28
  end
26
29
 
27
- # Called by Rack to setup the server.
30
+ # Called by Rack to set up the server.
28
31
  def call(env)
29
32
  setup_heartbeat_timer
30
33
  config.connection_class.call.new(self, env).process
@@ -36,7 +39,9 @@ module ActionCable
36
39
  end
37
40
 
38
41
  def restart
39
- connections.each(&:close)
42
+ connections.each do |connection|
43
+ connection.close(reason: ActionCable::INTERNAL[:disconnect_reasons][:server_restart])
44
+ end
40
45
 
41
46
  @mutex.synchronize do
42
47
  # Shutdown the worker pool
@@ -40,7 +40,7 @@ module ActionCable
40
40
  end
41
41
 
42
42
  def broadcast(message)
43
- server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
43
+ server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" }
44
44
 
45
45
  payload = { broadcasting: broadcasting, message: message, coder: coder }
46
46
  ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
@@ -2,6 +2,7 @@
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"
5
6
  require "concurrent"
6
7
 
7
8
  module ActionCable
@@ -56,19 +57,16 @@ module ActionCable
56
57
 
57
58
  def invoke(receiver, method, *args, connection:, &block)
58
59
  work(connection) do
59
- begin
60
- receiver.send method, *args, &block
61
- rescue Exception => e
62
- logger.error "There was an exception - #{e.class}(#{e.message})"
63
- logger.error e.backtrace.join("\n")
60
+ receiver.send method, *args, &block
61
+ rescue Exception => e
62
+ logger.error "There was an exception - #{e.class}(#{e.message})"
63
+ logger.error e.backtrace.join("\n")
64
64
 
65
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
66
- end
65
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
67
66
  end
68
67
  end
69
68
 
70
69
  private
71
-
72
70
  def logger
73
71
  ActionCable.server.logger
74
72
  end
@@ -11,7 +11,6 @@ module ActionCable
11
11
  autoload :Configuration
12
12
 
13
13
  autoload :Worker
14
- autoload :ActiveRecordConnectionManagement, "action_cable/server/worker/active_record_connection_management"
15
14
  end
16
15
  end
17
16
  end
@@ -25,6 +25,10 @@ module ActionCable
25
25
  def shutdown
26
26
  raise NotImplementedError
27
27
  end
28
+
29
+ def identifier
30
+ @server.config.cable[:id] ||= "ActionCable-PID-#{$$}"
31
+ end
28
32
  end
29
33
  end
30
34
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "pg", ">= 0.18", "< 2.0"
3
+ gem "pg", "~> 1.1"
4
4
  require "pg"
5
5
  require "thread"
6
6
  require "digest/sha1"
@@ -8,13 +8,15 @@ require "digest/sha1"
8
8
  module ActionCable
9
9
  module SubscriptionAdapter
10
10
  class PostgreSQL < Base # :nodoc:
11
+ prepend ChannelPrefix
12
+
11
13
  def initialize(*)
12
14
  super
13
15
  @listener = nil
14
16
  end
15
17
 
16
18
  def broadcast(channel, payload)
17
- with_connection do |pg_conn|
19
+ with_broadcast_connection do |pg_conn|
18
20
  pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
19
21
  end
20
22
  end
@@ -31,14 +33,25 @@ module ActionCable
31
33
  listener.shutdown
32
34
  end
33
35
 
34
- def with_connection(&block) # :nodoc:
35
- ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
36
- pg_conn = ar_conn.raw_connection
36
+ def with_subscriptions_connection(&block) # :nodoc:
37
+ 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
40
+ ActiveRecord::Base.connection_pool.remove(conn)
41
+ end
42
+ pg_conn = ar_conn.raw_connection
37
43
 
38
- unless pg_conn.is_a?(PG::Connection)
39
- raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
40
- end
44
+ verify!(pg_conn)
45
+ pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(identifier)}")
46
+ yield pg_conn
47
+ ensure
48
+ ar_conn.disconnect!
49
+ end
41
50
 
51
+ def with_broadcast_connection(&block) # :nodoc:
52
+ ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
53
+ pg_conn = ar_conn.raw_connection
54
+ verify!(pg_conn)
42
55
  yield pg_conn
43
56
  end
44
57
  end
@@ -52,6 +65,12 @@ module ActionCable
52
65
  @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
53
66
  end
54
67
 
68
+ def verify!(pg_conn)
69
+ unless pg_conn.is_a?(PG::Connection)
70
+ raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
71
+ end
72
+ end
73
+
55
74
  class Listener < SubscriberMap
56
75
  def initialize(adapter, event_loop)
57
76
  super()
@@ -67,7 +86,7 @@ module ActionCable
67
86
  end
68
87
 
69
88
  def listen
70
- @adapter.with_connection do |pg_conn|
89
+ @adapter.with_subscriptions_connection do |pg_conn|
71
90
  catch :shutdown do
72
91
  loop do
73
92
  until @queue.empty?
@@ -5,6 +5,8 @@ require "thread"
5
5
  gem "redis", ">= 3", "< 5"
6
6
  require "redis"
7
7
 
8
+ require "active_support/core_ext/hash/except"
9
+
8
10
  module ActionCable
9
11
  module SubscriptionAdapter
10
12
  class Redis < Base # :nodoc:
@@ -13,7 +15,7 @@ module ActionCable
13
15
  # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
14
16
  # This is needed, for example, when using Makara proxies for distributed Redis.
15
17
  cattr_accessor :redis_connector, default: ->(config) do
16
- ::Redis.new(config.slice(:url, :host, :port, :db, :password))
18
+ ::Redis.new(config.except(:adapter, :channel_prefix))
17
19
  end
18
20
 
19
21
  def initialize(*)
@@ -54,7 +56,7 @@ module ActionCable
54
56
  end
55
57
 
56
58
  def redis_connection
57
- self.class.redis_connector.call(@server.config.cable)
59
+ self.class.redis_connector.call(@server.config.cable.merge(id: identifier))
58
60
  end
59
61
 
60
62
  class Listener < SubscriberMap
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "async"
4
+
5
+ module ActionCable
6
+ module SubscriptionAdapter
7
+ # == Test adapter for Action Cable
8
+ #
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.
11
+ #
12
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
13
+ #
14
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
15
+ # so it could be used in system tests too.
16
+ class Test < Async
17
+ def broadcast(channel, payload)
18
+ broadcasts(channel) << payload
19
+ super
20
+ end
21
+
22
+ def broadcasts(channel)
23
+ channels_data[channel] ||= []
24
+ end
25
+
26
+ def clear_messages(channel)
27
+ channels_data[channel] = []
28
+ end
29
+
30
+ def clear
31
+ @channels_data = nil
32
+ end
33
+
34
+ private
35
+ def channels_data
36
+ @channels_data ||= {}
37
+ end
38
+ end
39
+ end
40
+ end
@@ -5,6 +5,7 @@ module ActionCable
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  autoload :Base
8
+ autoload :Test
8
9
  autoload :SubscriberMap
9
10
  autoload :ChannelPrefix
10
11
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+
5
+ module ActionCable
6
+ class TestCase < ActiveSupport::TestCase
7
+ include ActionCable::TestHelper
8
+
9
+ ActiveSupport.run_load_hooks(:action_cable_test_case, self)
10
+ end
11
+ end