actioncable 0.0.0 → 5.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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