turbo-rails 1.5.0 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,8 +14,8 @@
14
14
  # end
15
15
  # end
16
16
  #
17
- # This is an example from [HEY](https://hey.com), and the clearance is the model that drives
18
- # [the screener](https://hey.com/features/the-screener/), which gives users the power to deny first-time senders (petitioners)
17
+ # This is an example from {HEY}[https://hey.com], and the clearance is the model that drives
18
+ # {the screener}[https://hey.com/features/the-screener/], which gives users the power to deny first-time senders (petitioners)
19
19
  # access to their attention (as the examiner). When a new clearance is created upon receipt of an email from a first-time
20
20
  # sender, that'll trigger the call to broadcast_later, which in turn invokes <tt>broadcast_prepend_later_to</tt>.
21
21
  #
@@ -27,7 +27,7 @@
27
27
  # (which is derived by default from the plural model name of the model, but can be overwritten).
28
28
  #
29
29
  # You can also choose to render html instead of a partial inside of a broadcast
30
- # you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
30
+ # you do this by passing the +html:+ option to any broadcast method that accepts the **rendering argument. Example:
31
31
  #
32
32
  # class Message < ApplicationRecord
33
33
  # belongs_to :user
@@ -40,8 +40,8 @@
40
40
  # end
41
41
  # end
42
42
  #
43
- # If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the `template:` option.
44
- # Again, only to any broadcast method that accepts the `**rendering` argument. Example:
43
+ # If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the +template:+ option.
44
+ # Again, only to any broadcast method that accepts the +**rendering+ argument. Example:
45
45
  #
46
46
  # class Message < ApplicationRecord
47
47
  # belongs_to :user
@@ -54,7 +54,7 @@
54
54
  # end
55
55
  # end
56
56
  #
57
- # If you want to render a renderable object you can use the `renderable:` option.
57
+ # If you want to render a renderable object you can use the +renderable:+ option.
58
58
  #
59
59
  # class Message < ApplicationRecord
60
60
  # belongs_to :user
@@ -67,17 +67,99 @@
67
67
  # end
68
68
  # end
69
69
  #
70
- # There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
71
- # <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
70
+ # There are seven basic actions you can broadcast: <tt>after</tt>, <tt>append</tt>, <tt>before</tt>,
71
+ # <tt>prepend</tt>, <tt>remove</tt>, <tt>replace</tt>, and
72
+ # <tt>update</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
72
73
  # within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
73
74
  # execution. You don't need to do this for remove, since only the dom id for the model is used.
74
75
  #
75
- # In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
76
+ # In addition to the seven basic actions, you can also use <tt>broadcast_render</tt>,
76
77
  # <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
77
78
  # to render a turbo stream template with multiple actions.
