actioncable 7.0.8.7 → 7.2.2.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -180
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +4 -4
  5. data/app/assets/javascripts/action_cable.js +30 -9
  6. data/app/assets/javascripts/actioncable.esm.js +30 -9
  7. data/app/assets/javascripts/actioncable.js +30 -9
  8. data/lib/action_cable/channel/base.rb +114 -90
  9. data/lib/action_cable/channel/broadcasting.rb +25 -16
  10. data/lib/action_cable/channel/callbacks.rb +39 -0
  11. data/lib/action_cable/channel/naming.rb +10 -7
  12. data/lib/action_cable/channel/periodic_timers.rb +7 -7
  13. data/lib/action_cable/channel/streams.rb +77 -62
  14. data/lib/action_cable/channel/test_case.rb +117 -86
  15. data/lib/action_cable/connection/authorization.rb +4 -1
  16. data/lib/action_cable/connection/base.rb +70 -42
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +3 -1
  19. data/lib/action_cable/connection/identification.rb +9 -5
  20. data/lib/action_cable/connection/internal_channel.rb +7 -2
  21. data/lib/action_cable/connection/message_buffer.rb +4 -1
  22. data/lib/action_cable/connection/stream.rb +2 -2
  23. data/lib/action_cable/connection/stream_event_loop.rb +4 -4
  24. data/lib/action_cable/connection/subscriptions.rb +7 -2
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +12 -7
  26. data/lib/action_cable/connection/test_case.rb +67 -55
  27. data/lib/action_cable/connection/web_socket.rb +11 -7
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +18 -8
  30. data/lib/action_cable/gem_version.rb +6 -4
  31. data/lib/action_cable/helpers/action_cable_helper.rb +21 -19
  32. data/lib/action_cable/remote_connections.rb +25 -13
  33. data/lib/action_cable/server/base.rb +29 -14
  34. data/lib/action_cable/server/broadcasting.rb +24 -16
  35. data/lib/action_cable/server/configuration.rb +27 -14
  36. data/lib/action_cable/server/connections.rb +13 -5
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -0
  38. data/lib/action_cable/server/worker.rb +4 -3
  39. data/lib/action_cable/subscription_adapter/async.rb +1 -1
  40. data/lib/action_cable/subscription_adapter/base.rb +2 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +2 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +2 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +4 -3
  44. data/lib/action_cable/subscription_adapter/redis.rb +7 -7
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +2 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +6 -5
  47. data/lib/action_cable/test_case.rb +2 -0
  48. data/lib/action_cable/test_helper.rb +89 -59
  49. data/lib/action_cable/version.rb +3 -1
  50. data/lib/action_cable.rb +30 -12
  51. data/lib/rails/generators/channel/USAGE +14 -8
  52. data/lib/rails/generators/channel/channel_generator.rb +23 -7
  53. data/lib/rails/generators/test_unit/channel_generator.rb +2 -0
  54. metadata +27 -15
  55. data/lib/action_cable/channel.rb +0 -17
  56. data/lib/action_cable/connection.rb +0 -22
  57. data/lib/action_cable/server.rb +0 -16
  58. data/lib/action_cable/subscription_adapter.rb +0 -12
  59. /data/lib/rails/generators/channel/templates/application_cable/{channel.rb → channel.rb.tt} +0 -0
  60. /data/lib/rails/generators/channel/templates/application_cable/{connection.rb → connection.rb.tt} +0 -0
@@ -1,99 +1,112 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "set"
4
6
  require "active_support/rescuable"
7
+ require "active_support/parameter_filter"
5
8
 
6
9
  module ActionCable
7
10
  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
+ # # Action Cable Channel Base
11
12
  #
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.
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.
16
18
  #
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
+ # 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.
19
26
  #
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:
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.
22
31
  #
23
- # class ChatChannel < ApplicationCable::Channel
24
- # def subscribed
25
- # @room = Chat::Room[params[:room_number]]
26
- # end
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:
27
35
  #
28
- # def speak(data)
29
- # @room.speak data, user: current_user
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
30
44
  # end
31
- # end
32
45
  #
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.
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.
35
49
  #
36
- # == Action processing
50
+ # ## Action processing
37
51
  #
38
52
  # Unlike subclasses of ActionController::Base, channels do not follow a RESTful
