actioncable 5.0.1 → 6.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +31 -117
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +4 -535
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +20 -10
  7. data/lib/action_cable/channel.rb +3 -0
  8. data/lib/action_cable/channel/base.rb +31 -23
  9. data/lib/action_cable/channel/broadcasting.rb +22 -10
  10. data/lib/action_cable/channel/callbacks.rb +4 -2
  11. data/lib/action_cable/channel/naming.rb +5 -2
  12. data/lib/action_cable/channel/periodic_timers.rb +4 -3
  13. data/lib/action_cable/channel/streams.rb +39 -11
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +3 -2
  16. data/lib/action_cable/connection/authorization.rb +8 -6
  17. data/lib/action_cable/connection/base.rb +34 -26
  18. data/lib/action_cable/connection/client_socket.rb +20 -18
  19. data/lib/action_cable/connection/identification.rb +5 -4
  20. data/lib/action_cable/connection/internal_channel.rb +4 -2
  21. data/lib/action_cable/connection/message_buffer.rb +3 -2
  22. data/lib/action_cable/connection/stream.rb +9 -5
  23. data/lib/action_cable/connection/stream_event_loop.rb +4 -2
  24. data/lib/action_cable/connection/subscriptions.rb +14 -13
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +4 -2
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +7 -5
  28. data/lib/action_cable/engine.rb +7 -5
  29. data/lib/action_cable/gem_version.rb +5 -3
  30. data/lib/action_cable/helpers/action_cable_helper.rb +6 -4
  31. data/lib/action_cable/remote_connections.rb +9 -4
  32. data/lib/action_cable/server.rb +2 -1
  33. data/lib/action_cable/server/base.rb +17 -10
  34. data/lib/action_cable/server/broadcasting.rb +9 -3
  35. data/lib/action_cable/server/configuration.rb +21 -22
  36. data/lib/action_cable/server/connections.rb +2 -0
  37. data/lib/action_cable/server/worker.rb +11 -11
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -0
  39. data/lib/action_cable/subscription_adapter.rb +4 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +3 -1
  41. data/lib/action_cable/subscription_adapter/base.rb +6 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +2 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +40 -14
  45. data/lib/action_cable/subscription_adapter/redis.rb +19 -11
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +3 -1
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +3 -1
  51. data/lib/rails/generators/channel/USAGE +5 -6
  52. data/lib/rails/generators/channel/channel_generator.rb +16 -11
  53. data/lib/rails/generators/channel/templates/application_cable/{channel.rb → channel.rb.tt} +0 -0
  54. data/lib/rails/generators/channel/templates/application_cable/{connection.rb → connection.rb.tt} +0 -0
  55. data/lib/rails/generators/channel/templates/{channel.rb → channel.rb.tt} +0 -0
  56. data/lib/rails/generators/channel/templates/{assets/channel.js → javascript/channel.js.tt} +6 -4
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +46 -38
  62. data/lib/action_cable/connection/faye_client_socket.rb +0 -48
  63. data/lib/action_cable/connection/faye_event_loop.rb +0 -44
  64. data/lib/action_cable/subscription_adapter/evented_redis.rb +0 -79
  65. data/lib/assets/compiled/action_cable.js +0 -597
  66. data/lib/rails/generators/channel/templates/assets/cable.js +0 -13
  67. data/lib/rails/generators/channel/templates/assets/channel.coffee +0 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
@@ -31,8 +33,8 @@ module ActionCable
31
33
  end
32
34
  end
33
35
 
34
- protected
35
- def log(type, message)
36
+ private
37
+ def log(type, message) # :doc:
36
38
  tag(@logger) { @logger.send type, message }
37
39
  end
38
40
  end
@@ -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
@@ -1,11 +1,13 @@
1
- require 'websocket/driver'
1
+ # frozen_string_literal: true
2
+
3
+ require "websocket/driver"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
5
7
  # Wrap the real socket to minimize the externally-presented API
6
- class WebSocket
7
- def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols])
8
- @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil
8
+ class WebSocket # :nodoc:
9
+ def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
10
+ @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil
9
11
  end
10
12
 
11
13
  def possible?
@@ -32,7 +34,7 @@ module ActionCable
32
34
  websocket.rack_response
33
35
  end
34
36
 
35
- protected
37
+ private
36
38
  attr_reader :websocket
37
39
  end
38
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails"
2
4
  require "action_cable"
3
5
  require "action_cable/helpers/action_cable_helper"
@@ -22,19 +24,19 @@ module ActionCable
22
24
 
23
25
  initializer "action_cable.set_configs" do |app|
24
26
  options = app.config.action_cable
25
- options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
27
+ options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
26
28
 
27
29
  app.paths.add "config/cable", with: "config/cable.yml"
28
30
 
29
31
  ActiveSupport.on_load(:action_cable) do
30
32
  if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
31
- 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
32
34
  end
33
35
 
34
- previous_connection_class = self.connection_class
35
- self.connection_class = -> { 'ApplicationCable::Connection'.safe_constantize || previous_connection_class.call }
36
+ previous_connection_class = connection_class
37
+ self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
36
38
 