79
+ #
80
+ # == Page refreshes
81
+ #
82
+ # You can broadcast "page refresh" stream actions. This will make subscribed clients reload the
83
+ # page. For pages that configure morphing and scroll preservation, this will translate into smooth
84
+ # updates when it only updates the content that changed.
85
+
86
+ # This approach is an alternative to fine-grained stream actions targeting specific DOM elements. It
87
+ # offers good fidelity with a much simpler programming model. As a tradeoff, the fidelity you can reach
88
+ # is often not as high as with targeted stream actions since it renders the entire page again.
89
+ #
90
+ # The +broadcast_refreshes+ class method configures the model to broadcast a "page refresh" on creates,
91
+ # updates, and destroys to a stream name derived at runtime by the <tt>stream</tt> symbol invocation. Examples
92
+ #
93
+ # class Board < ApplicationRecord
94
+ # broadcast_refreshes
95
+ # end
96
+ #
97
+ # In this example, when a board is created, updated, or destroyed, a Turbo Stream for a
98
+ # page refresh will be broadcasted to all clients subscribed to the "boards" stream.
99
+ #
100
+ # This works great in hierarchical structures, where the child record touches parent records automatically
101
+ # to invalidate the cache:
102
+ #
103
+ # class Column < ApplicationRecord
104
+ # belongs_to :board, touch: true # +Board+ will trigger a page refresh on column changes
105
+ # end
106
+ #
107
+ # You can also specify the streamable declaratively by passing a symbol to the +broadcast_refreshes_to+ method:
108
+ #
109
+ # class Column < ApplicationRecord
110
+ # belongs_to :board
111
+ # broadcast_refreshes_to :board
112
+ # end
113
+ #
114
+ # For more granular control, you can also broadcast a "page refresh" to a stream name derived
115
+ # from the passed <tt>streamables</tt> by using the instance-level methods <tt>broadcast_refresh_to</tt> or
116
+ # <tt>broadcast_refresh_later_to</tt>. These methods are particularly useful when you want to trigger
117
+ # a page refresh for more specific scenarios. Example:
118
+ #
119
+ # class Clearance < ApplicationRecord
120
+ # belongs_to :petitioner, class_name: "Contact"
121
+ # belongs_to :examiner, class_name: "User"
122
+ #
123
+ # after_create_commit :broadcast_refresh_later
124
+ #
125
+ # private
126
+ # def broadcast_refresh_later
127
+ # broadcast_refresh_later_to examiner.identity, :clearances
128
+ # end
129
+ # end
130
+ #
131
+ # In this example, a "page refresh" is broadcast to the stream named "identity:<identity-id>:clearances"
132
+ # after a new clearance is created. All clients subscribed to this stream will refresh the page to reflect
133
+ # the changes.
134
+ #
135
+ # When broadcasting page refreshes, Turbo will automatically debounce multiple calls in a row to only broadcast the last one.
136
+ # This is meant for scenarios where you process records in mass. Because of the nature of such signals, it makes no sense to
137
+ # broadcast them repeatedly and individually.
138
+ # == Suppressing broadcasts
139
+ #
140
+ # Sometimes, you need to disable broadcasts in certain scenarios. You can use <tt>.suppressing_turbo_broadcasts</tt> to create
141
+ # execution contexts where broadcasts are disabled:
142
+ #
143
+ # class Message < ApplicationRecord
144
+ # after_create_commit :update_message
145
+ #
146
+ # private
147
+ # def update_message
148
+ # broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
149
+ # end
150
+ # end
151
+ #
152
+ # Message.suppressing_turbo_broadcasts do
153
+ # Message.create!(board: board) # This won't broadcast the replace action
154
+ # end
78
155
  module Turbo::Broadcastable
79
156
  extend ActiveSupport::Concern
80
157
 
158
+ included do
159
+ thread_mattr_accessor :suppressed_turbo_broadcasts, instance_accessor: false
160
+ delegate :suppressed_turbo_broadcasts?, to: "self.class"
161
+ end
162
+
81
163
  module ClassMethods
82
164
  # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
83
165
  # <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
@@ -112,10 +194,46 @@ module Turbo::Broadcastable
112
194
  after_destroy_commit -> { broadcast_remove }
113
195
  end
114
196
 
197
+ # Configures the model to broadcast a "page refresh" on creates, updates, and destroys to a stream
198
+ # name derived at runtime by the <tt>stream</tt> symbol invocation. Examples:
199
+ #
200
+ # class Message < ApplicationRecord
201
+ # belongs_to :board
202
+ # broadcasts_refreshes_to :board
203
+ # end
204
+ #
205
+ # class Message < ApplicationRecord
206
+ # belongs_to :board
207
+ # broadcasts_refreshes_to ->(message) { [ message.board, :messages ] }
208
+ # end
209
+ def broadcasts_refreshes_to(stream)
210
+ after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
211
+ end
212
+
213
+ # Same as <tt>#broadcasts_refreshes_to</tt>, but the designated stream for page refreshes is automatically set to
214
+ # the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
215
+ def broadcasts_refreshes(stream = model_name.plural)
216
+ after_create_commit -> { broadcast_refresh_later_to(stream) }
217
+ after_update_commit -> { broadcast_refresh_later }
218
+ after_destroy_commit -> { broadcast_refresh }
219
+ end
220
+
115
221
  # All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
