actioncable 5.0.0.beta2 → 5.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +39 -31
  4. data/lib/action_cable/channel/base.rb +26 -19
  5. data/lib/action_cable/channel/periodic_timers.rb +1 -1
  6. data/lib/action_cable/channel/streams.rb +23 -19
  7. data/lib/action_cable/connection/base.rb +34 -13
  8. data/lib/action_cable/connection/client_socket.rb +2 -5
  9. data/lib/action_cable/connection/identification.rb +1 -1
  10. data/lib/action_cable/connection/message_buffer.rb +2 -3
  11. data/lib/action_cable/connection/subscriptions.rb +3 -3
  12. data/lib/action_cable/engine.rb +6 -0
  13. data/lib/action_cable/gem_version.rb +1 -1
  14. data/lib/action_cable/helpers/action_cable_helper.rb +1 -1
  15. data/lib/action_cable/remote_connections.rb +7 -5
  16. data/lib/action_cable/server/base.rb +6 -7
  17. data/lib/action_cable/server/broadcasting.rb +14 -14
  18. data/lib/action_cable/server/configuration.rb +5 -12
  19. data/lib/action_cable/server/connections.rb +4 -5
  20. data/lib/action_cable/server/worker.rb +2 -2
  21. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -2
  22. data/lib/action_cable/subscription_adapter/evented_redis.rb +10 -2
  23. data/lib/action_cable/subscription_adapter/redis.rb +5 -1
  24. data/lib/assets/compiled/action_cable.js +57 -8
  25. data/lib/rails/generators/channel/channel_generator.rb +17 -0
  26. data/lib/rails/generators/channel/templates/application_cable/channel.rb +5 -0
  27. data/lib/rails/generators/channel/templates/application_cable/connection.rb +5 -0
  28. data/lib/rails/generators/channel/templates/channel.rb +1 -1
  29. metadata +7 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 666a8feb5df8f5f6a523f81a826c1a2fbd23a1c3
4
- data.tar.gz: 85b3d45b716f55eed7b2b8aad82e69dd7f00c1e7
3
+ metadata.gz: e3e7b9ae96c4f9f359281abaa75f8b909a8fb04f
4
+ data.tar.gz: 41a9536aea144e25988a510069c3b07b0250aa5b
5
5
  SHA512:
6
- metadata.gz: be0531d119c3792067b0cc54609830700ea1eacb28dd6199775e13b3914a83a90cdbc6e44d10d434a14dabc159e7dc2d7df0b984e3a3e748b4e8ec42ea554def
7
- data.tar.gz: 75e01690a3a351949df277f14492b79f050518a9c8f5e5a72a68df0f83595df635b9bfedea7f53e9882e0d0bac302fed69c5f79ce535ae765d22da607dc65fce
6
+ metadata.gz: 6f1879623b43518932a8c295c72acc97b7dd97008d1925dc3130f0398fbaa68a22d4bb0f6ad9f86b9d85e36c8a88e0070c235e4ac79480079d1cec546651e87e
7
+ data.tar.gz: a7c49b2304c60bb4d50674a457164a14b9d5f4ae0dc4aaabf65569bdf2a7f493ad6e5938162b017e9f0ff80cf2cd8fa028590b00a32ccbe14837d4ebf5d638c0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## Rails 5.0.0.beta3 (February 24, 2016) ##
2
+
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.
7
+
8
+ *DHH*
9
+
1
10
  ## Rails 5.0.0.beta2 (February 01, 2016) ##
2
11
 
3
12
  * Support PostgreSQL pubsub adapter.
data/README.md CHANGED
@@ -17,7 +17,7 @@ The client of a WebSocket connection is called the consumer.
17
17
 
18
18
  Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates
19
19
  a logical unit of work, similar to what a controller does in a regular MVC setup. For example,
20
- you could have a `ChatChannel` and a `AppearancesChannel`, and a consumer could be subscribed to either
20
+ you could have a `ChatChannel` and an `AppearancesChannel`, and a consumer could be subscribed to either
21
21
  or to both of these channels. At the very least, a consumer should be subscribed to one channel.
22
22
 
23
23
  When the consumer is subscribed to a channel, they act as a subscriber. The connection between
@@ -39,7 +39,7 @@ reflections of each unit.
39
39
  ### A full-stack example
40
40
 
41
41
  The first thing you must do is define your `ApplicationCable::Connection` class in Ruby. This
42
- is the place where you authorize the incoming connection, and proceed to establish it
42
+ is the place where you authorize the incoming connection, and proceed to establish it,
43
43
  if all is well. Here's the simplest example starting with the server-side connection class:
44
44
 
45
45
  ```ruby
@@ -73,7 +73,7 @@ use that to set the `current_user`. By identifying the connection by this same c
73
73
  you're also ensuring that you can later retrieve all open connections by a given user (and
74
74
  potentially disconnect them all if the user is deleted or deauthorized).
75
75
 
76
- Then you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put
76
+ Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put
77
77
  shared logic between your channels.
78
78
 
79
79
  ```ruby
@@ -94,7 +94,7 @@ The client-side needs to setup a consumer instance of this connection. That's do
94
94
  App.cable = ActionCable.createConsumer("ws://cable.example.com")