37
- options.each { |k,v| send("#{k}=", v) }
39
+ options.each { |k, v| send("#{k}=", v) }
38
40
  end
39
41
  end
40
42
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>.
3
5
  def self.gem_version
@@ -5,9 +7,9 @@ module ActionCable
5
7
  end
6
8
 
7
9
  module VERSION
8
- MAJOR = 5
9
- MINOR = 0
10
- TINY = 1
10
+ MAJOR = 6
11
+ MINOR = 1
12
+ TINY = 3
11
13
  PRE = nil
12
14
 
13
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Helpers
3
5
  module ActionCableHelper
@@ -6,15 +8,15 @@ module ActionCable
6
8
  #
7
9
  # <head>
8
10
  # <%= action_cable_meta_tag %>
9
- # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
11
+ # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
10
12
  # </head>
11
13
  #
12
14
  # This is then used by Action Cable to determine the URL of your WebSocket server.
13
- # 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
14
16
  # URL directly:
15
17
  #
16
- # #= require cable
17
- # @App = {}
18
+ # window.Cable = require("@rails/actioncable")
19
+ # window.App = {}
18
20
  # App.cable = Cable.createConsumer()
19
21
  #
20
22
  # Make sure to specify the correct server location in each of your environment
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/redefine_method"
4
+
1
5
  module ActionCable
2
6
  # If you need to disconnect a given connection, you can go through the
3
7
  # RemoteConnections. You can find the connections you're looking for by
@@ -41,20 +45,21 @@ module ActionCable
41
45
 
42
46
  # Uses the internal channel to disconnect the connection.
43
47
  def disconnect
44
- server.broadcast internal_channel, type: 'disconnect'
48
+ server.broadcast internal_channel, type: "disconnect"
45
49
  end
46
50
 
47
51
  # Returns all the identifiers that were applied to this connection.
48
- def identifiers
52
+ redefine_method :identifiers do
49
53
  server.connection_identifiers
50
54
  end
51
55
 
52
- private
56
+ protected
53
57
  attr_reader :server
54
58
 
59
+ private
55
60
  def set_identifier_instance_vars(ids)
56
61
  raise InvalidIdentifiersError unless valid_identifiers?(ids)
57
- ids.each { |k,v| instance_variable_set("@#{k}", v) }
62
+ ids.each { |k, v| instance_variable_set("@#{k}", v) }
58
63
  end
59
64
 
60
65
  def valid_identifiers?(ids)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Server
3
5
  extend ActiveSupport::Autoload
@@ -9,7 +11,6 @@ module ActionCable
9
11
  autoload :Configuration
10
12
 
11
13
  autoload :Worker
12
- autoload :ActiveRecordConnectionManagement, 'action_cable/server/worker/active_record_connection_management'
13
14
  end
14
15
  end
15
16
  end
@@ -1,4 +1,6 @@
1
- require 'monitor'
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
2
4
 
3
5
  module ActionCable
4
6
  module Server
@@ -10,31 +12,36 @@ module ActionCable
10
12
  include ActionCable::Server::Broadcasting
11
13
  include ActionCable::Server::Connections
12
14
 
13
- cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new }
15
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
16
+
17
+ attr_reader :config
14
18
 
15
19
  def self.logger; config.logger; end
16
20
  delegate :logger, to: :config
17
21
 
18
22
  attr_reader :mutex
19
23
 
20
- def initialize
24
+ def initialize(config: self.class.config)
25
+ @config = config
21
26
  @mutex = Monitor.new
22
27
  @remote_connections = @event_loop = @worker_pool = @pubsub = nil
23
28
  end
24
29
 
25
- # Called by Rack to setup the server.
30
+ # Called by Rack to set up the server.
26
31
  def call(env)
27
32
  setup_heartbeat_timer
28
33
  config.connection_class.call.new(self, env).process
29
34
  end
30
35
 
31
- # Disconnect all the connections identified by `identifiers` on this server or any others via RemoteConnections.
36
+ # Disconnect all the connections identified by +identifiers+ on this server or any others via RemoteConnections.
32
37
  def disconnect(identifiers)
33
38
  remote_connections.where(identifiers).disconnect
34
39
  end
35
40
 
36
41
  def restart
37
- connections.each(&:close)
42
+ connections.each do |connection|
43
+ connection.close(reason: ActionCable::INTERNAL[:disconnect_reasons][:server_restart])
44
+ end
38
45
 
39
46
  @mutex.synchronize do
40
47
  # Shutdown the worker pool
@@ -53,20 +60,20 @@ module ActionCable
53
60
  end
54
61
 
55
62
  def event_loop
56
- @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
63
+ @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
57
64
  end
58
65
 
59
66
  # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
60
67
  # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
61
- # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size.
68
+ # at 4 worker threads by default. Tune the size yourself with <tt>config.action_cable.worker_pool_size</tt>.
62
69
  #
63
70
  # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
64
71
  # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
65
72
  # connections.
66
73
  #
67
74
  # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
68
- # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
69
- # db connection pool instead.
75
+ # the database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
76
+ # database connection pool instead.
70
77
  def worker_pool
71
78
  @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
72
79
  end