116
222
  def broadcast_target_default
117
223
  model_name.plural
118
224
  end
225
+
226
+ # Executes +block+ preventing both synchronous and asynchronous broadcasts from this model.
227
+ def suppressing_turbo_broadcasts(&block)
228
+ original, self.suppressed_turbo_broadcasts = self.suppressed_turbo_broadcasts, true
229
+ yield
230
+ ensure
231
+ self.suppressed_turbo_broadcasts = original
232
+ end
233
+
234
+ def suppressed_turbo_broadcasts?
235
+ suppressed_turbo_broadcasts
236
+ end
119
237
  end
120
238
 
121
239
  # Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
@@ -124,7 +242,7 @@ module Turbo::Broadcastable
124
242
  # # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
125
243
  # clearance.broadcast_remove_to examiner.identity, :clearances
126
244
  def broadcast_remove_to(*streamables, target: self)
127
- Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target)
245
+ Turbo::StreamsChannel.broadcast_remove_to(*streamables, target: target) unless suppressed_turbo_broadcasts?
128
246
  end
129
247
 
130
248
  # Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
@@ -143,7 +261,7 @@ module Turbo::Broadcastable
143
261
  # # to the stream named "identity:2:clearances"
144
262
  # clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
145
263
  def broadcast_replace_to(*streamables, **rendering)
146
- Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
264
+ Turbo::StreamsChannel.broadcast_replace_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
147
265
  end
148
266
 
149
267
  # Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
@@ -162,7 +280,7 @@ module Turbo::Broadcastable
162
280
  # # to the stream named "identity:2:clearances"
163
281
  # clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
164
282
  def broadcast_update_to(*streamables, **rendering)
165
- Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
283
+ Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
166
284
  end
167
285
 
168
286
  # Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
@@ -215,7 +333,7 @@ module Turbo::Broadcastable
215
333
  # clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
216
334
  # partial: "clearances/other_partial", locals: { a: 1 }
217
335
  def broadcast_append_to(*streamables, target: broadcast_target_default, **rendering)
218
- Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
336
+ Turbo::StreamsChannel.broadcast_append_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
219
337
  end
220
338
 
221
339
  # Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
@@ -236,7 +354,7 @@ module Turbo::Broadcastable
236
354
  # clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
237
355
  # partial: "clearances/other_partial", locals: { a: 1 }
238
356
  def broadcast_prepend_to(*streamables, target: broadcast_target_default, **rendering)
239
- Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
357
+ Turbo::StreamsChannel.broadcast_prepend_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
240
358
  end
241
359
 
242
360
  # Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
@@ -244,24 +362,36 @@ module Turbo::Broadcastable
244
362
  broadcast_prepend_to self, target: target, **rendering
245
363
  end
246
364
 
365
+ # Broadcast a "page refresh" to the stream name identified by the passed <tt>streamables</tt>. Example:
366
+ #
367
+ # # Sends <turbo-stream action="refresh"></turbo-stream> to the stream named "identity:2:clearances"
368
+ # clearance.broadcast_refresh_to examiner.identity, :clearances
369
+ def broadcast_refresh_to(*streamables)
370
+ Turbo::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_turbo_broadcasts?
371
+ end
372
+
373
+ # Same as <tt>#broadcast_refresh_to</tt>, but the designated stream is automatically set to the current model.
374
+ def broadcast_refresh
375
+ broadcast_refresh_to self
376
+ end
377
+
247
378
  # Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
248
379
  #
249
380
  # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
250
381
  # # to the stream named "identity:2:clearances"
251
382
  # clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