95
95
  ```
96
96
 
97
- The ws://cable.example.com address must point to your set of Action Cable servers, and it
97
+ The `ws://cable.example.com` address must point to your Action Cable server(s), and it
98
98
  must share a cookie namespace with the rest of the application (which may live under http://example.com).
99
99
  This ensures that the signed cookie will be correctly sent.
100
100
 
@@ -105,8 +105,8 @@ is defined by declaring channels on the server and allowing the consumer to subs
105
105
 
106
106
  ### Channel example 1: User appearances
107
107
 
108
- Here's a simple example of a channel that tracks whether a user is online or not and what page they're on.
109
- (This is useful for creating presence features like showing a green dot next to a user name if they're online).
108
+ Here's a simple example of a channel that tracks whether a user is online or not, and also what page they are currently on.
109
+ (This is useful for creating presence features like showing a green dot next to a user's name if they're online).
110
110
 
111
111
  First you declare the server-side channel:
112
112
 
@@ -180,7 +180,7 @@ App.cable.subscriptions.create "AppearanceChannel",
180
180
  Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`,
181
181
  which in turn is linked to original `App.cable` -> `ApplicationCable::Connection` instances.
182
182
 
183
- We then link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side
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
185
185
  can be reached as remote procedure calls via a subscription's `perform` method.
186
186
 
@@ -188,7 +188,7 @@ can be reached as remote procedure calls via a subscription's `perform` method.
188
188
 
189
189
  The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection.
190
190
  But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes
191
- action on the client.
191
+ an action on the client.
192
192
 
193
193
  This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right
194
194
  streams:
@@ -215,7 +215,7 @@ ActionCable.server.broadcast \
215
215
  "web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' }
216
216
  ```
217
217
 
218
- The `ActionCable.server.broadcast` call places a message in the Redis' pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`.
218
+ The `ActionCable.server.broadcast` call places a message in the Action Cable pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`.
219
219
  The channel has been instructed to stream everything that arrives at `web_notifications_1` directly to the client by invoking the
220
220
  `#received(data)` callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip
221
221
  across the wire, and unpacked for the data argument arriving to `#received`.
@@ -234,7 +234,7 @@ class ChatChannel < ApplicationCable::Channel
234
234
  end
235
235
  ```
236
236
 
237
- Pass an object as the first argument to `subscriptions.create`, and that object will become your params hash in your cable channel. The keyword `channel` is required.
237
+ If you pass an object as the first argument to `subscriptions.create`, that object will become the params hash in your cable channel. The keyword `channel` is required.
238
238
 
239
239
  ```coffeescript
240
240
  # Client-side, which assumes you've already requested the right to send web notifications
@@ -293,27 +293,29 @@ The rebroadcast will be received by all connected clients, _including_ the clien
293
293
 
294
294
  ### More complete examples
295
295
 
296
- See the [rails/actioncable-examples](http://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app and adding channels.
296
+ See the [rails/actioncable-examples](http://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels.
297
297
 
298
298
 
299
299
  ## Configuration
300
300
 
301
- Action Cable has three required configurations: the Redis connection, allowed request origins, and the cable server url (which can optionally be set on the client side).
301
+ Action Cable has three required configurations: a subscription adapter, allowed request origins, and the cable server URL (which can optionally be set on the client side).
302
302
 
303
303
  ### Redis
304
304
 
305
305
  By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`.
306
- This file must specify a Redis url for each Rails environment. It may use the following format:
306
+ This file must specify an adapter and a URL for each Rails environment. It may use the following format:
307
307
 
308
308
  ```yaml
309
309
  production: &production
310
+ adapter: redis
310
311
  url: redis://10.10.3.153:6381
311
312
  development: &development
313
+ adapter: redis
312
314
  url: redis://localhost:6379
313
315
  test: *development
314
316
  ```
315
317
 
316
- You can also change the location of the Redis config file in a Rails initializer with something like:
318
+ You can also change the location of the Action Cable config file in a Rails initializer with something like:
317
319
 
318
320
  ```ruby
319
321
  Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml"
@@ -324,7 +326,7 @@ Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml"
324
326
  Action Cable will only accept requests from specified origins, which are passed to the server config as an array. The origins can be instances of strings or regular expressions, against which a check for match will be performed.
325
327
 
326
328
  ```ruby
327
- ActionCable.server.config.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
329
+ Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
328
330
  ```
329
331
 
330
332
  When running in the development environment, this defaults to "http://localhost:3000".
@@ -332,7 +334,7 @@ When running in the development environment, this defaults to "http://localhost:
332
334
  To disable and allow requests from any origin:
333
335
 
334
336
  ```ruby
335
- ActionCable.server.config.disable_request_forgery_protection = true
337
+ Rails.application.config.action_cable.disable_request_forgery_protection = true
336
338
  ```
337
339
 
338
340
  ### Consumer Configuration
@@ -347,11 +349,11 @@ something like: `App.cable = ActionCable.createConsumer("/cable")`.
347
349
  The second option is to pass the server url through the `action_cable_meta_tag` in your layout.
348
350
  This uses a url or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
349
351
 
350
- 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
351
- for your ActionCable server, but development might remain http and use the ws scheme. You might use localhost in development and your
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
+ for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your
352
354
  domain in production.
353
355
 
354
- 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:
355
357
 
356
358
  ```ruby
357
359
  config.action_cable.url = "ws://example.com:28080"
@@ -374,7 +376,7 @@ App.cable = ActionCable.createConsumer()
374
376
  The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp:
375
377
 
376
378
  ```ruby
377
- ActionCable.server.config.log_tags = [
379
+ Rails.application.config.action_cable.log_tags = [
378
380
  -> request { request.env['bc.account_id'] || "no-account" },
379
381
  :action_cable,
380
382
  -> request { request.uuid }
@@ -389,7 +391,7 @@ Also note that your server must provide at least the same number of database con
389
391
  ## Running the cable server
390
392
 
391
393
  ### Standalone
392
- The cable server(s) is separated from your normal application server. It's still a rack application, but it is its own rack
394
+ The cable server(s) is separated from your normal application server. It's still a Rack application, but it is its own Rack
393
395
  application. The recommended basic setup is as follows:
394
396
 
395
397
  ```ruby
@@ -410,7 +412,7 @@ The above will start a cable server on port 28080.
410
412
 
411
413
  ### In app
412
414
 
413
- If you are using a threaded server like Puma or Thin, the current implementation of ActionCable 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 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:
414
416
 
415
417
  ```ruby
416
418
  # config/routes.rb
@@ -419,7 +421,7 @@ Example::Application.routes.draw do
419
421
  end
420
422
  ```
421
423
 
422
- For every instance of your server you create and for every worker your server spawns, you will also have a new instance of ActionCable, but the use of Redis keeps messages synced across connections.
424
+ For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections.
423
425
 
424
426
  ### Notes
425
427
 
@@ -431,25 +433,31 @@ The WebSocket server doesn't have access to the session, but it has access to th
431
433
 
432
434
  ## Dependencies
433
435
 
434
- Action Cable is currently tied to Redis through its use of the pubsub feature to route
435
- messages back and forth over the WebSocket cable connection. This dependency may well
436
- be alleviated in the future, but for the moment that's what it is. So be sure to have
437
- Redis installed and running.
436
+ Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, evented Redis, and non-evented Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations.
438
437
 
439
- The Ruby side of things is built on top of [faye-websocket](https://github.com/faye/faye-websocket-ruby) and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
438
+ The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
440
439
 
441
440
 
442
441
  ## Deployment
443
442
 
444
- Action Cable is powered by a combination of websockets and threads. All of the
443
+ Action Cable is powered by a combination of WebSockets and threads. All of the
445
444
  connection management is handled internally by utilizing Ruby’s native thread
446
445
  support, which means you can use all your regular Rails models with no problems
447
446
  as long as you haven’t committed any thread-safety sins.
448
447
 
449
448
  But this also means that Action Cable needs to run in its own server process.
450
449
  So you'll have one set of server processes for your normal web work, and another
451
- set of server processes for the Action Cable. The former can be single-threaded,
452
- like Unicorn, but the latter must be multi-threaded, like Puma.
450
+ set of server processes for the Action Cable.
451
+
452
+ 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/)
454
+ to take over control of connections from the application server. Action Cable
455
+ then manages connections internally, in a multithreaded manner, regardless of
456
+ whether the application server is multi-threaded or not. So Action Cable works
457
+ with all the popular application servers -- Unicorn, Puma and Passenger.
458
+
459
+ Action Cable does not work with WEBrick, because WEBrick does not support the
460
+ Rack socket hijacking API.
453
461
 
454
462
  ## License
455
463
 
@@ -32,8 +32,11 @@ module ActionCable
32
32
  #
33
33
  # == Action processing
34
34
  #
35
- # Unlike Action Controllers, channels do not follow a REST constraint form for its actions. It's a remote-procedure call model. You can
36
- # declare any public method on the channel (optionally taking a data argument), and this method is automatically exposed as callable to the client.
35
+ # Unlike subclasses of ActionController::Base, channels do not follow a RESTful
36
+ # constraint form for their actions. Instead, Action Cable operates through a
37
+ # remote-procedure call model. You can declare any public method on the
38
+ # channel (optionally taking a <tt>data</tt> argument), and this method is
39
+ # automatically exposed as callable to the client.
37
40
  #
38
41
  # Example:
39
42
  #
@@ -60,18 +63,22 @@ module ActionCable
60
63
  # end
61
64
  # end
62
65
  #
63
- # In this example, subscribed/unsubscribed are not callable methods, as they were already declared in ActionCable::Channel::Base, but #appear/away
64
- # are. #generate_connection_token is also not callable as its a private method. You'll see that appear accepts a data parameter, which it then
65
- # uses as part of its model call. #away does not, it's simply a trigger action.
66
+ # In this example, the subscribed and unsubscribed methods are not callable methods, as they
67
+ # were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
68
+ # and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not
69
+ # callable, since it's a private method. You'll see that appear accepts a data
70
+ # parameter, which it then uses as part of its model call. <tt>#away</tt>
71
+ # does not, since it's simply a trigger action.
66
72
  #
67
- # Also note that in this example, current_user is available because it was marked as an identifying attribute on the connection.
68
- # All such identifiers will automatically create a delegation method of the same name on the channel instance.
73
+ # Also note that in this example, <tt>current_user</tt> is available because
74
+ # it was marked as an identifying attribute on the connection. All such
75
+ # identifiers will automatically create a delegation method of the same name
76
+ # on the channel instance.
69
77
  #
70
78
  # == Rejecting subscription requests
71
79
  #
72
- # A channel can reject a subscription request in the #subscribed callback by invoking #reject!
73
- #
74
- # Example:
80
+ # A channel can reject a subscription request in the #subscribed callback by
81
+ # invoking the #reject method:
75
82
  #
76
83
  # class ChatChannel < ApplicationCable::Channel
77
84
  # def subscribed
@@ -80,8 +87,10 @@ module ActionCable
80
87
  # end
81
88
  # end
82
89
  #
83
- # In this example, the subscription will be rejected if the current_user does not have access to the chat room.
84
- # On the client-side, Channel#rejected callback will get invoked when the server rejects the subscription request.
90
+ # In this example, the subscription will be rejected if the
91
+ # <tt>current_user</tt> does not have access to the chat room. On the
92
+ # client-side, the <tt>Channel#rejected</tt> callback will get invoked when
93
+ # the server rejects the subscription request.
85
94
  class Base
86
95
  include Callbacks
87
96
  include PeriodicTimers
@@ -116,7 +125,7 @@ module ActionCable
116
125
  protected
117
126
  # action_methods are cached and there is sometimes need to refresh
118
127
  # them. ::clear_action_methods! allows you to do that, so next time
119
- # you run action_methods, they will be recalculated
128
+ # you run action_methods, they will be recalculated.
120
129
  def clear_action_methods!
121
130
  @action_methods = nil
122
131
  end
@@ -157,9 +166,9 @@ module ActionCable
157
166
  end
158
167
  end
159
168
 
160
- # Called by the cable connection when its cut so the channel has a chance to cleanup with callbacks.
169
+ # Called by the cable connection when its cut, so the channel has a chance to cleanup with callbacks.
161
170
  # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
162
- def unsubscribe_from_channel
171
+ def unsubscribe_from_channel # :nodoc:
163
172
  run_callbacks :unsubscribe do
164
173
  unsubscribed
165
174
  end
@@ -174,7 +183,7 @@ module ActionCable
174
183
  end
175
184
 
176
185
  # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
177
- # people as offline or the like.
186
+ # users as offline or the like.
178
187
  def unsubscribed
179
188
  # Override in subclasses
180
189
  end
@@ -182,7 +191,7 @@ module ActionCable
182
191
  # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
183
192
  # the proper channel identifier marked as the recipient.
184
193
  def transmit(data, via: nil)
185
- logger.info "#{self.class.name} transmitting #{data.inspect}".tap { |m| m << " (via #{via})" if via }
194
+ logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via }
186
195
  connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data)
187
196
  end
188
197
 
@@ -215,7 +224,6 @@ module ActionCable
215
224
  end
216
225
  end
217
226
 
218
-
219
227
  def subscribe_to_channel
220
228
  run_callbacks :subscribe do
221
229
  subscribed
@@ -228,7 +236,6 @@ module ActionCable
228
236
  end
229
237
  end
230
238
 
231
-
232
239
  def extract_action(data)
233
240
  (data['action'].presence || :receive).to_sym
234
241
  end
@@ -12,7 +12,7 @@ module ActionCable
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- # Allow you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful
15
+ # Allows you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful
16
16
  # for sending a steady flow of updates to a client based off an object that was configured on subscription.
17
17
  # It's an alternative to using streams if the channel is able to do the work internally.
18
18
  def periodically(callback, every:)
@@ -1,8 +1,8 @@
1
1
  module ActionCable
2
2
  module Channel
3
3
  # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pub/sub queue where any data
4
- # put 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'll not get that update when connecting later.
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.
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
@@ -18,8 +18,10 @@ module ActionCable
18
18
  # end
19
19
  # end
20
20
  #
21
- # So the subscribers of this channel will get whatever data is put into the, let's say, `comments_for_45` broadcasting as soon as it's put there.
22
- # That looks like so from that side of things:
21
+ # Based on the above example, the subscribers of this channel will get whatever data is put into the,
22
+ # let's say, `comments_for_45` broadcasting as soon as it's put there.
23
+ #
24
+ # An example broadcasting for this channel looks like so:
23
25
  #
24
26
  # ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell'
25
27
  #
@@ -37,26 +39,27 @@ module ActionCable
37
39
  #
38
40
  # CommentsChannel.broadcast_to(@post, @comment)
39
41
  #
40
- # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can supply a callback that lets you alter what goes out.
41
- # Example below shows how you can use this to provide performance introspection in the process:
42
+ # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out.
43
+ # The below example shows how you can use this to provide performance introspection in the process:
42
44
  #
43
45
  # class ChatChannel < ApplicationCable::Channel
44
- # def subscribed
45
- # @room = Chat::Room[params[:room_number]]
46
+ # def subscribed
47
+ # @room = Chat::Room[params[:room_number]]
46
48
  #
47
- # stream_for @room, -> (encoded_message) do
48
- # message = ActiveSupport::JSON.decode(encoded_message)
49
+ # stream_for @room, -> (encoded_message) do
50
+ # message = ActiveSupport::JSON.decode(encoded_message)
49
51
  #
50
- # if message['originated_at'].present?
51
- # elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
52
+ # if message['originated_at'].present?
53
+ # elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
52
54
  #
53
- # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
54
- # logger.info "Message took #{elapsed_time}s to arrive"
55
- # end
55
+ # ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
56
+ # logger.info "Message took #{elapsed_time}s to arrive"
57
+ # end
56
58
  #
57
- # transmit message
58
- # end
59
- # end
59
+ # transmit message
60
+ # end
61
+ # end
62
+ # end
60
63
  #
61
64
  # You can stop streaming from all broadcasts by calling #stop_all_streams.
62
65
  module Streams
@@ -69,7 +72,7 @@ module ActionCable
69
72
  # Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
70
73
  # instead of the default of just transmitting the updates straight to the subscriber.
71
74
  def stream_from(broadcasting, callback = nil)
72
- # Hold off the confirmation until pubsub#subscribe is successful
75
+ # Don't send the confirmation until pubsub#subscribe is successful
73
76
  defer_subscription_confirmation!
74
77
 
75
78
  callback ||= default_stream_callback(broadcasting)
@@ -90,6 +93,7 @@ module ActionCable
90
93
  stream_from(broadcasting_for([ channel_name, model ]), callback)
91
94
  end
92
95
 
96
+ # Unsubscribes all streams associated with this channel from the pubsub queue.
93
97
  def stop_all_streams
94
98
  streams.each do |broadcasting, callback|
95
99
  pubsub.unsubscribe broadcasting, callback
@@ -2,9 +2,9 @@ require 'action_dispatch'
2
2
 
3
3
  module ActionCable
4
4
  module Connection
5
- # For every WebSocket the cable server is accepting, a Connection object will be instantiated. This instance becomes the parent
6
- # of all the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
7
- # based on an identifier sent by the cable consumer. The Connection itself does not deal with any specific application logic beyond
5
+ # For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
6
+ # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
7
+ # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
8
8
  # authentication and authorization.
9
9
  #
10
10
  # Here's a basic example:
@@ -33,9 +33,9 @@ module ActionCable
33
33
  # end
34
34
  # end
35
35
  #
36
- # First, we declare that this connection can be identified by its current_user. This allows us later to be able to find all connections
37
- # established for that current_user (and potentially disconnect them if the user was removed from an account). You can declare as many
38
- # identification indexes as you like. Declaring an identification means that a attr_accessor is automatically set for that key.
36
+ # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
37
+ # established for that current_user (and potentially disconnect them). You can declare as many
38
+ # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
39
39
  #
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.
@@ -65,8 +65,8 @@ module ActionCable
65
65
  end
66
66
 
67
67
  # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
68
- # This method should not be called directly. Rely on the #connect (and #disconnect) callback instead.
69
- def process
68
+ # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
69
+ def process # :nodoc:
70
70
  logger.info started_request_message
71
71
 
72
72
  if websocket.possible? && allow_request_origin?
@@ -76,7 +76,7 @@ module ActionCable
76
76
  end
77
77
  end
78
78
 
79
- # Data received over the cable is handled by this method. It's expected that everything inbound is JSON encoded.
79
+ # Data received over the WebSocket connection is handled by this method. It's expected that everything inbound is JSON encoded.
80
80
  # The data is routed to the proper channel that the connection has subscribed to.
81
81
  def receive(data_in_json)
82
82
  if websocket.alive?
@@ -88,7 +88,7 @@ module ActionCable
88
88
 
89
89
  # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the
90
90
  # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON.
91
- def transmit(data)
91
+ def transmit(data) # :nodoc:
92
92
  websocket.transmit data
93
93
  end
94
94
 
@@ -154,7 +154,7 @@ module ActionCable
154
154
  def handle_open
155
155
  connect if respond_to?(:connect)
156
156
  subscribe_to_internal_channel
157
- beat
157
+ confirm_connection_monitor_subscription
158
158
 
159
159
  message_buffer.process!
160
160
  server.add_connection(self)
@@ -173,6 +173,13 @@ module ActionCable
173
173
  disconnect if respond_to?(:disconnect)
174
174
  end
175
175
 
176
+ def confirm_connection_monitor_subscription
177
+ # Send confirmation message to the internal connection monitor channel.
178
+ # This ensures the connection monitor state is reset after a successful
179
+ # websocket connection.
180
+ transmit ActiveSupport::JSON.encode(identifier: ActionCable::INTERNAL[:identifiers][:ping], type: ActionCable::INTERNAL[:message_types][:confirmation])
181
+ end
182
+
176
183
  def allow_request_origin?
177
184
  return true if server.config.disable_request_forgery_protection
178
185
 
@@ -185,12 +192,14 @@ module ActionCable
185
192
  end
186
193
 
187
194
  def respond_to_successful_request
195
+ logger.info successful_request_message
188
196
  websocket.rack_response
189
197
  end
190
198
 
191
199
  def respond_to_invalid_request
192
200
  close if websocket.alive?
193
201
 
202
+ logger.error invalid_request_message
194
203
  logger.info finished_request_message
195
204
  [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ]
196
205
  end
@@ -205,7 +214,7 @@ module ActionCable
205
214
  'Started %s "%s"%s for %s at %s' % [
206
215
  request.request_method,
207
216
  request.filtered_path,
208
- websocket.possible? ? ' [WebSocket]' : '',
217
+ websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
209
218
  request.ip,
210
219
  Time.now.to_s ]
211
220
  end
@@ -213,10 +222,22 @@ module ActionCable
213
222
  def finished_request_message
214
223
  'Finished "%s"%s for %s at %s' % [
215
224
  request.filtered_path,
216
- websocket.possible? ? ' [WebSocket]' : '',
225
+ websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
217
226
  request.ip,
218
227
  Time.now.to_s ]
219
228
  end
229
+
230
+ def invalid_request_message
231
+ 'Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [
232
+ env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
233
+ ]
234
+ end
235
+
236
+ def successful_request_message
237
+ 'Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [
238
+ env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
239
+ ]
240
+ end
220
241
  end
221
242
  end
222
243
  end
@@ -132,11 +132,8 @@ module ActionCable
132
132
  @ready_state = CLOSING
133
133
  @close_params = [reason, code]
134
134
 
135
- if @stream
136
- @stream.shutdown
137
- else
138
- finalize_close
139
- end
135
+ @stream.shutdown if @stream
136
+ finalize_close
140
137
  end
141
138
 
142
139
  def finalize_close
@@ -12,7 +12,7 @@ module ActionCable
12
12
 
13
13
  class_methods do
14
14
  # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
15
- # Common identifiers are current_user and current_account, but could be anything really.
15
+ # Common identifiers are current_user and current_account, but could be anything, really.
16
16
  #
17
17
  # Note that anything marked as an identifier will automatically create a delegate by the same name on any
18
18
  # channel instances created off the connection.
@@ -1,8 +1,7 @@
1
1
  module ActionCable
2
2
  module Connection
3
- # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized and is ready to receive them.
4
- # Entirely internal operation and should not be used directly by the user.
5
- class MessageBuffer
3
+ # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
4
+ class MessageBuffer # :nodoc:
6
5
  def initialize(connection)
7
6
  @connection = connection
8
7
  @buffered_messages = []
@@ -3,8 +3,8 @@ require 'active_support/core_ext/hash/indifferent_access'
3
3
  module ActionCable
4
4
  module Connection
5
5
  # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
6
- # the connection to the proper channel. Should not be used directly by the user.
7
- class Subscriptions
6
+ # the connection to the proper channel.
7
+ class Subscriptions # :nodoc:
8
8
  def initialize(connection)
9
9
  @connection = connection
10
10
  @subscriptions = {}
@@ -54,7 +54,7 @@ module ActionCable
54
54
  end
55
55
 
56
56
  def unsubscribe_from_all
57
- subscriptions.each { |id, channel| channel.unsubscribe_from_channel }
57
+ subscriptions.each { |id, channel| remove_subscription(channel) }
58
58
  end
59
59
 
60
60
  protected
@@ -31,6 +31,12 @@ module ActionCable
31
31
  self.cable = Rails.application.config_for(config_path).with_indifferent_access
32
32
  end
33
33
 
34
+ if 'ApplicationCable::Connection'.safe_constantize
35
+ self.connection_class = ApplicationCable::Connection
36
+ end
37
+
38
+ self.channel_paths = Rails.application.paths['app/channels'].existent
39
+
34
40
  options.each { |k,v| send("#{k}=", v) }
35
41
  end
36
42
  end
@@ -8,7 +8,7 @@ module ActionCable
8
8
  MAJOR = 5
9
9
  MINOR = 0
10
10
  TINY = 0
11
- PRE = "beta2"
11
+ PRE = "beta3"
12
12
 
13
13
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
14
  end
@@ -9,7 +9,7 @@ module ActionCable
9
9
  # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
10
10
  # </head>
11
11
  #
12
- # This is then used by ActionCable to determine the url of your websocket server.
12
+ # This is then used by Action Cable to determine the url of your WebSocket server.
13
13
  # Your CoffeeScript can then connect to the server without needing to specify the
14
14
  # url directly:
15
15
  #
@@ -1,6 +1,7 @@
1
1
  module ActionCable
2
- # If you need to disconnect a given connection, you go through the RemoteConnections. You find the connections you're looking for by
3
- # searching the identifier declared on the connection. Example:
2
+ # If you need to disconnect a given connection, you can go through the
3
+ # RemoteConnections. You can find the connections you're looking for by
4
+ # searching for the identifier declared on the connection. For example:
4
5
  #
5
6
  # module ApplicationCable
6
7
  # class Connection < ActionCable::Connection::Base
@@ -11,8 +12,9 @@ module ActionCable
11
12
  #
12
13
  # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
13
14
  #
14
- # That will disconnect all the connections established for User.find(1) across all servers running on all machines (because it uses
15
- # the internal channel that all these servers are subscribed to).
15
+ # This will disconnect all the connections established for
16
+ # <tt>User.find(1)</tt>, across all servers running on all machines, because
17
+ # it uses the internal channel that all of these servers are subscribed to.
16
18
  class RemoteConnections
17
19
  attr_reader :server
18
20
 
@@ -25,7 +27,7 @@ module ActionCable
25
27
  end
26
28
 
27
29
  private
28
- # Represents a single remote connection found via ActionCable.server.remote_connections.where(*).
30
+ # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
29
31
  # Exists for the solely for the purpose of calling #disconnect on that connection.
30
32
  class RemoteConnection
31
33
  class InvalidIdentifiersError < StandardError; end
@@ -2,10 +2,10 @@ require 'thread'
2
2
 
3
3
  module ActionCable
4
4
  module Server
5
- # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the rack process that starts the cable server, but
6
- # also by the user to reach the RemoteConnections instead for finding and disconnecting connections across all servers.
5
+ # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
6
+ # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
7
7
  #
8
- # Also, this is the server instance used for broadcasting. See Broadcasting for details.
8
+ # Also, this is the server instance used for broadcasting. See Broadcasting for more information.
9
9
  class Base
10
10
  include ActionCable::Server::Broadcasting
11
11
  include ActionCable::Server::Connections
@@ -19,11 +19,10 @@ module ActionCable
19
19
 
20
20
  def initialize
21
21
  @mutex = Mutex.new
22
-
23
22
  @remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
24
23
  end
25
24
 
26
- # Called by rack to setup the server.
25
+ # Called by Rack to setup the server.
27
26
  def call(env)
28
27
  setup_heartbeat_timer
29
28
  config.connection_class.new(self, env).process
@@ -48,7 +47,7 @@ module ActionCable
48
47
  @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
49
48
  end
50
49
 
51
- # Requires and returns a hash of all the channel class constants keyed by name.
50
+ # Requires and returns a hash of all of the channel class constants, which are keyed by name.
52
51
  def channel_classes
53
52
  @channel_classes || @mutex.synchronize do
54
53
  @channel_classes ||= begin
@@ -63,7 +62,7 @@ module ActionCable
63
62
  @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
64
63
  end
65
64
 
66
- # All the identifiers applied to the connection class associated with this server.
65
+ # All of the identifiers applied to the connection class associated with this server.
67
66
  def connection_identifiers
68
67
  config.connection_class.identifiers
69
68
  end
@@ -1,29 +1,29 @@
1
1
  module ActionCable
2
2
  module Server
3
- # Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these
3
+ # 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
4
4
  # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
5
5
  #
6
6
  # class WebNotificationsChannel < ApplicationCable::Channel
7
- # def subscribed
8
- # stream_from "web_notifications_#{current_user.id}"
9
- # end
10
- # end
7
+ # def subscribed
8
+ # stream_from "web_notifications_#{current_user.id}"
9
+ # end
10
+ # end
11
11
  #
12
- # # Somewhere in your app this is called, perhaps from a NewCommentJob
13
- # ActionCable.server.broadcast \
14
- # "web_notifications_1", { title: 'New things!', body: 'All shit fit for print' }
12
+ # # Somewhere in your app this is called, perhaps from a NewCommentJob:
13
+ # ActionCable.server.broadcast \
14
+ # "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
15
15
  #
16
- # # Client-side coffescript, which assumes you've already requested the right to send web notifications
17
- # App.cable.subscriptions.create "WebNotificationsChannel",
18
- # received: (data) ->
19
- # new Notification data['title'], body: data['body']
16
+ # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
17
+ # App.cable.subscriptions.create "WebNotificationsChannel",
18
+ # received: (data) ->
19
+ # new Notification data['title'], body: data['body']
20
20
  module Broadcasting
21
- # Broadcast a hash directly to a named <tt>broadcasting</tt>. It'll automatically be JSON encoded.
21
+ # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
22
22
  def broadcast(broadcasting, message)
23
23
  broadcaster_for(broadcasting).broadcast(message)
24
24
  end
25
25
 
26
- # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have a object that
26
+ # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
27
27
  # may need multiple spots to transmit to a specific broadcasting over and over.
28
28
  def broadcaster_for(broadcasting)
29
29
  Broadcaster.new(self, broadcasting)
@@ -1,31 +1,24 @@
1
1
  module ActionCable
2
2
  module Server
3
- # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points
3
+ # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
4
4
  # in a Rails config initializer.
5
5
  class Configuration
6
6
  attr_accessor :logger, :log_tags
7
7
  attr_accessor :connection_class, :worker_pool_size
8
- attr_accessor :channel_load_paths
9
8
  attr_accessor :disable_request_forgery_protection, :allowed_request_origins
10
9
  attr_accessor :cable, :url
11
10
 
11
+ attr_accessor :channel_paths # :nodoc:
12
+
12
13
  def initialize
13
14
  @log_tags = []
14
15
 
15
- @connection_class = ApplicationCable::Connection
16
- @worker_pool_size = 100
17
-
18
- @channel_load_paths = [Rails.root.join('app/channels')]
16
+ @connection_class = ActionCable::Connection::Base
17
+ @worker_pool_size = 100
19
18
 
20
19
  @disable_request_forgery_protection = false
21
20
  end
22
21
 
23
- def channel_paths
24
- @channel_paths ||= channel_load_paths.flat_map do |path|
25
- Dir["#{path}/**/*_channel.rb"]
26
- end
27
- end
28
-
29
22
  def channel_class_names
30
23
  @channel_class_names ||= channel_paths.collect do |channel_path|
31
24
  Pathname.new(channel_path).basename.to_s.split('.').first.camelize
@@ -1,9 +1,8 @@
1
1
  module ActionCable
2
2
  module Server
3
- # Collection class for all the connections that's been established on this specific server. Remember, usually you'll run many cable servers, so
4
- # you can't use this collection as an full list of all the connections established against your application. Use RemoteConnections for that.
5
- # As such, this is primarily for internal use.
6
- module Connections
3
+ # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
4
+ # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
5
+ module Connections # :nodoc:
7
6
  BEAT_INTERVAL = 3
8
7
 
9
8
  def connections
@@ -19,7 +18,7 @@ module ActionCable
19
18
  end
20
19
 
21
20
  # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
22
- # then can't rely on being able to receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically
21
+ # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
23
22
  # disconnect.
24
23
  def setup_heartbeat_timer
25
24
  @heartbeat_timer ||= Concurrent::TimerTask.new(execution_interval: BEAT_INTERVAL) do
@@ -4,8 +4,8 @@ require 'concurrent'
4
4
 
5
5
  module ActionCable
6
6
  module Server
7
- # Worker used by Server.send_async to do connection work in threads. Only for internal use.
8
- class Worker
7
+ # Worker used by Server.send_async to do connection work in threads.
8
+ class Worker # :nodoc:
9
9
  include ActiveSupport::Callbacks
10
10
 
11
11
  thread_mattr_accessor :connection
@@ -1,7 +1,7 @@
1
1
  module ActionCable
2
2
  module Server
3
3
  class Worker
4
- # Clear active connections between units of work so the long-running channel or connection processes do not hoard connections.
4
+ # Clear active connections between units of work so that way long-running channels or connection processes do not hoard connections.
5
5
  module ActiveRecordConnectionManagement
6
6
  extend ActiveSupport::Concern
7
7
 
@@ -19,4 +19,4 @@ module ActionCable
19
19
  end
20
20
  end
21
21
  end
22
- end
22
+ end
@@ -13,6 +13,14 @@ module ActionCable
13
13
  class EventedRedis < Base # :nodoc:
14
14
  @@mutex = Mutex.new
15
15
 
16
+ # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis.
17
+ # This is needed, for example, when using Makara proxies for distributed Redis.
18
+ cattr_accessor(:em_redis_connector) { ->(config) { EM::Hiredis.connect(config[:url]) } }
19
+
20
+ # Overwrite this factory method for Redis connections if you want to use a different Redis connection library than Redis.
21
+ # This is needed, for example, when using Makara proxies for distributed Redis.
22
+ cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } }
23
+
16
24
  def initialize(*)
17
25
  super
18
26
  @redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil
@@ -41,7 +49,7 @@ module ActionCable
41
49
  def redis_connection_for_subscriptions
42
50
  ensure_reactor_running
43
51
  @redis_connection_for_subscriptions || @server.mutex.synchronize do
44
- @redis_connection_for_subscriptions ||= EM::Hiredis.connect(@server.config.cable[:url]).tap do |redis|
52
+ @redis_connection_for_subscriptions ||= self.class.em_redis_connector.call(@server.config.cable).tap do |redis|
45
53
  redis.on(:reconnect_failed) do
46
54
  @logger.info "[ActionCable] Redis reconnect failed."
47
55
  end
@@ -51,7 +59,7 @@ module ActionCable
51
59
 
52
60
  def redis_connection_for_broadcasts
53
61
  @redis_connection_for_broadcasts || @server.mutex.synchronize do
54
- @redis_connection_for_broadcasts ||= ::Redis.new(@server.config.cable)
62
+ @redis_connection_for_broadcasts ||= self.class.redis_connector.call(@server.config.cable)
55
63
  end
56
64
  end
57
65
 
@@ -6,6 +6,10 @@ require 'redis'
6
6
  module ActionCable
7
7
  module SubscriptionAdapter
8
8
  class Redis < Base # :nodoc:
9
+ # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis.
10
+ # This is needed, for example, when using Makara proxies for distributed Redis.
11
+ cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } }
12
+
9
13
  def initialize(*)
10
14
  super
11
15
  @listener = nil
@@ -39,7 +43,7 @@ module ActionCable
39
43
 
40
44
  def redis_connection_for_broadcasts
41
45
  @redis_connection_for_broadcasts || @server.mutex.synchronize do
42
- @redis_connection_for_broadcasts ||= ::Redis.new(@server.config.cable)
46
+ @redis_connection_for_broadcasts ||= self.class.redis_connector.call(@server.config.cable)
43
47
  end
44
48
  end
45
49
 
@@ -1,4 +1,6 @@
1
1
  (function() {
2
+ var slice = [].slice;
3
+
2
4
  this.ActionCable = {
3
5
  INTERNAL: {
4
6
  "identifiers": {
@@ -31,6 +33,20 @@
31
33
  } else {
32
34
  return url;
33
35
  }
36
+ },
37
+ startDebugging: function() {
38
+ return this.debugging = true;
39
+ },
40
+ stopDebugging: function() {
41
+ return this.debugging = null;
42
+ },
43
+ log: function() {
44
+ var messages;
45
+ messages = 1 <= arguments.length ? slice.call(arguments, 0) : [];
46
+ if (this.debugging) {
47
+ messages.push(Date.now());
48
+ return console.log.apply(console, ["[ActionCable]"].concat(slice.call(messages)));
49
+ }
34
50
  }
35
51
  };
36
52
 
@@ -62,9 +78,14 @@
62
78
  };
63
79
 
64
80
  Connection.prototype.open = function() {
65
- if (this.webSocket && !this.isState("closed")) {
81
+ if (this.isAlive()) {
82
+ ActionCable.log("Attemped to open WebSocket, but existing socket is " + (this.getState()));
66
83
  throw new Error("Existing connection must be closed before opening");
67
84
  } else {
85
+ ActionCable.log("Opening WebSocket, current state is " + (this.getState()));
86
+ if (this.webSocket != null) {
87
+ this.uninstallEventHandlers();
88
+ }
68
89
  this.webSocket = new WebSocket(this.consumer.url);
69
90
  this.installEventHandlers();
70
91
  return true;
@@ -77,14 +98,20 @@
77
98
  };
78
99
 
79
100
  Connection.prototype.reopen = function() {
80
- if (this.isState("closed")) {
81
- return this.open();
82
- } else {
101
+ var error, error1;
102
+ ActionCable.log("Reopening WebSocket, current state is " + (this.getState()));
103
+ if (this.isAlive()) {
83
104
  try {
84
105
  return this.close();
106
+ } catch (error1) {
107
+ error = error1;
108
+ return ActionCable.log("Failed to reopen WebSocket", error);
85
109
  } finally {
110
+ ActionCable.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
86
111
  setTimeout(this.open, this.constructor.reopenDelay);
87
112
  }
113
+ } else {
114
+ return this.open();
88
115
  }
89
116
  };
90
117
 
@@ -92,6 +119,10 @@
92
119
  return this.isState("open");
93
120
  };
94
121
 
122
+ Connection.prototype.isAlive = function() {
123
+ return (this.webSocket != null) && !this.isState("closing", "closed");
124
+ };
125
+
95
126
  Connection.prototype.isState = function() {
96
127
  var ref, states;
97
128
  states = 1 <= arguments.length ? slice.call(arguments, 0) : [];
@@ -117,6 +148,13 @@
117
148
  }
118
149
  };
119
150
 
151
+ Connection.prototype.uninstallEventHandlers = function() {
152
+ var eventName;
153
+ for (eventName in this.events) {
154
+ this.webSocket["on" + eventName] = function() {};
155
+ }
156
+ };
157
+
120
158
  Connection.prototype.events = {
121
159
  message: function(event) {
122
160
  var identifier, message, ref, type;
@@ -131,13 +169,16 @@
131
169
  }
132
170
  },
133
171
  open: function() {
172
+ ActionCable.log("WebSocket onopen event");
134
173
  this.disconnected = false;
135
174
  return this.consumer.subscriptions.reload();
136
175
  },
137
176
  close: function() {
177
+ ActionCable.log("WebSocket onclose event");
138
178
  return this.disconnect();
139
179
  },
140
180
  error: function() {
181
+ ActionCable.log("WebSocket onerror event");
141
182
  return this.disconnect();
142
183
  }
143
184
  };
@@ -180,7 +221,8 @@
180
221
  ConnectionMonitor.prototype.connected = function() {
181
222
  this.reset();
182
223
  this.pingedAt = now();
183
- return delete this.disconnectedAt;
224
+ delete this.disconnectedAt;
225
+ return ActionCable.log("ConnectionMonitor connected");
184
226
  };
185
227
 
186
228
  ConnectionMonitor.prototype.disconnected = function() {
@@ -200,12 +242,14 @@
200
242
  delete this.stoppedAt;
201
243
  this.startedAt = now();
202
244
  this.poll();
203
- return document.addEventListener("visibilitychange", this.visibilityDidChange);
245
+ document.addEventListener("visibilitychange", this.visibilityDidChange);
246
+ return ActionCable.log("ConnectionMonitor started, pollInterval is " + (this.getInterval()) + "ms");
204
247
  };
205
248
 
206
249
  ConnectionMonitor.prototype.stop = function() {
207
250
  this.stoppedAt = now();
208
- return document.removeEventListener("visibilitychange", this.visibilityDidChange);
251
+ document.removeEventListener("visibilitychange", this.visibilityDidChange);
252
+ return ActionCable.log("ConnectionMonitor stopped");
209
253
  };
210
254
 
211
255
  ConnectionMonitor.prototype.poll = function() {
@@ -228,8 +272,12 @@
228
272
 
229
273
  ConnectionMonitor.prototype.reconnectIfStale = function() {
230
274
  if (this.connectionIsStale()) {
275
+ ActionCable.log("ConnectionMonitor detected stale connection, reconnectAttempts = " + this.reconnectAttempts);
231
276
  this.reconnectAttempts++;
232
- if (!this.disconnectedRecently()) {
277
+ if (this.disconnectedRecently()) {
278
+ return ActionCable.log("ConnectionMonitor skipping reopen because recently disconnected at " + this.disconnectedAt);
279
+ } else {
280
+ ActionCable.log("ConnectionMonitor reopening");
233
281
  return this.consumer.connection.reopen();
234
282
  }
235
283
  }
@@ -249,6 +297,7 @@
249
297
  return setTimeout((function(_this) {
250
298
  return function() {
251
299
  if (_this.connectionIsStale() || !_this.consumer.connection.isOpen()) {
300
+ ActionCable.log("ConnectionMonitor reopening stale connection after visibilitychange to " + document.visibilityState);
252
301
  return _this.consumer.connection.reopen();
253
302
  }
254
303
  };
@@ -15,12 +15,29 @@ module Rails
15
15
  if options[:assets]
16
16
  template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee")
17
17
  end
18
+
19
+ generate_application_cable_files
18
20
  end
19
21
 
20
22
  protected
21
23
  def file_name
22
24
  @_file_name ||= super.gsub(/\_channel/i, '')
23
25
  end
26
+
27
+ # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
28
+ def generate_application_cable_files
29
+ return if self.behavior != :invoke
30
+
31
+ files = [
32
+ 'application_cable/channel.rb',
33
+ 'application_cable/connection.rb'
34
+ ]
35
+
36
+ files.each do |name|
37
+ path = File.join('app/channels/', name)
38
+ template(name, path) if !File.exist?(path)
39
+ end
40
+ end
24
41
  end
25
42
  end
26
43
  end
@@ -0,0 +1,5 @@
1
+ # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
2
+ module ApplicationCable
3
+ class Channel < ActionCable::Channel::Base
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
2
+ module ApplicationCable
3
+ class Connection < ActionCable::Connection::Base
4
+ end
5
+ end
@@ -1,4 +1,4 @@
1
- # Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading.
1
+ # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
2
2
  <% module_namespacing do -%>
3
3
  class <%= class_name %>Channel < ApplicationCable::Channel
4
4
  def subscribed
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actioncable
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0.beta2
4
+ version: 5.0.0.beta3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pratik Naik
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-02-01 00:00:00.000000000 Z
12
+ date: 2016-02-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - '='
19
19
  - !ruby/object:Gem::Version
20
- version: 5.0.0.beta2
20
+ version: 5.0.0.beta3
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - '='
26
26
  - !ruby/object:Gem::Version
27
- version: 5.0.0.beta2
27
+ version: 5.0.0.beta3
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: nio4r
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +108,8 @@ files:
108
108
  - lib/assets/compiled/action_cable.js
109
109
  - lib/rails/generators/channel/USAGE
110
110
  - lib/rails/generators/channel/channel_generator.rb
111
+ - lib/rails/generators/channel/templates/application_cable/channel.rb
112
+ - lib/rails/generators/channel/templates/application_cable/connection.rb
111
113
  - lib/rails/generators/channel/templates/assets/channel.coffee
112
114
  - lib/rails/generators/channel/templates/channel.rb
113
115
  homepage: http://rubyonrails.org
@@ -130,9 +132,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
132
  version: 1.3.1
131
133
  requirements: []
132
134
  rubyforge_project:
133
- rubygems_version: 2.5.2
135
+ rubygems_version: 2.5.1
134
136
  signing_key:
135
137
  specification_version: 4
136
138
  summary: WebSocket framework for Rails.
137
139
  test_files: []
138
- has_rdoc: