actioncable 5.0.0.beta3 → 5.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -6
  3. data/README.md +11 -15
  4. data/lib/action_cable.rb +5 -4
  5. data/lib/action_cable/channel/base.rb +19 -6
  6. data/lib/action_cable/channel/periodic_timers.rb +45 -7
  7. data/lib/action_cable/channel/streams.rb +70 -14
  8. data/lib/action_cable/connection.rb +2 -0
  9. data/lib/action_cable/connection/base.rb +33 -21
  10. data/lib/action_cable/connection/client_socket.rb +17 -9
  11. data/lib/action_cable/connection/faye_client_socket.rb +48 -0
  12. data/lib/action_cable/connection/faye_event_loop.rb +44 -0
  13. data/lib/action_cable/connection/internal_channel.rb +3 -5
  14. data/lib/action_cable/connection/message_buffer.rb +2 -2
  15. data/lib/action_cable/connection/stream.rb +9 -11
  16. data/lib/action_cable/connection/stream_event_loop.rb +10 -1
  17. data/lib/action_cable/connection/web_socket.rb +6 -2
  18. data/lib/action_cable/engine.rb +37 -1
  19. data/lib/action_cable/gem_version.rb +1 -1
  20. data/lib/action_cable/helpers/action_cable_helper.rb +19 -8
  21. data/lib/action_cable/remote_connections.rb +1 -1
  22. data/lib/action_cable/server/base.rb +26 -6
  23. data/lib/action_cable/server/broadcasting.rb +10 -9
  24. data/lib/action_cable/server/configuration.rb +19 -3
  25. data/lib/action_cable/server/connections.rb +3 -3
  26. data/lib/action_cable/server/worker.rb +27 -27
  27. data/lib/action_cable/server/worker/active_record_connection_management.rb +0 -3
  28. data/lib/action_cable/subscription_adapter/async.rb +8 -3
  29. data/lib/action_cable/subscription_adapter/evented_redis.rb +5 -1
  30. data/lib/action_cable/subscription_adapter/postgresql.rb +5 -4
  31. data/lib/action_cable/subscription_adapter/redis.rb +11 -6
  32. data/lib/assets/compiled/action_cable.js +248 -188
  33. data/lib/rails/generators/channel/USAGE +1 -1
  34. data/lib/rails/generators/channel/channel_generator.rb +4 -1
  35. data/lib/rails/generators/channel/templates/assets/cable.js +13 -0
  36. metadata +8 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e3e7b9ae96c4f9f359281abaa75f8b909a8fb04f
4
- data.tar.gz: 41a9536aea144e25988a510069c3b07b0250aa5b
3
+ metadata.gz: a5d4383eac68ebad915b6ba22ef556a6dbcdabf5
4
+ data.tar.gz: 04c95deb8c0ae1e283b0fd630172646f86d4e541
5
5
  SHA512:
6
- metadata.gz: 6f1879623b43518932a8c295c72acc97b7dd97008d1925dc3130f0398fbaa68a22d4bb0f6ad9f86b9d85e36c8a88e0070c235e4ac79480079d1cec546651e87e
7
- data.tar.gz: a7c49b2304c60bb4d50674a457164a14b9d5f4ae0dc4aaabf65569bdf2a7f493ad6e5938162b017e9f0ff80cf2cd8fa028590b00a32ccbe14837d4ebf5d638c0
6
+ metadata.gz: 9b37dfd802d1c0f95b97e5afe0832a0cf9143b77cc17615ed6a402dead98856e4c759a503c301a1a5359a95116fcbfe812dfb18655b6476b6548b7b182d988f4
7
+ data.tar.gz: 2657c42b971d5dc208906c98a3545aae2111c6775e6478561bb91071f854e83bd6c8b9eb7a8080d0c5d17bd67ae95e2385ac5b97d5ef063d917825bb0898c4a6
@@ -1,11 +1,57 @@
1
+ ## Rails 5.0.0.beta4 (April 27, 2016) ##
2
+
3
+ * WebSocket protocol negotiation.
4
+
5
+ Introduces an Action Cable protocol version that moves independently
6
+ of and, hopefully, more slowly than Action Cable itself. Client sockets
7
+ negotiate a protocol with the Cable server using WebSockets' native
8
+ subprotocol support:
9
+ * https://tools.ietf.org/html/rfc6455#section-1.9
10
+ * https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Subprotocols
11
+
12
+ If they can't negotiate a compatible protocol (usually due to upgrading
13
+ the Cable server with a browser still running old JavaScript) then the
14
+ client knows to disconnect, cease retrying, and tell the app that it hit
15
+ a protocol mismatch.
16
+
17
+ This allows us to evolve the Action Cable message format, handshaking,
18
+ pings, acknowledgements, and more without breaking older clients'
19
+ expectations of server behavior.
20
+
21
+ *Daniel Rhodes*
22
+
23
+ * Pubsub: automatic stream decoding.
24
+
25
+ stream_for @room, coder: ActiveSupport::JSON do |message|
26
+ # `message` is a Ruby hash here instead of a JSON string
27
+
28
+ The `coder` must respond to `#decode`. Defaults to `coder: nil`
29
+ which skips decoding entirely.
30
+
31
+ *Jeremy Daer*
32
+
33
+ * Add ActiveSupport::Notifications to ActionCable::Channel.
34
+
35
+ *Matthew Wear*
36
+
37
+ * Safely support autoloading and class unloading, by preventing concurrent
38
+ loads, and disconnecting all cables during reload.
39
+
40
+ *Matthew Draper*
41
+
42
+ * Ensure ActionCable behaves correctly for non-string queue names.
43
+
44
+ *Jay Hayes*
45
+
1
46
  ## Rails 5.0.0.beta3 (February 24, 2016) ##
2
47
 
3
- * Added ActionCable::SubscriptionAdapter::EventedRedis.em_redis_connector/redis_connector and
4
- ActionCable::SubscriptionAdapter::Redis.redis_connector factory methods for redis connections,
5
- so you can overwrite with your own initializers. This is used when you want to use different-than-standard Redis adapters,
6
- like for Makara distributed Redis.
48
+ * Added `em_redis_connector` and `redis_connector` to
49
+ `ActionCable::SubscriptionAdapter::EventedRedis` and added `redis_connector`
50
+ to `ActionCable::SubscriptionAdapter::Redis`, so you can overwrite with your
51
+ own initializers. This is used when you want to use different-than-standard
52
+ Redis adapters, like for Makara distributed Redis.
7
53
 
8
- *DHH*
54
+ *DHH*
9
55
 
10
56
  ## Rails 5.0.0.beta2 (February 01, 2016) ##
11
57
 
@@ -28,7 +74,7 @@
28
74
  ActionCable was changed from`config/redis/cable.yml` to
29
75
  `config/cable.yml`.
30
76
 
31
- *Jon Moss*
77
+ *Jon Moss*
32
78
 
33
79
  ## Rails 5.0.0.beta1 (December 18, 2015) ##
34
80
 
data/README.md CHANGED
@@ -178,7 +178,7 @@ App.cable.subscriptions.create "AppearanceChannel",
178
178
  ```
179
179
 
180
180
  Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`,
181
- which in turn is linked to original `App.cable` -> `ApplicationCable::Connection` instances.
181
+ which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances.
182
182
 
183
183
  Next, we link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side
184
184
  channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these
@@ -339,21 +339,21 @@ Rails.application.config.action_cable.disable_request_forgery_protection = true
339
339
 
340
340
  ### Consumer Configuration
341
341
 
342
- Once you have decided how to run your cable server (see below), you must provide the server url (or path) to your client-side setup.
342
+ Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup.
343
343
  There are two ways you can do this.
344
344
 
345
345
  The first is to simply pass it in when creating your consumer. For a standalone server,
346
346
  this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server,
347
347
  something like: `App.cable = ActionCable.createConsumer("/cable")`.
348
348
 
349
- The second option is to pass the server url through the `action_cable_meta_tag` in your layout.
350
- This uses a url or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
349
+ The second option is to pass the server URL through the `action_cable_meta_tag` in your layout.
350
+ This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
351
351
 
352
- This method is especially useful if your WebSocket url might change between environments. If you host your production server via https, you will need to use the wss scheme
352
+ This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme
353
353
  for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your
354
354
  domain in production.
355
355
 