252
- def broadcast_action_to(*streamables, action:, target: broadcast_target_default, **rendering)
253
- Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
383
+ def broadcast_action_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
384
+ Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
254
385
  end
255
386
 
256
387
  # Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
257
- def broadcast_action(action, target: broadcast_target_default, **rendering)
258
- broadcast_action_to self, action: action, target: target, **rendering
388
+ def broadcast_action(action, target: broadcast_target_default, attributes: {}, **rendering)
389
+ broadcast_action_to self, action: action, target: target, attributes: attributes, **rendering
259
390
  end
260
391
 
261
-
262
392
  # Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
263
393
  def broadcast_replace_later_to(*streamables, **rendering)
264
- Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
394
+ Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
265
395
  end
266
396
 
267
397
  # Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -271,7 +401,7 @@ module Turbo::Broadcastable
271
401
 
272
402
  # Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
273
403
  def broadcast_update_later_to(*streamables, **rendering)
274
- Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
404
+ Turbo::StreamsChannel.broadcast_update_later_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
275
405
  end
276
406
 
277
407
  # Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -281,7 +411,7 @@ module Turbo::Broadcastable
281
411
 
282
412
  # Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
283
413
  def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
284
- Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
414
+ Turbo::StreamsChannel.broadcast_append_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
285
415
  end
286
416
 
287
417
  # Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -291,7 +421,7 @@ module Turbo::Broadcastable
291
421
 
292
422
  # Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
293
423
  def broadcast_prepend_later_to(*streamables, target: broadcast_target_default, **rendering)
294
- Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering))
424
+ Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, target: target, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
295
425
  end
296
426
 
297
427
  # Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
@@ -299,14 +429,24 @@ module Turbo::Broadcastable
299
429
  broadcast_prepend_later_to self, target: target, **rendering
300
430
  end
301
431
 
432
+ # Same as <tt>broadcast_refresh_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
433
+ def broadcast_refresh_later_to(*streamables)
434
+ Turbo::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Turbo.current_request_id) unless suppressed_turbo_broadcasts?
435
+ end
436
+
437
+ # Same as <tt>#broadcast_refresh_later_to</tt>, but the designated stream is automatically set to the current model.
438
+ def broadcast_refresh_later
439
+ broadcast_refresh_later_to self
440
+ end
441
+
302
442
  # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
303
- def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, **rendering)
304
- Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, **broadcast_rendering_with_defaults(rendering))
443
+ def broadcast_action_later_to(*streamables, action:, target: broadcast_target_default, attributes: {}, **rendering)
444
+ Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, target: target, attributes: attributes, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
305
445
  end
306
446
 
307
447
  # Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
308
- def broadcast_action_later(action:, target: broadcast_target_default, **rendering)
309
- broadcast_action_later_to self, action: action, target: target, **rendering
448
+ def broadcast_action_later(action:, target: broadcast_target_default, attributes: {}, **rendering)
449
+ broadcast_action_later_to self, action: action, target: target, attributes: attributes, **rendering
310
450
  end
311
451
 
312
452
  # Render a turbo stream template with this broadcastable model passed as the local variable. Example:
@@ -325,7 +465,7 @@ module Turbo::Broadcastable
325
465
  #
326
466
  # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
327
467
  # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
328
- # be using `broadcast_render_later`, unless you specifically know why synchronous rendering is needed.
468
+ # be using +broadcast_render_later+, unless you specifically know why synchronous rendering is needed.
329
469
  def broadcast_render(**rendering)
330
470
  broadcast_render_to self, **rendering
331
471
  end
@@ -335,12 +475,12 @@ module Turbo::Broadcastable
335
475
  #
336
476
  # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
337
477
  # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
338
- # be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
478
+ # be using +broadcast_render_later_to+, unless you specifically know why synchronous rendering is needed.
339
479
  def broadcast_render_to(*streamables, **rendering)
340
- Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering))
480
+ Turbo::StreamsChannel.broadcast_render_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
341
481
  end
