actioncable 6.0.0

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 (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