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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +439 -21
  5. data/lib/action_cable.rb +47 -2
  6. data/lib/action_cable/channel.rb +14 -0
  7. data/lib/action_cable/channel/base.rb +277 -0
  8. data/lib/action_cable/channel/broadcasting.rb +29 -0
  9. data/lib/action_cable/channel/callbacks.rb +35 -0
  10. data/lib/action_cable/channel/naming.rb +22 -0
  11. data/lib/action_cable/channel/periodic_timers.rb +41 -0
  12. data/lib/action_cable/channel/streams.rb +114 -0
  13. data/lib/action_cable/connection.rb +16 -0
  14. data/lib/action_cable/connection/authorization.rb +13 -0
  15. data/lib/action_cable/connection/base.rb +221 -0
  16. data/lib/action_cable/connection/identification.rb +46 -0
  17. data/lib/action_cable/connection/internal_channel.rb +45 -0
  18. data/lib/action_cable/connection/message_buffer.rb +54 -0
  19. data/lib/action_cable/connection/subscriptions.rb +76 -0
  20. data/lib/action_cable/connection/tagged_logger_proxy.rb +40 -0
  21. data/lib/action_cable/connection/web_socket.rb +29 -0
  22. data/lib/action_cable/engine.rb +38 -0
  23. data/lib/action_cable/gem_version.rb +15 -0
  24. data/lib/action_cable/helpers/action_cable_helper.rb +29 -0
  25. data/lib/action_cable/process/logging.rb +10 -0
  26. data/lib/action_cable/remote_connections.rb +64 -0
  27. data/lib/action_cable/server.rb +19 -0
  28. data/lib/action_cable/server/base.rb +77 -0
  29. data/lib/action_cable/server/broadcasting.rb +54 -0
  30. data/lib/action_cable/server/configuration.rb +35 -0
  31. data/lib/action_cable/server/connections.rb +37 -0
  32. data/lib/action_cable/server/worker.rb +42 -0
  33. data/lib/action_cable/server/worker/active_record_connection_management.rb +22 -0
  34. data/lib/action_cable/version.rb +6 -1
  35. data/lib/assets/javascripts/action_cable.coffee.erb +23 -0
  36. data/lib/assets/javascripts/action_cable/connection.coffee +84 -0
  37. data/lib/assets/javascripts/action_cable/connection_monitor.coffee +84 -0
  38. data/lib/assets/javascripts/action_cable/consumer.coffee +31 -0
  39. data/lib/assets/javascripts/action_cable/subscription.coffee +68 -0
  40. data/lib/assets/javascripts/action_cable/subscriptions.coffee +78 -0
  41. data/lib/rails/generators/channel/USAGE +14 -0
  42. data/lib/rails/generators/channel/channel_generator.rb +21 -0
  43. data/lib/rails/generators/channel/templates/assets/channel.coffee +14 -0
  44. data/lib/rails/generators/channel/templates/channel.rb +17 -0
  45. metadata +161 -26
  46. data/.gitignore +0 -9
  47. data/Gemfile +0 -4
  48. data/LICENSE.txt +0 -21
  49. data/Rakefile +0 -2
  50. data/actioncable.gemspec +0 -22
  51. data/bin/console +0 -14
  52. data/bin/setup +0 -7
@@ -1,5 +1,50 @@
1
- require "action_cable/version"
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
- # Your code goes here...
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,14 @@
1
+ module ActionCable
2
+ module Channel
3
+ extend ActiveSupport::Autoload
4
+
5
+ eager_autoload do
6
+ autoload :Base
7
+ autoload :Broadcasting
8
+ autoload :Callbacks
9
+ autoload :Naming
10
+ autoload :PeriodicTimers
11
+ autoload :Streams
12
+ end
13
+ end
14
+ 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