342
482
 
343
- # Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
483
+ # Same as <tt>broadcast_render_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
344
484
  def broadcast_render_later(**rendering)
345
485
  broadcast_render_later_to self, **rendering
346
486
  end
@@ -348,10 +488,9 @@ module Turbo::Broadcastable
348
488
  # Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
349
489
  # <tt>streamables</tt>.
350
490
  def broadcast_render_later_to(*streamables, **rendering)
351
- Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering))
491
+ Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **broadcast_rendering_with_defaults(rendering)) unless suppressed_turbo_broadcasts?
352
492
  end
353
493
 
354
-
355
494
  private
356
495
  def broadcast_target_default
357
496
  self.class.broadcast_target_default
@@ -361,12 +500,14 @@ module Turbo::Broadcastable
361
500
  options.tap do |o|
362
501
  # Add the current instance into the locals with the element name (which is the un-namespaced name)
363
502
  # as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
364
- o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
503
+ o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self, request_id: Turbo.current_request_id).compact
365
504
 
366
505
  if o[:html] || o[:partial]
367
506
  return o
368
507
  elsif o[:template] || o[:renderable]
369
508
  o[:layout] = false
509
+ elsif o[:render] == false
510
+ return o
370
511
  else
371
512
  # if none of these options are passed in, it will set a partial from #to_partial_path
372
513
  o[:partial] ||= to_partial_path
@@ -0,0 +1,24 @@
1
+ class Turbo::Debouncer
2
+ attr_reader :delay, :scheduled_task
3
+
4
+ DEFAULT_DELAY = 0.5
5
+
6
+ def initialize(delay: DEFAULT_DELAY)
7
+ @delay = delay
8
+ @scheduled_task = nil
9
+ end
10
+
11
+ def debounce(&block)
12
+ scheduled_task&.cancel unless scheduled_task&.complete?
13
+ @scheduled_task = Concurrent::ScheduledTask.execute(delay, &block)
14
+ end
15
+
16
+ def wait
17
+ scheduled_task&.wait(wait_timeout)
18
+ end
19
+
20
+ private
21
+ def wait_timeout
22
+ delay + 1
23
+ end
24
+ end
@@ -22,6 +22,24 @@
22
22
  # <%= turbo_stream.append dom_id(topic_merge) do %>
23
23
  # <%= link_to topic_merge.topic.name, topic_path(topic_merge.topic) %>
24
24
  # <% end %>
25
+ #
26
+ # To integrate with custom actions, extend this class in response to the :turbo_streams_tag_builder load hook:
27
+ #
28
+ # ActiveSupport.on_load :turbo_streams_tag_builder do
29
+ # def highlight(target)
30
+ # action :highlight, target
31
+ # end
32
+ #
33
+ # def highlight_all(targets)
34
+ # action_all :highlight, targets
35
+ # end
36
+ # end
37
+ #
38
+ # turbo_stream.highlight "my-element"
39
+ # # => <turbo-stream action="highlight" target="my-element"><template></template></turbo-stream>
40
+ #
41
+ # turbo_stream.highlight_all ".my-selector"
42
+ # # => <turbo-stream action="highlight" targets=".my-selector"><template></template></turbo-stream>
25
43
  class Turbo::Streams::TagBuilder
26
44
  include Turbo::Streams::ActionHelper
27
45
 
@@ -246,4 +264,6 @@ class Turbo::Streams::TagBuilder
246
264
  @view_context.render(partial: record, formats: :html)
247
265
  end
248
266
  end
267
+
268
+ ActiveSupport.run_load_hooks :turbo_streams_tag_builder, self
249
269
  end
