turbo-rails 1.5.0 → 2.0.2

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.
@@ -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