356
- In any case, to vary the WebSocket url between environments, add the following configuration to each environment:
356
+ In any case, to vary the WebSocket URL between environments, add the following configuration to each environment:
357
357
 
358
358
  ```ruby
359
359
  config.action_cable.url = "ws://example.com:28080"
@@ -412,12 +412,12 @@ The above will start a cable server on port 28080.
412
412
 
413
413
  ### In app
414
414
 
415
- If you are using a threaded server like Puma or Thin, the current implementation of Action Cable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path:
415
+ If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`:
416
416
 
417
417
  ```ruby
418
- # config/routes.rb
419
- Example::Application.routes.draw do
420
- mount ActionCable.server => '/cable'
418
+ # config/application.rb
419
+ class Application < Rails::Application
420
+ config.action_cable.mount_path = '/websocket'
421
421
  end
422
422
  ```
423
423
 
@@ -445,12 +445,8 @@ connection management is handled internally by utilizing Ruby’s native thread
445
445
  support, which means you can use all your regular Rails models with no problems
446
446
  as long as you haven’t committed any thread-safety sins.
447
447
 
448
- But this also means that Action Cable needs to run in its own server process.
449
- So you'll have one set of server processes for your normal web work, and another
450
- set of server processes for the Action Cable.
451
-
452
448
  The Action Cable server does _not_ need to be a multi-threaded application server.
453
- This is because Action Cable uses the [Rack socket hijacking API](http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/)
449
+ This is because Action Cable uses the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking)
454
450
  to take over control of connections from the application server. Action Cable
455
451
  then manages connections internally, in a multithreaded manner, regardless of
456
452
  whether the application server is multi-threaded or not. So Action Cable works
@@ -29,13 +29,14 @@ module ActionCable
29
29
  extend ActiveSupport::Autoload
30
30
 
31
31
  INTERNAL = {
32
- identifiers: {
33
- ping: '_ping'.freeze
34
- },
35
32
  message_types: {
33
+ welcome: 'welcome'.freeze,
34
+ ping: 'ping'.freeze,
36
35
  confirmation: 'confirm_subscription'.freeze,
37
36
  rejection: 'reject_subscription'.freeze
38
- }
37
+ },
38
+ default_mount_path: '/cable'.freeze,
39
+ protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze
39
40
  }
40
41
 
41
42
  # Singleton instance of the server
@@ -160,13 +160,16 @@ module ActionCable
160
160
  action = extract_action(data)
161
161
 
162
162
  if processable_action?(action)
163
- dispatch_action(action, data)
163
+ payload = { channel_class: self.class.name, action: action, data: data }
164
+ ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
165
+ dispatch_action(action, data)
166
+ end
164
167
  else
165
168
  logger.error "Unable to process #{action_signature(action, data)}"
166
169
  end
167
170
  end
168
171
 
169
- # Called by the cable connection when its cut, so the channel has a chance to cleanup with callbacks.
172
+ # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
170
173
  # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
171
174
  def unsubscribe_from_channel # :nodoc:
172
175
  run_callbacks :unsubscribe do
@@ -192,7 +195,11 @@ module ActionCable
192
195
  # the proper channel identifier marked as the recipient.
193
196
  def transmit(data, via: nil)
194
197
  logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via }
195
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data)
198
+
199
+ payload = { channel_class: self.class.name, data: data, via: via }
200
+ ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
201
+ connection.transmit identifier: @identifier, message: data
202
+ end
196
203
  end
197
204
 
198
205
  def defer_subscription_confirmation!
@@ -265,8 +272,11 @@ module ActionCable
265
272
  def transmit_subscription_confirmation
266
273
  unless subscription_confirmation_sent?
267
274
  logger.info "#{self.class.name} is transmitting the subscription confirmation"
268
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation])
269
- @subscription_confirmation_sent = true
275
+
276
+ ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
277
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
278
+ @subscription_confirmation_sent = true
279
+ end
270
280
  end
271
281
  end
272
282
 
@@ -277,7 +287,10 @@ module ActionCable
277
287
 
278
288
  def transmit_subscription_rejection
279
289
  logger.info "#{self.class.name} is transmitting the subscription rejection"
280
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection])
290
+
291
+ ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
292
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
293
+ end
281
294
  end
282
295
  end