39
53
  # 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.
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.
43
57
  #
44
58
  # Example:
45
59
  #
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
60
+ # class AppearanceChannel < ApplicationCable::Channel
61
+ # def subscribed
62
+ # @connection_token = generate_connection_token
63
+ # end
54
64
  #
55
- # def appear(data)
56
- # current_user.appear @connection_token, on: data['appearing_on']
57
- # end
65
+ # def unsubscribed
66
+ # current_user.disappear @connection_token
67
+ # end
58
68
  #
59
- # def away
60
- # current_user.away @connection_token
61
- # end
69
+ # def appear(data)
70
+ # current_user.appear @connection_token, on: data['appearing_on']
71
+ # end
62
72
  #
63
- # private
64
- # def generate_connection_token
65
- # SecureRandom.hex(36)
73
+ # def away
74
+ # current_user.away @connection_token
66
75
  # end
67
- # end
68
76
  #
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.
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.
75
89
  #
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.
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.
80
94
  #
81
- # == Rejecting subscription requests
95
+ # ## Rejecting subscription requests
82
96
  #
83
97
  # A channel can reject a subscription request in the #subscribed callback by
84
98
  # invoking the #reject method:
85
99
  #
86
- # class ChatChannel < ApplicationCable::Channel
87
- # def subscribed
88
- # @room = Chat::Room[params[:room_number]]
89
- # reject unless current_user.can_access?(@room)
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
90
105
  # end
91
- # end
92
106
  #
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.
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.
97
110
  class Base
98
111
  include Callbacks
99
112
  include PeriodicTimers
@@ -106,14 +119,13 @@ module ActionCable
106
119
  delegate :logger, to: :connection
107
120
 
108
121
  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.
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.
114
126
  #
115
- # ==== Returns
116
- # * <tt>Set</tt> - A set of all methods that should be considered actions.
127
+ # #### Returns
128
+ # * `Set` - A set of all methods that should be considered actions.
117
129
  def action_methods
118
130
  @action_methods ||= begin
119
131
  # All public instance methods of this class, including ancestors
@@ -127,9 +139,9 @@ module ActionCable
127
139
  end
128
140
 
129
141
  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.
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.
133
145
  def clear_action_methods! # :doc:
134
146
  @action_methods = nil
135
147
  end
@@ -158,9 +170,9 @@ module ActionCable
158
170
  delegate_connection_identifiers
159
171
  end
160
172
 
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).
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).
164
176
  def perform_action(data)
165
177
  action = extract_action(data)
166
178
 
@@ -174,8 +186,8 @@ module ActionCable
174
186
  end
175
187
  end
176
188
 
177
- # This method is called after subscription has been added to the connection
178
- # and confirms or rejects the subscription.
189
+ # This method is called after subscription has been added to the connection and
190
+ # confirms or rejects the subscription.
179
191
  def subscribe_to_channel
180
192
  run_callbacks :subscribe do
181
193
  subscribed
@@ -185,8 +197,9 @@ module ActionCable
185
197
  ensure_confirmation_sent
186
198
  end
187
199
 
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, override the #unsubscribed callback.
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.
190
203
  def unsubscribe_from_channel # :nodoc:
191
204
  run_callbacks :unsubscribe do
192
205
  unsubscribed
@@ -194,24 +207,28 @@ module ActionCable
194
207
  end
195
208
 
196
209
  private
197
- # Called once a consumer has become a subscriber of the channel. Usually the place to set up any streams
198
- # you want this channel to be sending to the subscriber.
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.
199
213
  def subscribed # :doc:
200
214
  # Override in subclasses
201
215
  end
202
216
 
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.
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.
205
219
  def unsubscribed # :doc:
206
220
  # Override in subclasses
207
221
  end
208
222
 
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.
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.
211
226
  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)
227
+ logger.debug do
228
+ status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
229
+ status += " (via #{via})" if via
230
+ status
231
+ end
215
232
 
216
233
  payload = { channel_class: self.class.name, data: data, via: via }
217
234
  ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
@@ -262,7 +279,7 @@ module ActionCable
262
279
  end
263
280
 
264
281
  def dispatch_action(action, data)
265
- logger.info action_signature(action, data)
282
+ logger.debug action_signature(action, data)
266
283
 
