actioncable 0.0.0 → 5.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/MIT-LICENSE +20 -0
- data/README.md +439 -21
- data/lib/action_cable.rb +47 -2
- data/lib/action_cable/channel.rb +14 -0
- data/lib/action_cable/channel/base.rb +277 -0
- data/lib/action_cable/channel/broadcasting.rb +29 -0
- data/lib/action_cable/channel/callbacks.rb +35 -0
- data/lib/action_cable/channel/naming.rb +22 -0
- data/lib/action_cable/channel/periodic_timers.rb +41 -0
- data/lib/action_cable/channel/streams.rb +114 -0
- data/lib/action_cable/connection.rb +16 -0
- data/lib/action_cable/connection/authorization.rb +13 -0
- data/lib/action_cable/connection/base.rb +221 -0
- data/lib/action_cable/connection/identification.rb +46 -0
- data/lib/action_cable/connection/internal_channel.rb +45 -0
- data/lib/action_cable/connection/message_buffer.rb +54 -0
- data/lib/action_cable/connection/subscriptions.rb +76 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +40 -0
- data/lib/action_cable/connection/web_socket.rb +29 -0
- data/lib/action_cable/engine.rb +38 -0
- data/lib/action_cable/gem_version.rb +15 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +29 -0
- data/lib/action_cable/process/logging.rb +10 -0
- data/lib/action_cable/remote_connections.rb +64 -0
- data/lib/action_cable/server.rb +19 -0
- data/lib/action_cable/server/base.rb +77 -0
- data/lib/action_cable/server/broadcasting.rb +54 -0
- data/lib/action_cable/server/configuration.rb +35 -0
- data/lib/action_cable/server/connections.rb +37 -0
- data/lib/action_cable/server/worker.rb +42 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +22 -0
- data/lib/action_cable/version.rb +6 -1
- data/lib/assets/javascripts/action_cable.coffee.erb +23 -0
- data/lib/assets/javascripts/action_cable/connection.coffee +84 -0
- data/lib/assets/javascripts/action_cable/connection_monitor.coffee +84 -0
- data/lib/assets/javascripts/action_cable/consumer.coffee +31 -0
- data/lib/assets/javascripts/action_cable/subscription.coffee +68 -0
- data/lib/assets/javascripts/action_cable/subscriptions.coffee +78 -0
- data/lib/rails/generators/channel/USAGE +14 -0
- data/lib/rails/generators/channel/channel_generator.rb +21 -0
- data/lib/rails/generators/channel/templates/assets/channel.coffee +14 -0
- data/lib/rails/generators/channel/templates/channel.rb +17 -0
- metadata +161 -26
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -2
- data/actioncable.gemspec +0 -22
- data/bin/console +0 -14
- data/bin/setup +0 -7
data/lib/action_cable.rb
CHANGED
@@ -1,5 +1,50 @@
|
|
1
|
-
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2015 Basecamp, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'active_support'
|
25
|
+
require 'active_support/rails'
|
26
|
+
require 'action_cable/version'
|
2
27
|
|
3
28
|
module ActionCable
|
4
|
-
|
29
|
+
extend ActiveSupport::Autoload
|
30
|
+
|
31
|
+
INTERNAL = {
|
32
|
+
identifiers: {
|
33
|
+
ping: '_ping'.freeze
|
34
|
+
},
|
35
|
+
message_types: {
|
36
|
+
confirmation: 'confirm_subscription'.freeze,
|
37
|
+
rejection: 'reject_subscription'.freeze
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
# Singleton instance of the server
|
42
|
+
module_function def server
|
43
|
+
@server ||= ActionCable::Server::Base.new
|
44
|
+
end
|
45
|
+
|
46
|
+
autoload :Server
|
47
|
+
autoload :Connection
|
48
|
+
autoload :Channel
|
49
|
+
autoload :RemoteConnections
|
5
50
|
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Channel
|
5
|
+
# The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection.
|
6
|
+
# You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply
|
7
|
+
# responding to the subscriber's direct requests.
|
8
|
+
#
|
9
|
+
# Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then
|
10
|
+
# lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care
|
11
|
+
# not to do anything silly in a channel that would balloon its memory footprint or whatever. The references are forever, so they won't be released
|
12
|
+
# as is normally the case with a controller instance that gets thrown away after every request.
|
13
|
+
#
|
14
|
+
# Long-lived channels (and connections) also mean you're responsible for ensuring that the data is fresh. If you hold a reference to a user
|
15
|
+
# record, but the name is changed while that reference is held, you may be sending stale data if you don't take precautions to avoid it.
|
16
|
+
#
|
17
|
+
# The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests
|
18
|
+
# can interact with. Here's a quick example:
|
19
|
+
#
|
20
|
+
# class ChatChannel < ApplicationCable::Channel
|
21
|
+
# def subscribed
|
22
|
+
# @room = Chat::Room[params[:room_number]]
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def speak(data)
|
26
|
+
# @room.speak data, user: current_user
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that
|
31
|
+
# subscriber wants to say something in the room.
|
32
|
+
#
|
33
|
+
# == Action processing
|
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.
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
#
|
40
|
+
# class AppearanceChannel < ApplicationCable::Channel
|
41
|
+
# def subscribed
|
42
|
+
# @connection_token = generate_connection_token
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# def unsubscribed
|
46
|
+
# current_user.disappear @connection_token
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# def appear(data)
|
50
|
+
# current_user.appear @connection_token, on: data['appearing_on']
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# def away
|
54
|
+
# current_user.away @connection_token
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# private
|
58
|
+
# def generate_connection_token
|
59
|
+
# SecureRandom.hex(36)
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
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
|
+
#
|
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.
|
69
|
+
#
|
70
|
+
# == Rejecting subscription requests
|
71
|
+
#
|
72
|
+
# A channel can reject a subscription request in the #subscribed callback by invoking #reject!
|
73
|
+
#
|
74
|
+
# Example:
|
75
|
+
#
|
76
|
+
# class ChatChannel < ApplicationCable::Channel
|
77
|
+
# def subscribed
|
78
|
+
# @room = Chat::Room[params[:room_number]]
|
79
|
+
# reject unless current_user.can_access?(@room)
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
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.
|
85
|
+
class Base
|
86
|
+
include Callbacks
|
87
|
+
include PeriodicTimers
|
88
|
+
include Streams
|
89
|
+
include Naming
|
90
|
+
include Broadcasting
|
91
|
+
|
92
|
+
attr_reader :params, :connection, :identifier
|
93
|
+
delegate :logger, to: :connection
|
94
|
+
|
95
|
+
class << self
|
96
|
+
# A list of method names that should be considered actions. This
|
97
|
+
# includes all public instance methods on a channel, less
|
98
|
+
# any internal methods (defined on Base), adding back in
|
99
|
+
# any methods that are internal, but still exist on the class
|
100
|
+
# itself.
|
101
|
+
#
|
102
|
+
# ==== Returns
|
103
|
+
# * <tt>Set</tt> - A set of all methods that should be considered actions.
|
104
|
+
def action_methods
|
105
|
+
@action_methods ||= begin
|
106
|
+
# All public instance methods of this class, including ancestors
|
107
|
+
methods = (public_instance_methods(true) -
|
108
|
+
# Except for public instance methods of Base and its ancestors
|
109
|
+
ActionCable::Channel::Base.public_instance_methods(true) +
|
110
|
+
# Be sure to include shadowed public instance methods of this class
|
111
|
+
public_instance_methods(false)).uniq.map(&:to_s)
|
112
|
+
methods.to_set
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
# action_methods are cached and there is sometimes need to refresh
|
118
|
+
# them. ::clear_action_methods! allows you to do that, so next time
|
119
|
+
# you run action_methods, they will be recalculated
|
120
|
+
def clear_action_methods!
|
121
|
+
@action_methods = nil
|
122
|
+
end
|
123
|
+
|
124
|
+
# Refresh the cached action_methods when a new action_method is added.
|
125
|
+
def method_added(name)
|
126
|
+
super
|
127
|
+
clear_action_methods!
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def initialize(connection, identifier, params = {})
|
132
|
+
@connection = connection
|
133
|
+
@identifier = identifier
|
134
|
+
@params = params
|
135
|
+
|
136
|
+
# When a channel is streaming via redis pubsub, we want to delay the confirmation
|
137
|
+
# transmission until redis pubsub subscription is confirmed.
|
138
|
+
@defer_subscription_confirmation = false
|
139
|
+
|
140
|
+
@reject_subscription = nil
|
141
|
+
@subscription_confirmation_sent = nil
|
142
|
+
|
143
|
+
delegate_connection_identifiers
|
144
|
+
subscribe_to_channel
|
145
|
+
end
|
146
|
+
|
147
|
+
# Extract the action name from the passed data and process it via the channel. The process will ensure
|
148
|
+
# that the action requested is a public method on the channel declared by the user (so not one of the callbacks
|
149
|
+
# like #subscribed).
|
150
|
+
def perform_action(data)
|
151
|
+
action = extract_action(data)
|
152
|
+
|
153
|
+
if processable_action?(action)
|
154
|
+
dispatch_action(action, data)
|
155
|
+
else
|
156
|
+
logger.error "Unable to process #{action_signature(action, data)}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Called by the cable connection when its cut so the channel has a chance to cleanup with callbacks.
|
161
|
+
# This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
|
162
|
+
def unsubscribe_from_channel
|
163
|
+
run_callbacks :unsubscribe do
|
164
|
+
unsubscribed
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
protected
|
170
|
+
# Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams
|
171
|
+
# you want this channel to be sending to the subscriber.
|
172
|
+
def subscribed
|
173
|
+
# Override in subclasses
|
174
|
+
end
|
175
|
+
|
176
|
+
# 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.
|
178
|
+
def unsubscribed
|
179
|
+
# Override in subclasses
|
180
|
+
end
|
181
|
+
|
182
|
+
# Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
|
183
|
+
# the proper channel identifier marked as the recipient.
|
184
|
+
def transmit(data, via: nil)
|
185
|
+
logger.info "#{self.class.name} transmitting #{data.inspect}".tap { |m| m << " (via #{via})" if via }
|
186
|
+
connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data)
|
187
|
+
end
|
188
|
+
|
189
|
+
def defer_subscription_confirmation!
|
190
|
+
@defer_subscription_confirmation = true
|
191
|
+
end
|
192
|
+
|
193
|
+
def defer_subscription_confirmation?
|
194
|
+
@defer_subscription_confirmation
|
195
|
+
end
|
196
|
+
|
197
|
+
def subscription_confirmation_sent?
|
198
|
+
@subscription_confirmation_sent
|
199
|
+
end
|
200
|
+
|
201
|
+
def reject
|
202
|
+
@reject_subscription = true
|
203
|
+
end
|
204
|
+
|
205
|
+
def subscription_rejected?
|
206
|
+
@reject_subscription
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
def delegate_connection_identifiers
|
211
|
+
connection.identifiers.each do |identifier|
|
212
|
+
define_singleton_method(identifier) do
|
213
|
+
connection.send(identifier)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
def subscribe_to_channel
|
220
|
+
run_callbacks :subscribe do
|
221
|
+
subscribed
|
222
|
+
end
|
223
|
+
|
224
|
+
if subscription_rejected?
|
225
|
+
reject_subscription
|
226
|
+
else
|
227
|
+
transmit_subscription_confirmation unless defer_subscription_confirmation?
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
def extract_action(data)
|
233
|
+
(data['action'].presence || :receive).to_sym
|
234
|
+
end
|
235
|
+
|
236
|
+
def processable_action?(action)
|
237
|
+
self.class.action_methods.include?(action.to_s)
|
238
|
+
end
|
239
|
+
|
240
|
+
def dispatch_action(action, data)
|
241
|
+
logger.info action_signature(action, data)
|
242
|
+
|
243
|
+
if method(action).arity == 1
|
244
|
+
public_send action, data
|
245
|
+
else
|
246
|
+
public_send action
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def action_signature(action, data)
|
251
|
+
"#{self.class.name}##{action}".tap do |signature|
|
252
|
+
if (arguments = data.except('action')).any?
|
253
|
+
signature << "(#{arguments.inspect})"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def transmit_subscription_confirmation
|
259
|
+
unless subscription_confirmation_sent?
|
260
|
+
logger.info "#{self.class.name} is transmitting the subscription confirmation"
|
261
|
+
connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation])
|
262
|
+
@subscription_confirmation_sent = true
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def reject_subscription
|
267
|
+
connection.subscriptions.remove_subscription self
|
268
|
+
transmit_subscription_rejection
|
269
|
+
end
|
270
|
+
|
271
|
+
def transmit_subscription_rejection
|
272
|
+
logger.info "#{self.class.name} is transmitting the subscription rejection"
|
273
|
+
connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection])
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'active_support/core_ext/object/to_param'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Channel
|
5
|
+
module Broadcasting
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
delegate :broadcasting_for, to: :class
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
# Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
|
12
|
+
def broadcast_to(model, message)
|
13
|
+
ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message)
|
14
|
+
end
|
15
|
+
|
16
|
+
def broadcasting_for(model) #:nodoc:
|
17
|
+
case
|
18
|
+
when model.is_a?(Array)
|
19
|
+
model.map { |m| broadcasting_for(m) }.join(':')
|
20
|
+
when model.respond_to?(:to_gid_param)
|
21
|
+
model.to_gid_param
|
22
|
+
else
|
23
|
+
model.to_param
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_support/callbacks'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Channel
|
5
|
+
module Callbacks
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveSupport::Callbacks
|
8
|
+
|
9
|
+
included do
|
10
|
+
define_callbacks :subscribe
|
11
|
+
define_callbacks :unsubscribe
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def before_subscribe(*methods, &block)
|
16
|
+
set_callback(:subscribe, :before, *methods, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_subscribe(*methods, &block)
|
20
|
+
set_callback(:subscribe, :after, *methods, &block)
|
21
|
+
end
|
22
|
+
alias_method :on_subscribe, :after_subscribe
|
23
|
+
|
24
|
+
def before_unsubscribe(*methods, &block)
|
25
|
+
set_callback(:unsubscribe, :before, *methods, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_unsubscribe(*methods, &block)
|
29
|
+
set_callback(:unsubscribe, :after, *methods, &block)
|
30
|
+
end
|
31
|
+
alias_method :on_unsubscribe, :after_unsubscribe
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Channel
|
3
|
+
module Naming
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
# Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
|
8
|
+
# If the channel is in a namespace, then the namespaces are represented by single
|
9
|
+
# colon separators in the channel name.
|
10
|
+
#
|
11
|
+
# ChatChannel.channel_name # => 'chat'
|
12
|
+
# Chats::AppearancesChannel.channel_name # => 'chats:appearances'
|
13
|
+
def channel_name
|
14
|
+
@channel_name ||= name.sub(/Channel$/, '').gsub('::',':').underscore
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Delegates to the class' <tt>channel_name</tt>
|
19
|
+
delegate :channel_name, to: :class
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Channel
|
3
|
+
module PeriodicTimers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :periodic_timers, instance_reader: false
|
8
|
+
self.periodic_timers = []
|
9
|
+
|
10
|
+
after_subscribe :start_periodic_timers
|
11
|
+
after_unsubscribe :stop_periodic_timers
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
# Allow you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful
|
16
|
+
# for sending a steady flow of updates to a client based off an object that was configured on subscription.
|
17
|
+
# It's an alternative to using streams if the channel is able to do the work internally.
|
18
|
+
def periodically(callback, every:)
|
19
|
+
self.periodic_timers += [ [ callback, every: every ] ]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def active_periodic_timers
|
25
|
+
@active_periodic_timers ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_periodic_timers
|
29
|
+
self.class.periodic_timers.each do |callback, options|
|
30
|
+
active_periodic_timers << EventMachine::PeriodicTimer.new(options[:every]) do
|
31
|
+
connection.worker_pool.async.run_periodic_timer(self, callback)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop_periodic_timers
|
37
|
+
active_periodic_timers.each { |timer| timer.cancel }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|