283
296
  end
@@ -12,11 +12,42 @@ module ActionCable
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- # Allows you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful
16
- # for sending a steady flow of updates to a client based off an object that was configured on subscription.
17
- # It's an alternative to using streams if the channel is able to do the work internally.
18
- def periodically(callback, every:)
19
- self.periodic_timers += [ [ callback, every: every ] ]
15
+ # Periodically performs a task on the channel, like updating an online
16
+ # user counter, polling a backend for new status messages, sending
17
+ # regular "heartbeat" messages, or doing some internal work and giving
18
+ # progress updates.
19
+ #
20
+ # Pass a method name or lambda argument or provide a block to call.
21
+ # Specify the calling period in seconds using the <tt>every:</tt>
22
+ # keyword argument.
23
+ #
24
+ # periodically :transmit_progress, every: 5.seconds
25
+ #
26
+ # periodically every: 3.minutes do
27
+ # transmit action: :update_count, count: current_count
28
+ # end
29
+ #
30
+ def periodically(callback_or_method_name = nil, every:, &block)
31
+ callback =
32
+ if block_given?
33
+ raise ArgumentError, 'Pass a block or provide a callback arg, not both' if callback_or_method_name
34
+ block
35
+ else
36
+ case callback_or_method_name
37
+ when Proc
38
+ callback_or_method_name
39
+ when Symbol
40
+ -> { __send__ callback_or_method_name }
41
+ else
42
+ raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
43
+ end
44
+ end
45
+
46
+ unless every.kind_of?(Numeric) && every > 0
47
+ raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
48
+ end
49
+
50
+ self.periodic_timers += [[ callback, every: every ]]
20
51
  end
21
52
  end
22
53
 
@@ -27,14 +58,21 @@ module ActionCable
27
58
 
28
59
  def start_periodic_timers
29
60
  self.class.periodic_timers.each do |callback, options|
30
- active_periodic_timers << Concurrent::TimerTask.new(execution_interval: options[:every]) do
31
- connection.worker_pool.async_run_periodic_timer(self, callback)
61
+ active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every))
62
+ end
63
+ end
64
+
65
+ def start_periodic_timer(callback, every:)
66
+ connection.server.event_loop.timer every do
67
+ connection.worker_pool.async_invoke connection do
68
+ instance_exec(&callback)
32
69
  end
33
70
  end
34
71
  end
35
72
 
36
73
  def stop_periodic_timers
37
74
  active_periodic_timers.each { |timer| timer.shutdown }
75
+ active_periodic_timers.clear
38
76
  end
39
77
  end
40
78
  end
@@ -1,8 +1,8 @@
1
1
  module ActionCable
2
2
  module Channel
3
- # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pub/sub queue where any data
3
+ # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
4
4
  # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
5
- # streaming a broadcasting at the very moment it sends out an update, you will not get that update, if you connect after it has been sent.
5
+ # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
6
6
  #
7
7
  # Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
8
8
  # the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
@@ -46,9 +46,7 @@ module ActionCable
46
46
  # def subscribed
47
47
  # @room = Chat::Room[params[:room_number]]
48
48
  #
49
- # stream_for @room, -> (encoded_message) do
50
- # message = ActiveSupport::JSON.decode(encoded_message)
51
- #
49
+ # stream_for @room, coder: ActiveSupport::JSON do |message|
52
50
  # if message['originated_at'].present?
53
51
  # elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
54
52
  #
@@ -71,15 +69,21 @@ module ActionCable
71
69
 
72
70
  # Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
73
71
  # instead of the default of just transmitting the updates straight to the subscriber.
74
- def stream_from(broadcasting, callback = nil)
72
+ # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
73
+ # Defaults to `coder: nil` which does no decoding, passes raw messages.
74
+ def stream_from(broadcasting, callback = nil, coder: nil, &block)
75
+ broadcasting = String(broadcasting)
76
+
75
77
  # Don't send the confirmation until pubsub#subscribe is successful
76
78
  defer_subscription_confirmation!
77
79
 
78
- callback ||= default_stream_callback(broadcasting)
79
- streams << [ broadcasting, callback ]
80
+ # Build a stream handler by wrapping the user-provided callback with
81
+ # a decoder or defaulting to a JSON-decoding retransmitter.
82
+ handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
83
+ streams << [ broadcasting, handler ]
80
84
 
