actioncable-next 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +17 -0
- data/lib/action_cable/channel/base.rb +335 -0
- data/lib/action_cable/channel/broadcasting.rb +50 -0
- data/lib/action_cable/channel/callbacks.rb +76 -0
- data/lib/action_cable/channel/naming.rb +28 -0
- data/lib/action_cable/channel/periodic_timers.rb +81 -0
- data/lib/action_cable/channel/streams.rb +213 -0
- data/lib/action_cable/channel/test_case.rb +329 -0
- data/lib/action_cable/connection/authorization.rb +18 -0
- data/lib/action_cable/connection/base.rb +165 -0
- data/lib/action_cable/connection/callbacks.rb +57 -0
- data/lib/action_cable/connection/identification.rb +51 -0
- data/lib/action_cable/connection/internal_channel.rb +50 -0
- data/lib/action_cable/connection/subscriptions.rb +124 -0
- data/lib/action_cable/connection/test_case.rb +294 -0
- data/lib/action_cable/deprecator.rb +9 -0
- data/lib/action_cable/engine.rb +98 -0
- data/lib/action_cable/gem_version.rb +19 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
- data/lib/action_cable/remote_connections.rb +82 -0
- data/lib/action_cable/server/base.rb +163 -0
- data/lib/action_cable/server/broadcasting.rb +62 -0
- data/lib/action_cable/server/configuration.rb +75 -0
- data/lib/action_cable/server/connections.rb +44 -0
- data/lib/action_cable/server/socket/client_socket.rb +159 -0
- data/lib/action_cable/server/socket/message_buffer.rb +56 -0
- data/lib/action_cable/server/socket/stream.rb +117 -0
- data/lib/action_cable/server/socket/web_socket.rb +47 -0
- data/lib/action_cable/server/socket.rb +180 -0
- data/lib/action_cable/server/stream_event_loop.rb +119 -0
- data/lib/action_cable/server/tagged_logger_proxy.rb +46 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/subscription_adapter/async.rb +14 -0
- data/lib/action_cable/subscription_adapter/base.rb +39 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
- data/lib/action_cable/subscription_adapter/inline.rb +40 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +130 -0
- data/lib/action_cable/subscription_adapter/redis.rb +257 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +80 -0
- data/lib/action_cable/subscription_adapter/test.rb +41 -0
- data/lib/action_cable/test_case.rb +13 -0
- data/lib/action_cable/test_helper.rb +163 -0
- data/lib/action_cable/version.rb +12 -0
- data/lib/action_cable.rb +81 -0
- data/lib/actioncable-next.rb +5 -0
- data/lib/rails/generators/channel/USAGE +19 -0
- data/lib/rails/generators/channel/channel_generator.rb +127 -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 +1 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a2a646309358247e05b84be4246c044bfae6f289eab7776e5821d6c54e8b1f00
|
4
|
+
data.tar.gz: d2ec2b1f743af72cdc61a8be7c41e80778cff79d9335baa059fbd791272cf07a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8ff34021af6ec2c0e69676ba92fb64f2f1c9a9141db355eaa1f16fc7e9e7f3e4689b79efd86ebada9fd698e9b76bf95a214969198feb0c4334bbebb86a0597ec
|
7
|
+
data.tar.gz: aed1822bc56226c76a02f6d9a95f3bc915fe429d3345bd3dbbc964ef6e384337347ed0496427b609788846951409042accb001ca157519f746997d512665b0d3
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2024 Vladimir Dementyev
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Action Cable Next
|
2
|
+
|
3
|
+
This gem provides the functionality of the _server adapterization_ PR: [rails/rails#50979](https://github.com/rails/rails/pull/50979).
|
4
|
+
|
5
|
+
See the PR description for more information on the purpose of this refactoring.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile **before Rails or Action Cable**:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem "actioncable-next"
|
13
|
+
|
14
|
+
gem "rails", "~> 7.0"
|
15
|
+
```
|
16
|
+
|
17
|
+
Then, you can use Action Cable as before. Under the hood, the new implementation would be used.
|
@@ -0,0 +1,335 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "set"
|
6
|
+
require "active_support/rescuable"
|
7
|
+
require "active_support/parameter_filter"
|
8
|
+
|
9
|
+
module ActionCable
|
10
|
+
module Channel
|
11
|
+
# # Action Cable Channel Base
|
12
|
+
#
|
13
|
+
# The channel provides the basic structure of grouping behavior into logical
|
14
|
+
# units when communicating over the WebSocket connection. You can think of a
|
15
|
+
# channel like a form of controller, but one that's capable of pushing content
|
16
|
+
# to the subscriber in addition to simply responding to the subscriber's direct
|
17
|
+
# requests.
|
18
|
+
#
|
19
|
+
# Channel instances are long-lived. A channel object will be instantiated when
|
20
|
+
# the cable consumer becomes a subscriber, and then lives until the consumer
|
21
|
+
# disconnects. This may be seconds, minutes, hours, or even days. That means you
|
22
|
+
# have to take special care not to do anything silly in a channel that would
|
23
|
+
# balloon its memory footprint or whatever. The references are forever, so they
|
24
|
+
# won't be released as is normally the case with a controller instance that gets
|
25
|
+
# thrown away after every request.
|
26
|
+
#
|
27
|
+
# Long-lived channels (and connections) also mean you're responsible for
|
28
|
+
# ensuring that the data is fresh. If you hold a reference to a user record, but
|
29
|
+
# the name is changed while that reference is held, you may be sending stale
|
30
|
+
# data if you don't take precautions to avoid it.
|
31
|
+
#
|
32
|
+
# The upside of long-lived channel instances is that you can use instance
|
33
|
+
# variables to keep reference to objects that future subscriber requests can
|
34
|
+
# interact with. Here's a quick example:
|
35
|
+
#
|
36
|
+
# class ChatChannel < ApplicationCable::Channel
|
37
|
+
# def subscribed
|
38
|
+
# @room = Chat::Room[params[:room_number]]
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def speak(data)
|
42
|
+
# @room.speak data, user: current_user
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# The #speak action simply uses the Chat::Room object that was created when the
|
47
|
+
# channel was first subscribed to by the consumer when that subscriber wants to
|
48
|
+
# say something in the room.
|
49
|
+
#
|
50
|
+
# ## Action processing
|
51
|
+
#
|
52
|
+
# Unlike subclasses of ActionController::Base, channels do not follow a RESTful
|
53
|
+
# constraint form for their actions. Instead, Action Cable operates through a
|
54
|
+
# remote-procedure call model. You can declare any public method on the channel
|
55
|
+
# (optionally taking a `data` argument), and this method is automatically
|
56
|
+
# exposed as callable to the client.
|
57
|
+
#
|
58
|
+
# Example:
|
59
|
+
#
|
60
|
+
# class AppearanceChannel < ApplicationCable::Channel
|
61
|
+
# def subscribed
|
62
|
+
# @connection_token = generate_connection_token
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# def unsubscribed
|
66
|
+
# current_user.disappear @connection_token
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# def appear(data)
|
70
|
+
# current_user.appear @connection_token, on: data['appearing_on']
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# def away
|
74
|
+
# current_user.away @connection_token
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# private
|
78
|
+
# def generate_connection_token
|
79
|
+
# SecureRandom.hex(36)
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# In this example, the subscribed and unsubscribed methods are not callable
|
84
|
+
# methods, as they were already declared in ActionCable::Channel::Base, but
|
85
|
+
# `#appear` and `#away` are. `#generate_connection_token` is also not callable,
|
86
|
+
# since it's a private method. You'll see that appear accepts a data parameter,
|
87
|
+
# which it then uses as part of its model call. `#away` does not, since it's
|
88
|
+
# simply a trigger action.
|
89
|
+
#
|
90
|
+
# Also note that in this example, `current_user` is available because it was
|
91
|
+
# marked as an identifying attribute on the connection. All such identifiers
|
92
|
+
# will automatically create a delegation method of the same name on the channel
|
93
|
+
# instance.
|
94
|
+
#
|
95
|
+
# ## Rejecting subscription requests
|
96
|
+
#
|
97
|
+
# A channel can reject a subscription request in the #subscribed callback by
|
98
|
+
# invoking the #reject method:
|
99
|
+
#
|
100
|
+
# class ChatChannel < ApplicationCable::Channel
|
101
|
+
# def subscribed
|
102
|
+
# @room = Chat::Room[params[:room_number]]
|
103
|
+
# reject unless current_user.can_access?(@room)
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# In this example, the subscription will be rejected if the `current_user` does
|
108
|
+
# not have access to the chat room. On the client-side, the `Channel#rejected`
|
109
|
+
# callback will get invoked when the server rejects the subscription request.
|
110
|
+
class Base
|
111
|
+
include Callbacks
|
112
|
+
include PeriodicTimers
|
113
|
+
include Streams
|
114
|
+
include Naming
|
115
|
+
include Broadcasting
|
116
|
+
include ActiveSupport::Rescuable
|
117
|
+
|
118
|
+
attr_reader :params, :connection, :identifier
|
119
|
+
delegate :logger, to: :connection
|
120
|
+
|
121
|
+
class << self
|
122
|
+
# A list of method names that should be considered actions. This includes all
|
123
|
+
# public instance methods on a channel, less any internal methods (defined on
|
124
|
+
# Base), adding back in any methods that are internal, but still exist on the
|
125
|
+
# class itself.
|
126
|
+
#
|
127
|
+
# #### Returns
|
128
|
+
# * `Set` - A set of all methods that should be considered actions.
|
129
|
+
def action_methods
|
130
|
+
@action_methods ||= begin
|
131
|
+
# All public instance methods of this class, including ancestors
|
132
|
+
methods = (public_instance_methods(true) -
|
133
|
+
# Except for public instance methods of Base and its ancestors
|
134
|
+
ActionCable::Channel::Base.public_instance_methods(true) +
|
135
|
+
# Be sure to include shadowed public instance methods of this class
|
136
|
+
public_instance_methods(false)).uniq.map(&:to_s)
|
137
|
+
methods.to_set
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
# action_methods are cached and there is sometimes need to refresh them.
|
143
|
+
# ::clear_action_methods! allows you to do that, so next time you run
|
144
|
+
# action_methods, they will be recalculated.
|
145
|
+
def clear_action_methods! # :doc:
|
146
|
+
@action_methods = nil
|
147
|
+
end
|
148
|
+
|
149
|
+
# Refresh the cached action_methods when a new action_method is added.
|
150
|
+
def method_added(name) # :doc:
|
151
|
+
super
|
152
|
+
clear_action_methods!
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def initialize(connection, identifier, params = {})
|
157
|
+
@connection = connection
|
158
|
+
@identifier = identifier
|
159
|
+
@params = params
|
160
|
+
|
161
|
+
# When a channel is streaming via pubsub, we want to delay the confirmation
|
162
|
+
# transmission until pubsub subscription is confirmed.
|
163
|
+
#
|
164
|
+
# The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
|
165
|
+
@defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)
|
166
|
+
|
167
|
+
@reject_subscription = nil
|
168
|
+
@subscription_confirmation_sent = nil
|
169
|
+
|
170
|
+
delegate_connection_identifiers
|
171
|
+
end
|
172
|
+
|
173
|
+
# Extract the action name from the passed data and process it via the channel.
|
174
|
+
# The process will ensure that the action requested is a public method on the
|
175
|
+
# channel declared by the user (so not one of the callbacks like #subscribed).
|
176
|
+
def perform_action(data)
|
177
|
+
action = extract_action(data)
|
178
|
+
|
179
|
+
if processable_action?(action)
|
180
|
+
payload = { channel_class: self.class.name, action: action, data: data }
|
181
|
+
ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
|
182
|
+
dispatch_action(action, data)
|
183
|
+
end
|
184
|
+
else
|
185
|
+
logger.error "Unable to process #{action_signature(action, data)}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# This method is called after subscription has been added to the connection and
|
190
|
+
# confirms or rejects the subscription.
|
191
|
+
def subscribe_to_channel
|
192
|
+
run_callbacks :subscribe do
|
193
|
+
subscribed
|
194
|
+
end
|
195
|
+
|
196
|
+
reject_subscription if subscription_rejected?
|
197
|
+
ensure_confirmation_sent
|
198
|
+
end
|
199
|
+
|
200
|
+
# Called by the cable connection when it's cut, so the channel has a chance to
|
201
|
+
# cleanup with callbacks. This method is not intended to be called directly by
|
202
|
+
# the user. Instead, override the #unsubscribed callback.
|
203
|
+
def unsubscribe_from_channel # :nodoc:
|
204
|
+
run_callbacks :unsubscribe do
|
205
|
+
unsubscribed
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
# Called once a consumer has become a subscriber of the channel. Usually the
|
211
|
+
# place to set up any streams you want this channel to be sending to the
|
212
|
+
# subscriber.
|
213
|
+
def subscribed # :doc:
|
214
|
+
# Override in subclasses
|
215
|
+
end
|
216
|
+
|
217
|
+
# Called once a consumer has cut its cable connection. Can be used for cleaning
|
218
|
+
# up connections or marking users as offline or the like.
|
219
|
+
def unsubscribed # :doc:
|
220
|
+
# Override in subclasses
|
221
|
+
end
|
222
|
+
|
223
|
+
# Transmit a hash of data to the subscriber. The hash will automatically be
|
224
|
+
# wrapped in a JSON envelope with the proper channel identifier marked as the
|
225
|
+
# recipient.
|
226
|
+
def transmit(data, via: nil) # :doc:
|
227
|
+
logger.debug do
|
228
|
+
status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
|
229
|
+
status += " (via #{via})" if via
|
230
|
+
status
|
231
|
+
end
|
232
|
+
|
233
|
+
payload = { channel_class: self.class.name, data: data, via: via }
|
234
|
+
ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
|
235
|
+
connection.transmit identifier: @identifier, message: data
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def ensure_confirmation_sent # :doc:
|
240
|
+
return if subscription_rejected?
|
241
|
+
@defer_subscription_confirmation_counter.decrement
|
242
|
+
transmit_subscription_confirmation unless defer_subscription_confirmation?
|
243
|
+
end
|
244
|
+
|
245
|
+
def defer_subscription_confirmation! # :doc:
|
246
|
+
@defer_subscription_confirmation_counter.increment
|
247
|
+
end
|
248
|
+
|
249
|
+
def defer_subscription_confirmation? # :doc:
|
250
|
+
@defer_subscription_confirmation_counter.value > 0
|
251
|
+
end
|
252
|
+
|
253
|
+
def subscription_confirmation_sent? # :doc:
|
254
|
+
@subscription_confirmation_sent
|
255
|
+
end
|
256
|
+
|
257
|
+
def reject # :doc:
|
258
|
+
@reject_subscription = true
|
259
|
+
end
|
260
|
+
|
261
|
+
def subscription_rejected? # :doc:
|
262
|
+
@reject_subscription
|
263
|
+
end
|
264
|
+
|
265
|
+
def delegate_connection_identifiers
|
266
|
+
connection.identifiers.each do |identifier|
|
267
|
+
define_singleton_method(identifier) do
|
268
|
+
connection.send(identifier)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def extract_action(data)
|
274
|
+
(data["action"].presence || :receive).to_sym
|
275
|
+
end
|
276
|
+
|
277
|
+
def processable_action?(action)
|
278
|
+
self.class.action_methods.include?(action.to_s) unless subscription_rejected?
|
279
|
+
end
|
280
|
+
|
281
|
+
def dispatch_action(action, data)
|
282
|
+
logger.debug action_signature(action, data)
|
283
|
+
|
284
|
+
if method(action).arity == 1
|
285
|
+
public_send action, data
|
286
|
+
else
|
287
|
+
public_send action
|
288
|
+
end
|
289
|
+
rescue Exception => exception
|
290
|
+
rescue_with_handler(exception) || raise
|
291
|
+
end
|
292
|
+
|
293
|
+
def action_signature(action, data)
|
294
|
+
(+"#{self.class.name}##{action}").tap do |signature|
|
295
|
+
arguments = data.except("action")
|
296
|
+
|
297
|
+
if arguments.any?
|
298
|
+
arguments = parameter_filter.filter(arguments)
|
299
|
+
signature << "(#{arguments.inspect})"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def parameter_filter
|
305
|
+
@parameter_filter ||= ActiveSupport::ParameterFilter.new(connection.config.filter_parameters)
|
306
|
+
end
|
307
|
+
|
308
|
+
def transmit_subscription_confirmation
|
309
|
+
unless subscription_confirmation_sent?
|
310
|
+
logger.debug "#{self.class.name} is transmitting the subscription confirmation"
|
311
|
+
|
312
|
+
ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name, identifier: @identifier) do
|
313
|
+
connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
|
314
|
+
@subscription_confirmation_sent = true
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def reject_subscription
|
320
|
+
connection.subscriptions.remove_subscription self
|
321
|
+
transmit_subscription_rejection
|
322
|
+
end
|
323
|
+
|
324
|
+
def transmit_subscription_rejection
|
325
|
+
logger.debug "#{self.class.name} is transmitting the subscription rejection"
|
326
|
+
|
327
|
+
ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name, identifier: @identifier) do
|
328
|
+
connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
ActiveSupport.run_load_hooks(:action_cable_channel, ActionCable::Channel::Base)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/core_ext/object/to_param"
|
6
|
+
|
7
|
+
module ActionCable
|
8
|
+
module Channel
|
9
|
+
module Broadcasting
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Broadcast a hash to a unique broadcasting for this `model` 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 `model` 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 would
|
23
|
+
# be serialized into a string under the hood.
|
24
|
+
def broadcasting_for(model)
|
25
|
+
serialize_broadcasting([ channel_name, model ])
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def serialize_broadcasting(object) # :nodoc:
|
30
|
+
case
|
31
|
+
when object.is_a?(Array)
|
32
|
+
object.map { |m| serialize_broadcasting(m) }.join(":")
|
33
|
+
when object.respond_to?(:to_gid_param)
|
34
|
+
object.to_gid_param
|
35
|
+
else
|
36
|
+
object.to_param
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def broadcasting_for(model)
|
42
|
+
self.class.broadcasting_for(model)
|
43
|
+
end
|
44
|
+
|
45
|
+
def broadcast_to(model, message)
|
46
|
+
self.class.broadcast_to(model, message)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/callbacks"
|
6
|
+
|
7
|
+
module ActionCable
|
8
|
+
module Channel
|
9
|
+
# # Action Cable Channel Callbacks
|
10
|
+
#
|
11
|
+
# Action Cable Channel provides callback hooks that are invoked during the life
|
12
|
+
# cycle of a channel:
|
13
|
+
#
|
14
|
+
# * [before_subscribe](rdoc-ref:ClassMethods#before_subscribe)
|
15
|
+
# * [after_subscribe](rdoc-ref:ClassMethods#after_subscribe) (aliased as
|
16
|
+
# [on_subscribe](rdoc-ref:ClassMethods#on_subscribe))
|
17
|
+
# * [before_unsubscribe](rdoc-ref:ClassMethods#before_unsubscribe)
|
18
|
+
# * [after_unsubscribe](rdoc-ref:ClassMethods#after_unsubscribe) (aliased as
|
19
|
+
# [on_unsubscribe](rdoc-ref:ClassMethods#on_unsubscribe))
|
20
|
+
#
|
21
|
+
#
|
22
|
+
# #### Example
|
23
|
+
#
|
24
|
+
# class ChatChannel < ApplicationCable::Channel
|
25
|
+
# after_subscribe :send_welcome_message, unless: :subscription_rejected?
|
26
|
+
# after_subscribe :track_subscription
|
27
|
+
#
|
28
|
+
# private
|
29
|
+
# def send_welcome_message
|
30
|
+
# broadcast_to(...)
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def track_subscription
|
34
|
+
# # ...
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
module Callbacks
|
39
|
+
extend ActiveSupport::Concern
|
40
|
+
include ActiveSupport::Callbacks
|
41
|
+
|
42
|
+
included do
|
43
|
+
define_callbacks :subscribe
|
44
|
+
define_callbacks :unsubscribe
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def before_subscribe(*methods, &block)
|
49
|
+
set_callback(:subscribe, :before, *methods, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# This callback will be triggered after the Base#subscribed method is called,
|
53
|
+
# even if the subscription was rejected with the Base#reject method.
|
54
|
+
#
|
55
|
+
# To trigger the callback only on successful subscriptions, use the
|
56
|
+
# Base#subscription_rejected? method:
|
57
|
+
#
|
58
|
+
# after_subscribe :my_method, unless: :subscription_rejected?
|
59
|
+
#
|
60
|
+
def after_subscribe(*methods, &block)
|
61
|
+
set_callback(:subscribe, :after, *methods, &block)
|
62
|
+
end
|
63
|
+
alias_method :on_subscribe, :after_subscribe
|
64
|
+
|
65
|
+
def before_unsubscribe(*methods, &block)
|
66
|
+
set_callback(:unsubscribe, :before, *methods, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
def after_unsubscribe(*methods, &block)
|
70
|
+
set_callback(:unsubscribe, :after, *methods, &block)
|
71
|
+
end
|
72
|
+
alias_method :on_unsubscribe, :after_unsubscribe
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Channel
|
7
|
+
module Naming
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Returns the name of the channel, underscored, without the `Channel` ending. If
|
12
|
+
# the channel is in a namespace, then the namespaces are represented by single
|
13
|
+
# colon separators in the channel name.
|
14
|
+
#
|
15
|
+
# ChatChannel.channel_name # => 'chat'
|
16
|
+
# Chats::AppearancesChannel.channel_name # => 'chats:appearances'
|
17
|
+
# FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
|
18
|
+
def channel_name
|
19
|
+
@channel_name ||= name.delete_suffix("Channel").gsub("::", ":").underscore
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def channel_name
|
24
|
+
self.class.channel_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Channel
|
7
|
+
module PeriodicTimers
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :periodic_timers, instance_reader: false, default: []
|
12
|
+
|
13
|
+
after_subscribe :start_periodic_timers
|
14
|
+
after_unsubscribe :stop_periodic_timers
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
# Periodically performs a task on the channel, like updating an online user
|
19
|
+
# counter, polling a backend for new status messages, sending regular
|
20
|
+
# "heartbeat" messages, or doing some internal work and giving progress updates.
|
21
|
+
#
|
22
|
+
# Pass a method name or lambda argument or provide a block to call. Specify the
|
23
|
+
# calling period in seconds using the `every:` keyword argument.
|
24
|
+
#
|
25
|
+
# periodically :transmit_progress, every: 5.seconds
|
26
|
+
#
|
27
|
+
# periodically every: 3.minutes do
|
28
|
+
# transmit action: :update_count, count: current_count
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
def periodically(callback_or_method_name = nil, every:, &block)
|
32
|
+
callback =
|
33
|
+
if block_given?
|
34
|
+
raise ArgumentError, "Pass a block or provide a callback arg, not both" if callback_or_method_name
|
35
|
+
block
|
36
|
+
else
|
37
|
+
case callback_or_method_name
|
38
|
+
when Proc
|
39
|
+
callback_or_method_name
|
40
|
+
when Symbol
|
41
|
+
-> { __send__ callback_or_method_name }
|
42
|
+
else
|
43
|
+
raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
unless every.kind_of?(Numeric) && every > 0
|
48
|
+
raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
|
49
|
+
end
|
50
|
+
|
51
|
+
self.periodic_timers += [[ callback, every: every ]]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def active_periodic_timers
|
57
|
+
@active_periodic_timers ||= []
|
58
|
+
end
|
59
|
+
|
60
|
+
def start_periodic_timers
|
61
|
+
self.class.periodic_timers.each do |callback, options|
|
62
|
+
active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def start_periodic_timer(timer_callback, every:)
|
67
|
+
# A callback must be executed within the channel context
|
68
|
+
callback = -> { instance_exec(&timer_callback) }
|
69
|
+
|
70
|
+
connection.executor.timer(every) do
|
71
|
+
connection.perform_work callback, :call
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def stop_periodic_timers
|
76
|
+
active_periodic_timers.each { |timer| timer.shutdown }
|
77
|
+
active_periodic_timers.clear
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|