actioncable 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +169 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +62 -0
  7. data/lib/action_cable/channel.rb +17 -0
  8. data/lib/action_cable/channel/base.rb +311 -0
  9. data/lib/action_cable/channel/broadcasting.rb +41 -0
  10. data/lib/action_cable/channel/callbacks.rb +37 -0
  11. data/lib/action_cable/channel/naming.rb +25 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +176 -0
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +22 -0
  16. data/lib/action_cable/connection/authorization.rb +15 -0
  17. data/lib/action_cable/connection/base.rb +264 -0
  18. data/lib/action_cable/connection/client_socket.rb +157 -0
  19. data/lib/action_cable/connection/identification.rb +47 -0
  20. data/lib/action_cable/connection/internal_channel.rb +45 -0
  21. data/lib/action_cable/connection/message_buffer.rb +54 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +79 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +41 -0
  28. data/lib/action_cable/engine.rb +79 -0
  29. data/lib/action_cable/gem_version.rb +17 -0
  30. data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
  31. data/lib/action_cable/remote_connections.rb +71 -0
  32. data/lib/action_cable/server.rb +17 -0
  33. data/lib/action_cable/server/base.rb +94 -0
  34. data/lib/action_cable/server/broadcasting.rb +54 -0
  35. data/lib/action_cable/server/configuration.rb +56 -0
  36. data/lib/action_cable/server/connections.rb +36 -0
  37. data/lib/action_cable/server/worker.rb +75 -0
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
  39. data/lib/action_cable/subscription_adapter.rb +12 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  41. data/lib/action_cable/subscription_adapter/base.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +37 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
  45. data/lib/action_cable/subscription_adapter/redis.rb +181 -0
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +10 -0
  51. data/lib/rails/generators/channel/USAGE +13 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +52 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +149 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Copyright (c) 2015-2019 Basecamp, LLC
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ require "active_support"
27
+ require "active_support/rails"
28
+ require "action_cable/version"
29
+
30
+ module ActionCable
31
+ extend ActiveSupport::Autoload
32
+
33
+ INTERNAL = {
34
+ message_types: {
35
+ welcome: "welcome",
36
+ disconnect: "disconnect",
37
+ ping: "ping",
38
+ confirmation: "confirm_subscription",
39
+ rejection: "reject_subscription"
40
+ },
41
+ disconnect_reasons: {
42
+ unauthorized: "unauthorized",
43
+ invalid_request: "invalid_request",
44
+ server_restart: "server_restart"
45
+ },
46
+ default_mount_path: "/cable",
47
+ protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze
48
+ }
49
+
50
+ # Singleton instance of the server
51
+ module_function def server
52
+ @server ||= ActionCable::Server::Base.new
53
+ end
54
+
55
+ autoload :Server
56
+ autoload :Connection
57
+ autoload :Channel
58
+ autoload :RemoteConnections
59
+ autoload :SubscriptionAdapter
60
+ autoload :TestHelper
61
+ autoload :TestCase
62
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Channel
5
+ extend ActiveSupport::Autoload
6
+
7
+ eager_autoload do
8
+ autoload :Base
9
+ autoload :Broadcasting
10
+ autoload :Callbacks
11
+ autoload :Naming
12
+ autoload :PeriodicTimers
13
+ autoload :Streams
14
+ autoload :TestCase
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "active_support/rescuable"
5
+
6
+ module ActionCable
7
+ module Channel
8
+ # The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection.
9
+ # 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
10
+ # responding to the subscriber's direct requests.
11
+ #
12
+ # Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then
13
+ # lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care
14
+ # 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
15
+ # as is normally the case with a controller instance that gets thrown away after every request.
16
+ #
17
+ # 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
18
+ # 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.
19
+ #
20
+ # The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests
21
+ # can interact with. Here's a quick example:
22
+ #
23
+ # class ChatChannel < ApplicationCable::Channel
24
+ # def subscribed
25
+ # @room = Chat::Room[params[:room_number]]
26
+ # end
27
+ #
28
+ # def speak(data)
29
+ # @room.speak data, user: current_user
30
+ # end
31
+ # end
32
+ #
33
+ # The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that
34
+ # subscriber wants to say something in the room.
35
+ #
36
+ # == Action processing
37
+ #
38
+ # Unlike subclasses of ActionController::Base, channels do not follow a RESTful
39
+ # constraint form for their actions. Instead, Action Cable operates through a
40
+ # remote-procedure call model. You can declare any public method on the
41
+ # channel (optionally taking a <tt>data</tt> argument), and this method is
42
+ # automatically exposed as callable to the client.
43
+ #
44
+ # Example:
45
+ #
46
+ # class AppearanceChannel < ApplicationCable::Channel
47
+ # def subscribed
48
+ # @connection_token = generate_connection_token
49
+ # end
50
+ #
51
+ # def unsubscribed
52
+ # current_user.disappear @connection_token
53
+ # end
54
+ #
55
+ # def appear(data)
56
+ # current_user.appear @connection_token, on: data['appearing_on']
57
+ # end
58
+ #
59
+ # def away
60
+ # current_user.away @connection_token
61
+ # end
62
+ #
63
+ # private
64
+ # def generate_connection_token
65
+ # SecureRandom.hex(36)
66
+ # end
67
+ # end
68
+ #
69
+ # In this example, the subscribed and unsubscribed methods are not callable methods, as they
70
+ # were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
71
+ # and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not
72
+ # callable, since it's a private method. You'll see that appear accepts a data
73
+ # parameter, which it then uses as part of its model call. <tt>#away</tt>
74
+ # does not, since it's simply a trigger action.
75
+ #
76
+ # Also note that in this example, <tt>current_user</tt> is available because
77
+ # it was marked as an identifying attribute on the connection. All such
78
+ # identifiers will automatically create a delegation method of the same name
79
+ # on the channel instance.
80
+ #
81
+ # == Rejecting subscription requests
82
+ #
83
+ # A channel can reject a subscription request in the #subscribed callback by
84
+ # invoking the #reject method:
85
+ #
86
+ # class ChatChannel < ApplicationCable::Channel
87
+ # def subscribed
88
+ # @room = Chat::Room[params[:room_number]]
89
+ # reject unless current_user.can_access?(@room)
90
+ # end
91
+ # end
92
+ #
93
+ # In this example, the subscription will be rejected if the
94
+ # <tt>current_user</tt> does not have access to the chat room. On the
95
+ # client-side, the <tt>Channel#rejected</tt> callback will get invoked when
96
+ # the server rejects the subscription request.
97
+ class Base
98
+ include Callbacks
99
+ include PeriodicTimers
100
+ include Streams
101
+ include Naming
102
+ include Broadcasting
103
+ include ActiveSupport::Rescuable
104
+
105
+ attr_reader :params, :connection, :identifier
106
+ delegate :logger, to: :connection
107
+
108
+ class << self
109
+ # A list of method names that should be considered actions. This
110
+ # includes all public instance methods on a channel, less
111
+ # any internal methods (defined on Base), adding back in
112
+ # any methods that are internal, but still exist on the class
113
+ # itself.
114
+ #
115
+ # ==== Returns
116
+ # * <tt>Set</tt> - A set of all methods that should be considered actions.
117
+ def action_methods
118
+ @action_methods ||= begin
119
+ # All public instance methods of this class, including ancestors
120
+ methods = (public_instance_methods(true) -
121
+ # Except for public instance methods of Base and its ancestors
122
+ ActionCable::Channel::Base.public_instance_methods(true) +
123
+ # Be sure to include shadowed public instance methods of this class
124
+ public_instance_methods(false)).uniq.map(&:to_s)
125
+ methods.to_set
126
+ end
127
+ end
128
+
129
+ private
130
+ # action_methods are cached and there is sometimes need to refresh
131
+ # them. ::clear_action_methods! allows you to do that, so next time
132
+ # you run action_methods, they will be recalculated.
133
+ def clear_action_methods! # :doc:
134
+ @action_methods = nil
135
+ end
136
+
137
+ # Refresh the cached action_methods when a new action_method is added.
138
+ def method_added(name) # :doc:
139
+ super
140
+ clear_action_methods!
141
+ end
142
+ end
143
+
144
+ def initialize(connection, identifier, params = {})
145
+ @connection = connection
146
+ @identifier = identifier
147
+ @params = params
148
+
149
+ # When a channel is streaming via pubsub, we want to delay the confirmation
150
+ # transmission until pubsub subscription is confirmed.
151
+ #
152
+ # The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
153
+ @defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)
154
+
155
+ @reject_subscription = nil
156
+ @subscription_confirmation_sent = nil
157
+
158
+ delegate_connection_identifiers
159
+ end
160
+
161
+ # Extract the action name from the passed data and process it via the channel. The process will ensure
162
+ # that the action requested is a public method on the channel declared by the user (so not one of the callbacks
163
+ # like #subscribed).
164
+ def perform_action(data)
165
+ action = extract_action(data)
166
+
167
+ if processable_action?(action)
168
+ payload = { channel_class: self.class.name, action: action, data: data }
169
+ ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
170
+ dispatch_action(action, data)
171
+ end
172
+ else
173
+ logger.error "Unable to process #{action_signature(action, data)}"
174
+ end
175
+ end
176
+
177
+ # This method is called after subscription has been added to the connection
178
+ # and confirms or rejects the subscription.
179
+ def subscribe_to_channel
180
+ run_callbacks :subscribe do
181
+ subscribed
182
+ end
183
+
184
+ reject_subscription if subscription_rejected?
185
+ ensure_confirmation_sent
186
+ end
187
+
188
+ # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
189
+ # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
190
+ def unsubscribe_from_channel # :nodoc:
191
+ run_callbacks :unsubscribe do
192
+ unsubscribed
193
+ end
194
+ end
195
+
196
+ private
197
+ # Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams
198
+ # you want this channel to be sending to the subscriber.
199
+ def subscribed # :doc:
200
+ # Override in subclasses
201
+ end
202
+
203
+ # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
204
+ # users as offline or the like.
205
+ def unsubscribed # :doc:
206
+ # Override in subclasses
207
+ end
208
+
209
+ # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
210
+ # the proper channel identifier marked as the recipient.
211
+ def transmit(data, via: nil) # :doc:
212
+ status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
213
+ status += " (via #{via})" if via
214
+ logger.debug(status)
215
+
216
+ payload = { channel_class: self.class.name, data: data, via: via }
217
+ ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
218
+ connection.transmit identifier: @identifier, message: data
219
+ end
220
+ end
221
+
222
+ def ensure_confirmation_sent # :doc:
223
+ return if subscription_rejected?
224
+ @defer_subscription_confirmation_counter.decrement
225
+ transmit_subscription_confirmation unless defer_subscription_confirmation?
226
+ end
227
+
228
+ def defer_subscription_confirmation! # :doc:
229
+ @defer_subscription_confirmation_counter.increment
230
+ end
231
+
232
+ def defer_subscription_confirmation? # :doc:
233
+ @defer_subscription_confirmation_counter.value > 0
234
+ end
235
+
236
+ def subscription_confirmation_sent? # :doc:
237
+ @subscription_confirmation_sent
238
+ end
239
+
240
+ def reject # :doc:
241
+ @reject_subscription = true
242
+ end
243
+
244
+ def subscription_rejected? # :doc:
245
+ @reject_subscription
246
+ end
247
+
248
+ def delegate_connection_identifiers
249
+ connection.identifiers.each do |identifier|
250
+ define_singleton_method(identifier) do
251
+ connection.send(identifier)
252
+ end
253
+ end
254
+ end
255
+
256
+ def extract_action(data)
257
+ (data["action"].presence || :receive).to_sym
258
+ end
259
+
260
+ def processable_action?(action)
261
+ self.class.action_methods.include?(action.to_s) unless subscription_rejected?
262
+ end
263
+
264
+ def dispatch_action(action, data)
265
+ logger.info action_signature(action, data)
266
+
267
+ if method(action).arity == 1
268
+ public_send action, data
269
+ else
270
+ public_send action
271
+ end
272
+ rescue Exception => exception
273
+ rescue_with_handler(exception) || raise
274
+ end
275
+
276
+ def action_signature(action, data)
277
+ (+"#{self.class.name}##{action}").tap do |signature|
278
+ if (arguments = data.except("action")).any?
279
+ signature << "(#{arguments.inspect})"
280
+ end
281
+ end
282
+ end
283
+
284
+ def transmit_subscription_confirmation
285
+ unless subscription_confirmation_sent?
286
+ logger.info "#{self.class.name} is transmitting the subscription confirmation"
287
+
288
+ ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
289
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
290
+ @subscription_confirmation_sent = true
291
+ end
292
+ end
293
+ end
294
+
295
+ def reject_subscription
296
+ connection.subscriptions.remove_subscription self
297
+ transmit_subscription_rejection
298
+ end
299
+
300
+ def transmit_subscription_rejection
301
+ logger.info "#{self.class.name} is transmitting the subscription rejection"
302
+
303
+ ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
304
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
310
+
311
+ ActiveSupport.run_load_hooks(:action_cable_channel, ActionCable::Channel::Base)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/to_param"
4
+
5
+ module ActionCable
6
+ module Channel
7
+ module Broadcasting
8
+ extend ActiveSupport::Concern
9
+
10
+ delegate :broadcasting_for, :broadcast_to, to: :class
11
+
12
+ module ClassMethods
13
+ # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
14
+ def broadcast_to(model, message)
15
+ ActionCable.server.broadcast(broadcasting_for(model), message)
16
+ end
17
+
18
+ # Returns a unique broadcasting identifier for this <tt>model</tt> in this channel:
19
+ #
20
+ # CommentsChannel.broadcasting_for("all") # => "comments:all"
21
+ #
22
+ # You can pass any object as a target (e.g. Active Record model), and it
23
+ # would be serialized into a string under the hood.
24
+ def broadcasting_for(model)
25
+ serialize_broadcasting([ channel_name, model ])
26
+ end
27
+
28
+ def serialize_broadcasting(object) #:nodoc:
29
+ case
30
+ when object.is_a?(Array)
31
+ object.map { |m| serialize_broadcasting(m) }.join(":")
32
+ when object.respond_to?(:to_gid_param)
33
+ object.to_gid_param
34
+ else
35
+ object.to_param
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+
5
+ module ActionCable
6
+ module Channel
7
+ module Callbacks
8
+ extend ActiveSupport::Concern
9
+ include ActiveSupport::Callbacks
10
+
11
+ included do
12
+ define_callbacks :subscribe
13
+ define_callbacks :unsubscribe
14
+ end
15
+
16
+ module ClassMethods
17
+ def before_subscribe(*methods, &block)
18
+ set_callback(:subscribe, :before, *methods, &block)
19
+ end
20
+
21
+ def after_subscribe(*methods, &block)
22
+ set_callback(:subscribe, :after, *methods, &block)
23
+ end
24
+ alias_method :on_subscribe, :after_subscribe
25
+
26
+ def before_unsubscribe(*methods, &block)
27
+ set_callback(:unsubscribe, :before, *methods, &block)
28
+ end
29
+
30
+ def after_unsubscribe(*methods, &block)
31
+ set_callback(:unsubscribe, :after, *methods, &block)
32
+ end
33
+ alias_method :on_unsubscribe, :after_unsubscribe
34
+ end
35
+ end
36
+ end
37
+ end