81
- Concurrent.global_io_executor.post do
82
- pubsub.subscribe(broadcasting, callback, lambda do
85
+ connection.server.event_loop.post do
86
+ pubsub.subscribe(broadcasting, handler, lambda do
83
87
  transmit_subscription_confirmation
84
88
  logger.info "#{self.class.name} is streaming from #{broadcasting}"
85
89
  end)
@@ -89,8 +93,11 @@ module ActionCable
89
93
  # Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
90
94
  # <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
91
95
  # to the subscriber.
92
- def stream_for(model, callback = nil)
93
- stream_from(broadcasting_for([ channel_name, model ]), callback)
96
+ #
97
+ # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
98
+ # Defaults to `coder: nil` which does no decoding, passes raw messages.
99
+ def stream_for(model, callback = nil, coder: nil, &block)
100
+ stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
94
101
  end
95
102
 
96
103
  # Unsubscribes all streams associated with this channel from the pubsub queue.
@@ -108,11 +115,60 @@ module ActionCable
108
115
  @_streams ||= []
109
116
  end
110
117
 
111
- def default_stream_callback(broadcasting)
118
+ # Always wrap the outermost handler to invoke the user handler on the
119
+ # worker pool rather than blocking the event loop.
120
+ def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
121
+ handler = stream_handler(broadcasting, user_handler, coder: coder)
122
+
123
+ -> message do
124
+ connection.worker_pool.async_invoke handler, :call, message, connection: connection
125
+ end
126
+ end
127
+
128
+ # May be overridden to add instrumentation, logging, specialized error
129
+ # handling, or other forms of handler decoration.
130
+ #
131
+ # TODO: Tests demonstrating this.
132
+ def stream_handler(broadcasting, user_handler, coder: nil)
133
+ if user_handler
134
+ stream_decoder user_handler, coder: coder
135
+ else
136
+ default_stream_handler broadcasting, coder: coder
137
+ end
138
+ end
139
+
140
+ # May be overridden to change the default stream handling behavior
141
+ # which decodes JSON and transmits to client.
142
+ #
143
+ # TODO: Tests demonstrating this.
144
+ #
145
+ # TODO: Room for optimization. Update transmit API to be coder-aware
146
+ # so we can no-op when pubsub and connection are both JSON-encoded.
147
+ # Then we can skip decode+encode if we're just proxying messages.
148
+ def default_stream_handler(broadcasting, coder:)
149
+ coder ||= ActiveSupport::JSON
150
+ stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting
151
+ end
152
+
153
+ def stream_decoder(handler = identity_handler, coder:)
154
+ if coder
155
+ -> message { handler.(coder.decode(message)) }
156
+ else
157
+ handler
158
+ end
159
+ end
160
+
161
+ def stream_transmitter(handler = identity_handler, broadcasting:)
162
+ via = "streamed from #{broadcasting}"
163
+
112
164
  -> (message) do
113
- transmit ActiveSupport::JSON.decode(message), via: "streamed from #{broadcasting}"
165
+ transmit handler.(message), via: via
114
166
  end
115
167
  end
168
+
169
+ def identity_handler
170
+ -> message { message }
171
+ end
116
172
  end
117
173
  end
118
174
  end
@@ -8,6 +8,8 @@ module ActionCable
8
8
  autoload :ClientSocket
9
9
  autoload :Identification
10
10
  autoload :InternalChannel
11
+ autoload :FayeClientSocket
12
+ autoload :FayeEventLoop
11
13
  autoload :MessageBuffer
12
14
  autoload :Stream
13
15
  autoload :StreamEventLoop
@@ -40,7 +40,7 @@ module ActionCable
40
40
  # Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes
41
41
  # it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection.
42
42
  #
43
- # Finally, we add a tag to the connection-specific logger with name of the current user to easily distinguish their messages in the log.
43
+ # Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log.
44
44
  #
45
45
  # Pretty simple, eh?
46
46
  class Base
@@ -48,15 +48,16 @@ module ActionCable
48
48
  include InternalChannel
49
49
  include Authorization
50
50
 
51
- attr_reader :server, :env, :subscriptions, :logger
52
- delegate :stream_event_loop, :worker_pool, :pubsub, to: :server
51
+ attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
52
+ delegate :event_loop, :pubsub, to: :server
53
53
 
54
- def initialize(server, env)
55
- @server, @env = server, env
54
+ def initialize(server, env, coder: ActiveSupport::JSON)
55
+ @server, @env, @coder = server, env, coder
56
56
 
57
+ @worker_pool = server.worker_pool
57
58
  @logger = new_tagged_logger
58
59
 
59
- @websocket = ActionCable::Connection::WebSocket.new(env, self, stream_event_loop)
60
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
60
61
  @subscriptions = ActionCable::Connection::Subscriptions.new(self)
61
62
  @message_buffer = ActionCable::Connection::MessageBuffer.new(self)
62
63
 
@@ -66,7 +67,7 @@ module ActionCable
66
67
 
67
68
  # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
68
69
  # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
69
- def process # :nodoc:
70
+ def process #:nodoc:
70
71
  logger.info started_request_message
71
72
 
72
73
  if websocket.possible? && allow_request_origin?
@@ -76,20 +77,22 @@ module ActionCable
76
77
  end
77
78
  end
78
79
 
79
- # Data received over the WebSocket connection is handled by this method. It's expected that everything inbound is JSON encoded.
80
- # The data is routed to the proper channel that the connection has subscribed to.
81
- def receive(data_in_json)
80
+ # Decodes WebSocket messages and dispatches them to subscribed channels.
81
+ # WebSocket message transfer encoding is always JSON.
82
+ def receive(websocket_message) #:nodoc:
83
+ send_async :dispatch_websocket_message, websocket_message
84
+ end
85
+
86
+ def dispatch_websocket_message(websocket_message) #:nodoc:
82
87
  if websocket.alive?
83
- subscriptions.execute_command ActiveSupport::JSON.decode(data_in_json)
88
+ subscriptions.execute_command decode(websocket_message)
84
89
  else
85
- logger.error "Received data without a live WebSocket (#{data_in_json.inspect})"
90
+ logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
86
91
  end
87
92
  end
88
93
 
89
- # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the
90
- # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON.
91
- def transmit(data) # :nodoc:
92
- websocket.transmit data
94
+ def transmit(cable_message) # :nodoc:
95
+ websocket.transmit encode(cable_message)
93
96
  end
94
97
 
95
98
  # Close the WebSocket connection.
@@ -114,7 +117,7 @@ module ActionCable
114
117
  end
115
118
 
116
119
  def beat
117
- transmit ActiveSupport::JSON.encode(identifier: ActionCable::INTERNAL[:identifiers][:ping], message: Time.now.to_i)
120
+ transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i
118
121
  end
119
122
 
120
123
  def on_open # :nodoc:
@@ -151,10 +154,19 @@ module ActionCable
151
154
  attr_reader :message_buffer
152
155
 
153
156
  private
157
+ def encode(cable_message)
158
+ @coder.encode cable_message
159
+ end
160
+
161
+ def decode(websocket_message)
162
+ @coder.decode websocket_message
163
+ end
164
+
154
165
  def handle_open
166
+ @protocol = websocket.protocol
155
167
  connect if respond_to?(:connect)
156
168
  subscribe_to_internal_channel
157
- confirm_connection_monitor_subscription
169
+ send_welcome_message
158
170
 
159
171
  message_buffer.process!
160
172
  server.add_connection(self)
@@ -173,11 +185,11 @@ module ActionCable
173
185
  disconnect if respond_to?(:disconnect)
174
186
  end
175
187
 
176
- def confirm_connection_monitor_subscription
177
- # Send confirmation message to the internal connection monitor channel.
188
+ def send_welcome_message
189
+ # Send welcome message to the internal connection monitor channel.
178
190
  # This ensures the connection monitor state is reset after a successful
179
191
  # websocket connection.
180
- transmit ActiveSupport::JSON.encode(identifier: ActionCable::INTERNAL[:identifiers][:ping], type: ActionCable::INTERNAL[:message_types][:confirmation])
192
+ transmit type: ActionCable::INTERNAL[:message_types][:welcome]
181
193
  end
182
194
 
183
195
  def allow_request_origin?