@@ -0,0 +1,28 @@
1
+ # A decorated debouncer that will store instances in the current thread clearing them
2
+ # after the debounced logic triggers.
3
+ class Turbo::ThreadDebouncer
4
+ delegate :wait, to: :debouncer
5
+
6
+ def self.for(key, delay: Turbo::Debouncer::DEFAULT_DELAY)
7
+ Thread.current[key] ||= new(key, Thread.current, delay: delay)
8
+ end
9
+
10
+ private_class_method :new
11
+
12
+ def initialize(key, thread, delay: )
13
+ @key = key
14
+ @debouncer = Turbo::Debouncer.new(delay: delay)
15
+ @thread = thread
16
+ end
17
+
18
+ def debounce
19
+ debouncer.debounce do
20
+ yield.tap do
21
+ thread[key] = nil
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+ attr_reader :key, :debouncer, :thread
28
+ end
data/config/routes.rb CHANGED
@@ -1,4 +1,3 @@
1
- # FIXME: Offer flag to opt out of these native routes
2
1
  Rails.application.routes.draw do
3
2
  get "recede_historical_location" => "turbo/native/navigation#recede", as: :turbo_recede_historical_location
4
3
  get "resume_historical_location" => "turbo/native/navigation#resume", as: :turbo_resume_historical_location
@@ -2,4 +2,4 @@ say "Import Turbo"
2
2
  append_to_file "app/javascript/application.js", %(import "@hotwired/turbo-rails"\n)
3
3
 
4
4
  say "Pin Turbo"
5
- append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true\n)
5
+ append_to_file "config/importmap.rb", %(pin "@hotwired/turbo-rails", to: "turbo.min.js"\n)
@@ -11,14 +11,14 @@ module Turbo
11
11
 
12
12
  # Asserts that `<turbo-stream>` elements were broadcast over Action Cable
13
13
  #
14
- # === Arguments
14
+ # ==== Arguments
15
15
  #
16
16
  # * <tt>stream_name_or_object</tt> the objects used to generate the
17
17
  # channel Action Cable name, or the name itself
18
18
  # * <tt>&block</tt> optional block executed before the
19
19
  # assertion
20
20
  #
21
- # === Options
21
+ # ==== Options
22
22
  #
23
23
  # * <tt>count:</tt> the number of `<turbo-stream>` elements that are
24
24
  # expected to be broadcast
@@ -64,13 +64,13 @@ module Turbo
64
64
  else
65
65
  broadcasts = "Turbo Stream broadcast".pluralize(count)
66
66
 
67
- assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were none"
67
+ assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were #{payloads.count}"
68
68
  end
69
69
  end
70
70
 
71
71
  # Asserts that no `<turbo-stream>` elements were broadcast over Action Cable
72
72
  #
73
- # === Arguments
73
+ # ==== Arguments
74
74
  #
75
75
  # * <tt>stream_name_or_object</tt> the objects used to generate the
76
76
  # channel Action Cable name, or the name itself
@@ -113,7 +113,7 @@ module Turbo
113
113
 
114
114
  # Captures any `<turbo-stream>` elements that were broadcast over Action Cable
115
115
  #
116
- # === Arguments
116
+ # ==== Arguments
117
117
  #
118
118
  # * <tt>stream_name_or_object</tt> the objects used to generate the
119
119
  # channel Action Cable name, or the name itself
data/lib/turbo/engine.rb CHANGED
@@ -46,6 +46,12 @@ module Turbo
46
46
  end
47
47
  end
48
48
 
49
+ initializer "turbo.request_id_tracking" do
50
+ ActiveSupport.on_load(:action_controller) do
51
+ include Turbo::RequestIdTracking
52
+ end
53
+ end
54
+
49
55
  initializer "turbo.broadcastable" do
50
56
  ActiveSupport.on_load(:active_record) do
51
57
  include Turbo::Broadcastable
@@ -78,6 +84,7 @@ module Turbo
78
84
  require "turbo/broadcastable/test_helper"
79
85
 
80
86
  include Turbo::TestAssertions
