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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +39 -31
- data/lib/action_cable/channel/base.rb +26 -19
- data/lib/action_cable/channel/periodic_timers.rb +1 -1
- data/lib/action_cable/channel/streams.rb +23 -19
- data/lib/action_cable/connection/base.rb +34 -13
- data/lib/action_cable/connection/client_socket.rb +2 -5
- data/lib/action_cable/connection/identification.rb +1 -1
- data/lib/action_cable/connection/message_buffer.rb +2 -3
- data/lib/action_cable/connection/subscriptions.rb +3 -3
- data/lib/action_cable/engine.rb +6 -0
- data/lib/action_cable/gem_version.rb +1 -1
- data/lib/action_cable/helpers/action_cable_helper.rb +1 -1
- data/lib/action_cable/remote_connections.rb +7 -5
- data/lib/action_cable/server/base.rb +6 -7
- data/lib/action_cable/server/broadcasting.rb +14 -14
- data/lib/action_cable/server/configuration.rb +5 -12
- data/lib/action_cable/server/connections.rb +4 -5
- data/lib/action_cable/server/worker.rb +2 -2
- data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -2
- data/lib/action_cable/subscription_adapter/evented_redis.rb +10 -2
- data/lib/action_cable/subscription_adapter/redis.rb +5 -1
- data/lib/assets/compiled/action_cable.js +57 -8
- data/lib/rails/generators/channel/channel_generator.rb +17 -0
- data/lib/rails/generators/channel/templates/application_cable/channel.rb +5 -0
- data/lib/rails/generators/channel/templates/application_cable/connection.rb +5 -0
- data/lib/rails/generators/channel/templates/channel.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3e7b9ae96c4f9f359281abaa75f8b909a8fb04f
|
4
|
+
data.tar.gz: 41a9536aea144e25988a510069c3b07b0250aa5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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:
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
351
|
-
for 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
|
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
|
-
|
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
|
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
|
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
|
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
|
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 [
|
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
|
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.
|
452
|
-
|
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
|
36
|
-
#
|
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
|
64
|
-
#
|
65
|
-
#
|
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
|
68
|
-
#
|
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
|
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
|
84
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
5
|
-
# streaming a broadcasting at the very moment it sends out an update, you
|
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
|
-
#
|
22
|
-
#
|
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
|
41
|
-
#
|
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
|
-
#
|
45
|
-
#
|
46
|
+
# def subscribed
|
47
|
+
# @room = Chat::Room[params[:room_number]]
|
46
48
|
#
|
47
|
-
#
|
48
|
-
#
|
49
|
+
# stream_for @room, -> (encoded_message) do
|
50
|
+
# message = ActiveSupport::JSON.decode(encoded_message)
|
49
51
|
#
|
50
|
-
#
|
51
|
-
#
|
52
|
+
# if message['originated_at'].present?
|
53
|
+
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
|
52
54
|
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
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
|
-
#
|
58
|
-
#
|
59
|
-
#
|
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
|
-
#
|
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
|
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
|
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
|
37
|
-
# established for that current_user (and potentially disconnect them
|
38
|
-
# identification indexes as you like. Declaring an identification means that
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
57
|
+
subscriptions.each { |id, channel| remove_subscription(channel) }
|
58
58
|
end
|
59
59
|
|
60
60
|
protected
|
data/lib/action_cable/engine.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
3
|
-
#
|
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
|
-
#
|
15
|
-
#
|
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
|
6
|
-
# also by the user to reach the RemoteConnections
|
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
|
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
|
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
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
7
|
+
# def subscribed
|
8
|
+
# stream_from "web_notifications_#{current_user.id}"
|
9
|
+
# end
|
10
|
+
# end
|
11
11
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
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
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
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>.
|
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
|
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
|
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
|
16
|
-
@worker_pool_size
|
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
|
4
|
-
# you can't use this collection as
|
5
|
-
|
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
|
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.
|
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
|
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 ||=
|
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 ||=
|
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 ||=
|
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.
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 (
|
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
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Be sure to restart your server when you modify this file. Action Cable runs in
|
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.
|
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-
|
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.
|
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.
|
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.
|
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:
|