actioncable 5.0.1 → 6.1.3

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 (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
  extend ActiveSupport::Autoload
@@ -8,13 +10,12 @@ module ActionCable
8
10
  autoload :ClientSocket
9
11
  autoload :Identification
10
12
  autoload :InternalChannel
11
- autoload :FayeClientSocket
12
- autoload :FayeEventLoop
13
13
  autoload :MessageBuffer
14
14
  autoload :Stream
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
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  module Authorization
4
6
  class UnauthorizedError < StandardError; end
5
7
 
6
- private
7
- def reject_unauthorized_connection
8
- logger.error "An unauthorized connection attempt was rejected"
9
- raise UnauthorizedError
10
- end
8
+ # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
9
+ def reject_unauthorized_connection
10
+ logger.error "An unauthorized connection attempt was rejected"
11
+ raise UnauthorizedError
12
+ end
11
13
  end
12
14
  end
13
- end
15
+ end
@@ -1,8 +1,11 @@
1
- require 'action_dispatch'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+ require "active_support/rescuable"
2
5
 
3
6
  module ActionCable
4
7
  module Connection
5
- # For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
8
+ # For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
6
9
  # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
7
10
  # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
8
11
  # authentication and authorization.
@@ -22,13 +25,10 @@ module ActionCable
22
25
  # # Any cleanup work needed when the cable connection is cut.
23
26
  # end
24
27
  #
25
- # protected
28
+ # private
26
29
  # def find_verified_user
27
- # if current_user = User.find_by_identity cookies.signed[:identity_id]
28
- # current_user
29
- # else
30
+ # User.find_by_identity(cookies.encrypted[:identity_id]) ||
30
31
  # reject_unauthorized_connection
31
- # end
32
32
  # end
33
33
  # end
34
34
  # end
@@ -47,6 +47,7 @@ module ActionCable
47
47
  include Identification
48
48
  include InternalChannel
49
49
  include Authorization
50
+ include ActiveSupport::Rescuable
50
51
 
51
52
  attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
52
53
  delegate :event_loop, :pubsub, to: :server
@@ -57,7 +58,7 @@ module ActionCable
57
58
  @worker_pool = server.worker_pool
58
59
  @logger = new_tagged_logger
59
60
 
60
- @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
61
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
61
62
  @subscriptions = ActionCable::Connection::Subscriptions.new(self)
62
63
  @message_buffer = ActionCable::Connection::MessageBuffer.new(self)
63
64
 
@@ -96,7 +97,12 @@ module ActionCable
96
97
  end
97
98
 
98
99
  # Close the WebSocket connection.
99
- def close
100
+ def close(reason: nil, reconnect: true)
101
+ transmit(
102
+ type: ActionCable::INTERNAL[:message_types][:disconnect],
103
+ reason: reason,
104
+ reconnect: reconnect
105
+ )
100
106
  websocket.close
101
107
  end
102
108
 
@@ -105,14 +111,14 @@ module ActionCable
105
111
  worker_pool.async_invoke(self, method, *arguments)
106
112
  end
107
113
 
108
- # Return a basic hash of statistics for the connection keyed with `identifier`, `started_at`, and `subscriptions`.
114
+ # Return a basic hash of statistics for the connection keyed with <tt>identifier</tt>, <tt>started_at</tt>, <tt>subscriptions</tt>, and <tt>request_id</tt>.
109
115
  # This can be returned by a health check against the connection.
110
116
  def statistics
111
117
  {
112
118
  identifier: connection_identifier,
113
119
  started_at: @started_at,
114
120
  subscriptions: subscriptions.identifiers,
115
- request_id: @env['action_dispatch.request_id']
121
+ request_id: @env["action_dispatch.request_id"]
116
122
  }
117
123
  end
118
124
 
@@ -129,16 +135,20 @@ module ActionCable
129
135
  end
130
136
 
131
137
  def on_error(message) # :nodoc:
132
- # ignore
138
+ # log errors to make diagnosing socket errors easier
139
+ logger.error "WebSocket error occurred: #{message}"
133
140
  end
134
141
 
135
142
  def on_close(reason, code) # :nodoc:
136
143
  send_async :handle_close
137
144
  end
138
145
 
139
- protected
146
+ private
147
+ attr_reader :websocket
148
+ attr_reader :message_buffer
149
+
140
150
  # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
141
- def request
151
+ def request # :doc:
142
152
  @request ||= begin
143
153
  environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
144
154
  ActionDispatch::Request.new(environment || env)
@@ -146,14 +156,10 @@ module ActionCable
146
156
  end
147
157
 
148
158
  # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
149
- def cookies
159
+ def cookies # :doc:
150
160
  request.cookie_jar
151
161
  end
152
162
 
153
- attr_reader :websocket
154
- attr_reader :message_buffer
155
-
156
- private
157
163
  def encode(cable_message)
158
164
  @coder.encode cable_message
159
165
  end
@@ -171,7 +177,7 @@ module ActionCable
171
177
  message_buffer.process!
172
178
  server.add_connection(self)
173
179
  rescue ActionCable::Connection::Authorization::UnauthorizedError
174
- respond_to_invalid_request
180
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
175
181
  end
176
182
 
177
183
  def handle_close
@@ -212,11 +218,11 @@ module ActionCable
212
218
  end
213
219
 
214
220
  def respond_to_invalid_request
215
- close if websocket.alive?
221
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
216
222
 
217
223
  logger.error invalid_request_message
218
224
  logger.info finished_request_message
219
- [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ]
225
+ [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
220
226
  end
221
227
 
222
228
  # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
@@ -229,7 +235,7 @@ module ActionCable
229
235
  'Started %s "%s"%s for %s at %s' % [
230
236
  request.request_method,
231
237
  request.filtered_path,
232
- websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
238
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
233
239
  request.ip,
234
240
  Time.now.to_s ]
235
241
  end
@@ -237,22 +243,24 @@ module ActionCable
237
243
  def finished_request_message
238
244
  'Finished "%s"%s for %s at %s' % [
239
245
  request.filtered_path,
240
- websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
246
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
241
247
  request.ip,
242
248
  Time.now.to_s ]
243
249
  end
244
250
 
245
251
  def invalid_request_message
246
- 'Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [
252
+ "Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
247
253
  env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
248
254
  ]
249
255
  end
250
256
 
251
257
  def successful_request_message
252
- 'Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [
258
+ "Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
253
259
  env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
254
260
  ]
255
261
  end
256
262
  end
257
263
  end
258
264
  end
265
+
266
+ ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
@@ -1,4 +1,6 @@
1
- require 'websocket/driver'
1
+ # frozen_string_literal: true
2
+
3
+ require "websocket/driver"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -8,18 +10,18 @@ module ActionCable
8
10
  # Copyright (c) 2010-2015 James Coglan
9
11
  class ClientSocket # :nodoc:
10
12
  def self.determine_url(env)
11
- scheme = secure_request?(env) ? 'wss:' : 'ws:'
13
+ scheme = secure_request?(env) ? "wss:" : "ws:"
12
14
  "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
13
15
  end
14
16
 
15
17
  def self.secure_request?(env)
16
- return true if env['HTTPS'] == 'on'
17
- return true if env['HTTP_X_FORWARDED_SSL'] == 'on'
18
- return true if env['HTTP_X_FORWARDED_SCHEME'] == 'https'
19
- return true if env['HTTP_X_FORWARDED_PROTO'] == 'https'
20
- return true if env['rack.url_scheme'] == 'https'
18
+ return true if env["HTTPS"] == "on"
19
+ return true if env["HTTP_X_FORWARDED_SSL"] == "on"
20
+ return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
21
+ return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
22
+ return true if env["rack.url_scheme"] == "https"
21
23
 
22
- return false
24
+ false
23
25
  end
24
26
 
25
27
  CONNECTING = 0
@@ -37,7 +39,7 @@ module ActionCable
37
39
  @url = ClientSocket.determine_url(@env)
38
40
 
39
41
  @driver = @driver_started = nil
40
- @close_params = ['', 1006]
42
+ @close_params = ["", 1006]
41
43
 
42
44
  @ready_state = CONNECTING
43
45
 
@@ -56,7 +58,7 @@ module ActionCable
56
58
  return if @driver.nil? || @driver_started
57
59
  @stream.hijack_rack_socket
58
60
 
59
- if callback = @env['async.callback']
61
+ if callback = @env["async.callback"]
60
62
  callback.call([101, {}, @stream])
61
63
  end
62
64
 
@@ -78,20 +80,20 @@ module ActionCable
78
80
  def transmit(message)
79
81
  return false if @ready_state > OPEN
80
82
  case message
81
- when Numeric then @driver.text(message.to_s)
82
- when String then @driver.text(message)
83
- when Array then @driver.binary(message)
84
- else false
83
+ when Numeric then @driver.text(message.to_s)
84
+ when String then @driver.text(message)
85
+ when Array then @driver.binary(message)
86
+ else false
85
87
  end
86
88
  end
87
89
 
88
90
  def close(code = nil, reason = nil)
89
91
  code ||= 1000
90
- reason ||= ''
92
+ reason ||= ""
91
93
 
92
- unless code == 1000 or (code >= 3000 and code <= 4999)
93
- raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
94
- "The code must be either 1000, or between 3000 and 4999. " +
94
+ unless code == 1000 || (code >= 3000 && code <= 4999)
95
+ raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
96
+ "The code must be either 1000, or between 3000 and 4999. " \
95
97
  "#{code} is neither."
96
98
  end
97
99
 