87
+ include Turbo::Broadcastable::TestHelper
81
88
  end
82
89
 
83
90
  ActiveSupport.on_load(:action_dispatch_integration_test) do
@@ -4,7 +4,7 @@ module Turbo
4
4
  # Assert that the Turbo Stream request's response body's HTML contains a
5
5
  # `<turbo-stream>` element.
6
6
  #
7
- # === Options
7
+ # ==== Options
8
8
  #
9
9
  # * <tt>:status</tt> [Integer, Symbol] the HTTP response status
10
10
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
@@ -47,7 +47,7 @@ module Turbo
47
47
  # Assert that the Turbo Stream request's response body's HTML does not
48
48
  # contain a `<turbo-stream>` element.
49
49
  #
50
- # === Options
50
+ # ==== Options
51
51
  #
52
52
  # * <tt>:status</tt> [Integer, Symbol] the HTTP response status
53
53
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
@@ -10,7 +10,7 @@ module Turbo
10
10
  # Assert that the rendered fragment of HTML contains a `<turbo-stream>`
11
11
  # element.
12
12
  #
13
- # === Options
13
+ # ==== Options
14
14
  #
15
15
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
16
16
  # attribute
@@ -55,7 +55,7 @@ module Turbo
55
55
  # Assert that the rendered fragment of HTML does not contain a `<turbo-stream>`
56
56
  # element.
57
57
  #
58
- # === Options
58
+ # ==== Options
59
59
  #
60
60
  # * <tt>:action</tt> [String] matches the element's <tt>[action]</tt>
61
61
  # attribute
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "1.5.0"
2
+ VERSION = "2.0.2"
3
3
  end
data/lib/turbo-rails.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require "turbo/engine"
2
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
2
3
 
3
4
  module Turbo
4
5
  extend ActiveSupport::Autoload
5
6
 
6
7
  mattr_accessor :draw_routes, default: true
7
8
 
9
+ thread_mattr_accessor :current_request_id
10
+
8
11
  class << self
9
12
  attr_writer :signed_stream_verifier_key
10
13
 
@@ -15,5 +18,12 @@ module Turbo
15
18
  def signed_stream_verifier_key
16
19
  @signed_stream_verifier_key or raise ArgumentError, "Turbo requires a signed_stream_verifier_key"
17
20
  end
21
+
22
+ def with_request_id(request_id)
23
+ old_request_id, self.current_request_id = self.current_request_id, request_id
24
+ yield
25
+ ensure
26
+ self.current_request_id = old_request_id
27
+ end
18
28
  end
19
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-10-11 00:00:00.000000000 Z
13
+ date: 2024-02-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activejob
@@ -69,6 +69,7 @@ files:
69
69
  - app/channels/turbo/streams/broadcasts.rb
70
70
  - app/channels/turbo/streams/stream_name.rb
71
71
  - app/channels/turbo/streams_channel.rb
72
+ - app/controllers/concerns/turbo/request_id_tracking.rb
72
73
  - app/controllers/turbo/frames/frame_request.rb
73
74
  - app/controllers/turbo/native/navigation.rb
74
75
  - app/controllers/turbo/native/navigation_controller.rb
@@ -85,8 +86,11 @@ files:
85
86
  - app/javascript/turbo/snakeize.js
86
87
  - app/jobs/turbo/streams/action_broadcast_job.rb
87
88
  - app/jobs/turbo/streams/broadcast_job.rb
89
+ - app/jobs/turbo/streams/broadcast_stream_job.rb
88
90
  - app/models/concerns/turbo/broadcastable.rb
91
+ - app/models/turbo/debouncer.rb
89
92
  - app/models/turbo/streams/tag_builder.rb
93
+ - app/models/turbo/thread_debouncer.rb
90
94
  - app/views/layouts/turbo_rails/frame.html.erb
91
95
  - config/routes.rb
92
96
  - lib/install/turbo_needs_redis.rb