267
284
  if method(action).arity == 1
268
285
  public_send action, data
@@ -275,12 +292,19 @@ module ActionCable
275
292
 
276
293
  def action_signature(action, data)
277
294
  (+"#{self.class.name}##{action}").tap do |signature|
278
- if (arguments = data.except("action")).any?
295
+ arguments = data.except("action")
296
+
297
+ if arguments.any?
298
+ arguments = parameter_filter.filter(arguments)
279
299
  signature << "(#{arguments.inspect})"
280
300
  end
281
301
  end
282
302
  end
283
303
 
304
+ def parameter_filter
305
+ @parameter_filter ||= ActiveSupport::ParameterFilter.new(connection.config.filter_parameters)
306
+ end
307
+
284
308
  def transmit_subscription_confirmation
285
309
  unless subscription_confirmation_sent?
286
310
  logger.debug "#{self.class.name} is transmitting the subscription confirmation"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/core_ext/object/to_param"
4
6
 
5
7
  module ActionCable
@@ -7,34 +9,41 @@ module ActionCable
7
9
  module Broadcasting
8
10
  extend ActiveSupport::Concern
9
11
 
10
- delegate :broadcasting_for, :broadcast_to, to: :class
11
-
12
12
  module ClassMethods
13
- # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
13
+ # Broadcast a hash to a unique broadcasting for this `model` in this channel.
14
14
  def broadcast_to(model, message)
15
15
  ActionCable.server.broadcast(broadcasting_for(model), message)
16
16
  end
17
17
 
18
- # Returns a unique broadcasting identifier for this <tt>model</tt> in this channel:
18
+ # Returns a unique broadcasting identifier for this `model` in this channel:
19
19
  #
20
- # CommentsChannel.broadcasting_for("all") # => "comments:all"
20
+ # CommentsChannel.broadcasting_for("all") # => "comments:all"
21
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.
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
24
  def broadcasting_for(model)
25
25
  serialize_broadcasting([ channel_name, model ])
26
26
  end
27
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
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
36
38
  end
37
- 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)
38
47
  end
39
48
  end
40
49
  end
@@ -1,9 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/callbacks"
4
6
 
5
7
  module ActionCable
6
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
+ #
7
38
  module Callbacks
8
39
  extend ActiveSupport::Concern
9
40
  include ActiveSupport::Callbacks
@@ -18,6 +49,14 @@ module ActionCable
18
49
  set_callback(:subscribe, :before, *methods, &block)
19
50
  end
20
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
+ #
21
60
  def after_subscribe(*methods, &block)
22
61
  set_callback(:subscribe, :after, *methods, &block)
23
62
  end
@@ -1,25 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module Channel
5
7
  module Naming
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  module ClassMethods
9
- # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
10
- # If the channel is in a namespace, then the namespaces are represented by single
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
11
13
  # colon separators in the channel name.
12
14
  #
13
- # ChatChannel.channel_name # => 'chat'
14
- # Chats::AppearancesChannel.channel_name # => 'chats:appearances'
15
- # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
15
+ # ChatChannel.channel_name # => 'chat'
16
+ # Chats::AppearancesChannel.channel_name # => 'chats:appearances'
17
+ # FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
16
18
  def channel_name
17
19
  @channel_name ||= name.delete_suffix("Channel").gsub("::", ":").underscore
18
20
  end
19
21
  end
20
22
 
21
- # Delegates to the class's ::channel_name.
22
- delegate :channel_name, to: :class
23
+ def channel_name
24
+ self.class.channel_name
25
+ end
23
26
  end
24
27
  end
25
28
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionCable
4
6
  module Channel
5
7
  module PeriodicTimers
@@ -13,14 +15,12 @@ module ActionCable
13
15
  end
14
16
 
15
17
  module ClassMethods
16
- # Periodically performs a task on the channel, like updating an online
17
- # user counter, polling a backend for new status messages, sending
18
- # regular "heartbeat" messages, or doing some internal work and giving
19
- # progress updates.
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.
20
21
  #
21
- # Pass a method name or lambda argument or provide a block to call.
22
- # Specify the calling period in seconds using the <tt>every:</tt>
23
- # keyword argument.
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
24
  #
25
25
  # periodically :transmit_progress, every: 5.seconds
26
26
  #