@@ -1,4 +1,6 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -6,11 +8,10 @@ module ActionCable
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  included do
9
- class_attribute :identifiers
10
- self.identifiers = Set.new
11
+ class_attribute :identifiers, default: Set.new
11
12
  end
12
13
 
13
- class_methods do
14
+ module ClassMethods
14
15
  # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
15
16
  # Common identifiers are current_user and current_account, but could be anything, really.
16
17
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  # Makes it possible for the RemoteConnection to disconnect a specific connection.
@@ -27,8 +29,8 @@ module ActionCable
27
29
  end
28
30
 
29
31
  def process_internal_message(message)
30
- case message['type']
31
- when 'disconnect'
32
+ case message["type"]
33
+ when "disconnect"
32
34
  logger.info "Removing connection (#{connection_identifier})"
33
35
  websocket.close
34
36
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
@@ -28,11 +30,10 @@ module ActionCable
28
30
  receive_buffered_messages
29
31
  end
30
32
 
31
- protected
33
+ private
32
34
  attr_reader :connection
33
35
  attr_reader :buffered_messages
34
36
 
35
- private
36
37
  def valid?(message)
37
38
  message.is_a?(String)
38
39
  end
@@ -1,4 +1,6 @@
1
- require 'thread'
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -10,7 +12,7 @@ module ActionCable
10
12
  def initialize(event_loop, socket)
11
13
  @event_loop = event_loop
12
14
  @socket_object = socket
13
- @stream_send = socket.env['stream.send']
15
+ @stream_send = socket.env["stream.send"]
14
16
 
15
17
  @rack_hijack_io = nil
16
18
  @write_lock = Mutex.new
@@ -94,10 +96,12 @@ module ActionCable
94
96
  end
95
97
 
96
98
  def hijack_rack_socket
97
- return unless @socket_object.env['rack.hijack']
99
+ return unless @socket_object.env["rack.hijack"]
98
100
 
99
- @socket_object.env['rack.hijack'].call
100
- @rack_hijack_io = @socket_object.env['rack.hijack_io']
101
+ # This should return the underlying io according to the SPEC:
102
+ @rack_hijack_io = @socket_object.env["rack.hijack"].call
103
+ # Retain existing behaviour if required:
104
+ @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
101
105
 
102
106
  @event_loop.attach(@rack_hijack_io, self)
103
107
  end
@@ -1,5 +1,7 @@
1
- require 'nio'
2
- require 'thread'
1
+ # frozen_string_literal: true
2
+
3
+ require "nio"
4
+ require "thread"
3
5
 
4
6
  module ActionCable
5
7
  module Connection
@@ -1,4 +1,6 @@
1
- require 'active_support/core_ext/hash/indifferent_access'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -11,19 +13,20 @@ module ActionCable
11
13
  end
12
14
 
13
15
  def execute_command(data)
14
- case data['command']
15
- when 'subscribe' then add data
16
- when 'unsubscribe' then remove data
17
- when 'message' then perform_action data
16
+ case data["command"]
17
+ when "subscribe" then add data
18
+ when "unsubscribe" then remove data
19
+ when "message" then perform_action data
18
20
  else
19
21
  logger.error "Received unrecognized command in #{data.inspect}"
20
22
  end
21
23
  rescue Exception => e
22
- logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
24
+ @connection.rescue_with_handler(e)
25
+ logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
23
26
  end
24
27
 
25
28
  def add(data)
26
- id_key = data['identifier']
29
+ id_key = data["identifier"]
27
30
  id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
28
31
 
29
32
  return if subscriptions.key?(id_key)
@@ -41,7 +44,7 @@ module ActionCable
41
44
 
42
45
  def remove(data)
43
46
  logger.info "Unsubscribing from channel: #{data['identifier']}"
44
- remove_subscription subscriptions[data['identifier']]
47
+ remove_subscription find(data)
45
48
  end
46
49
 
47
50
  def remove_subscription(subscription)
@@ -50,7 +53,7 @@ module ActionCable
50
53
  end
51
54
 
52
55
  def perform_action(data)
53
- find(data).perform_action ActiveSupport::JSON.decode(data['data'])
56
+ find(data).perform_action ActiveSupport::JSON.decode(data["data"])
54
57
  end
55
58
 
56
59
  def identifiers
@@ -61,14 +64,12 @@ module ActionCable
61
64
  subscriptions.each { |id, channel| remove_subscription(channel) }
62
65
  end
63
66
 
64
- protected
65
- attr_reader :connection, :subscriptions
66
-
67
67
  private
68
+ attr_reader :connection, :subscriptions
68
69
  delegate :logger, to: :connection
69
70
 
70
71
  def find(data)
71
- if subscription = subscriptions[data['identifier']]
72
+ if subscription = subscriptions[data["identifier"]]
72
73
  subscription
73
74
  else
74
75
  raise "Unable to find subscription with identifier: #{data['identifier']}"