actioncable 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +169 -0
- data/MIT-LICENSE +20 -0
- data/README.md +24 -0
- data/app/assets/javascripts/action_cable.js +517 -0
- data/lib/action_cable.rb +62 -0
- data/lib/action_cable/channel.rb +17 -0
- data/lib/action_cable/channel/base.rb +311 -0
- data/lib/action_cable/channel/broadcasting.rb +41 -0
- data/lib/action_cable/channel/callbacks.rb +37 -0
- data/lib/action_cable/channel/naming.rb +25 -0
- data/lib/action_cable/channel/periodic_timers.rb +78 -0
- data/lib/action_cable/channel/streams.rb +176 -0
- data/lib/action_cable/channel/test_case.rb +310 -0
- data/lib/action_cable/connection.rb +22 -0
- data/lib/action_cable/connection/authorization.rb +15 -0
- data/lib/action_cable/connection/base.rb +264 -0
- data/lib/action_cable/connection/client_socket.rb +157 -0
- data/lib/action_cable/connection/identification.rb +47 -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/stream.rb +117 -0
- data/lib/action_cable/connection/stream_event_loop.rb +136 -0
- data/lib/action_cable/connection/subscriptions.rb +79 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
- data/lib/action_cable/connection/test_case.rb +234 -0
- data/lib/action_cable/connection/web_socket.rb +41 -0
- data/lib/action_cable/engine.rb +79 -0
- data/lib/action_cable/gem_version.rb +17 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
- data/lib/action_cable/remote_connections.rb +71 -0
- data/lib/action_cable/server.rb +17 -0
- data/lib/action_cable/server/base.rb +94 -0
- data/lib/action_cable/server/broadcasting.rb +54 -0
- data/lib/action_cable/server/configuration.rb +56 -0
- data/lib/action_cable/server/connections.rb +36 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
- data/lib/action_cable/subscription_adapter.rb +12 -0
- data/lib/action_cable/subscription_adapter/async.rb +29 -0
- data/lib/action_cable/subscription_adapter/base.rb +30 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
- data/lib/action_cable/subscription_adapter/inline.rb +37 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
- data/lib/action_cable/subscription_adapter/redis.rb +181 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
- data/lib/action_cable/subscription_adapter/test.rb +40 -0
- data/lib/action_cable/test_case.rb +11 -0
- data/lib/action_cable/test_helper.rb +133 -0
- data/lib/action_cable/version.rb +10 -0
- data/lib/rails/generators/channel/USAGE +13 -0
- data/lib/rails/generators/channel/channel_generator.rb +52 -0
- data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
- data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
- data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +149 -0
data/lib/action_cable.rb
ADDED
@@ -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
|