hotsock-turbo 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.
@@ -0,0 +1,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Hotsock
6
+ module Turbo
7
+ # Provides broadcast functionality for ActiveRecord models.
8
+ # Include this module to gain access to hotsock_broadcast methods
9
+ # that mirror turbo-rails' broadcast methods but use Hotsock for delivery.
10
+ #
11
+ # Example usage:
12
+ #
13
+ # class Board < ApplicationRecord
14
+ # hotsock_broadcasts_refreshes
15
+ # end
16
+ #
17
+ # class Column < ApplicationRecord
18
+ # belongs_to :board
19
+ # hotsock_broadcasts_refreshes_to :board
20
+ # end
21
+ #
22
+ # class Message < ApplicationRecord
23
+ # belongs_to :board
24
+ # hotsock_broadcasts_to :board
25
+ # end
26
+ #
27
+ module Broadcastable
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ thread_mattr_accessor :suppressed_turbo_broadcasts, instance_accessor: false
32
+ delegate :suppressed_turbo_broadcasts?, to: "self.class"
33
+ end
34
+
35
+ module ClassMethods
36
+ # Configures the model to broadcast creates, updates, and destroys to a stream name
37
+ # derived at runtime by the +stream+ symbol invocation.
38
+ def hotsock_broadcasts_to(stream, inserts_by: :append, target: hotsock_broadcast_target_default, **rendering)
39
+ after_create_commit -> {
40
+ hotsock_broadcast_action_later_to(
41
+ stream.try(:call, self) || send(stream),
42
+ action: inserts_by,
43
+ target: target.try(:call, self) || target,
44
+ **rendering
45
+ )
46
+ }
47
+ after_update_commit -> { hotsock_broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
48
+ after_destroy_commit -> { hotsock_broadcast_remove_to(stream.try(:call, self) || send(stream)) }
49
+ end
50
+
51
+ # Same as +hotsock_broadcasts_to+, but the designated stream for updates and destroys
52
+ # is automatically set to the current model, for creates - to the model plural name.
53
+ def hotsock_broadcasts(stream = model_name.plural, inserts_by: :append, target: hotsock_broadcast_target_default, **rendering)
54
+ after_create_commit -> {
55
+ hotsock_broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering)
56
+ }
57
+ after_update_commit -> { hotsock_broadcast_replace_later(**rendering) }
58
+ after_destroy_commit -> { hotsock_broadcast_remove }
59
+ end
60
+
61
+ # Configures the model to broadcast a "page refresh" on creates, updates, and destroys
62
+ # to a stream name derived at runtime by the +stream+ symbol invocation.
63
+ def hotsock_broadcasts_refreshes_to(stream)
64
+ after_commit -> { hotsock_broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
65
+ end
66
+
67
+ # Same as +hotsock_broadcasts_refreshes_to+, but the designated stream for page refreshes
68
+ # is automatically set to the model plural name, which can be overridden by passing +stream+.
69
+ # Uses async for create/update and sync for destroy (matching turbo-rails behavior).
70
+ def hotsock_broadcasts_refreshes(stream = model_name.plural)
71
+ after_create_commit -> { hotsock_broadcast_refresh_later_to(stream) }
72
+ after_update_commit -> { hotsock_broadcast_refresh_later }
73
+ after_destroy_commit -> { hotsock_broadcast_refresh }
74
+ end
75
+
76
+ # All default targets will use the return of this method.
77
+ def hotsock_broadcast_target_default
78
+ model_name.plural
79
+ end
80
+
81
+ # Executes +block+ preventing both synchronous and asynchronous broadcasts from this model.
82
+ def suppressing_turbo_broadcasts(&block)
83
+ original, self.suppressed_turbo_broadcasts = suppressed_turbo_broadcasts, true
84
+ yield
85
+ ensure
86
+ self.suppressed_turbo_broadcasts = original
87
+ end
88
+
89
+ def suppressed_turbo_broadcasts?
90
+ suppressed_turbo_broadcasts
91
+ end
92
+ end
93
+
94
+ # ==================
95
+ # Sync methods
96
+ # ==================
97
+
98
+ def hotsock_broadcast_remove_to(*streamables, target: self, **rendering)
99
+ unless suppressed_turbo_broadcasts?
100
+ Hotsock::Turbo::StreamsChannel.broadcast_remove_to(
101
+ *streamables,
102
+ target: hotsock_extract_target(target)
103
+ )
104
+ end
105
+ end
106
+
107
+ def hotsock_broadcast_remove(**rendering)
108
+ hotsock_broadcast_remove_to(self, **rendering)
109
+ end
110
+
111
+ def hotsock_broadcast_replace_to(*streamables, **rendering)
112
+ unless suppressed_turbo_broadcasts?
113
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
114
+ *streamables,
115
+ action: :replace,
116
+ **hotsock_broadcast_rendering_with_defaults(rendering)
117
+ )
118
+ end
119
+ end
120
+
121
+ def hotsock_broadcast_replace(**rendering)
122
+ hotsock_broadcast_replace_to(self, **rendering)
123
+ end
124
+
125
+ def hotsock_broadcast_update_to(*streamables, **rendering)
126
+ unless suppressed_turbo_broadcasts?
127
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
128
+ *streamables,
129
+ action: :update,
130
+ **hotsock_broadcast_rendering_with_defaults(rendering)
131
+ )
132
+ end
133
+ end
134
+
135
+ def hotsock_broadcast_update(**rendering)
136
+ hotsock_broadcast_update_to(self, **rendering)
137
+ end
138
+
139
+ def hotsock_broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
140
+ raise ArgumentError, "at least one of target or targets is required" unless target || targets
141
+ return if suppressed_turbo_broadcasts?
142
+
143
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
144
+ *streamables,
145
+ action: :before,
146
+ target:,
147
+ targets:,
148
+ **hotsock_broadcast_rendering_with_defaults(rendering, target: nil)
149
+ )
150
+ end
151
+
152
+ def hotsock_broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
153
+ raise ArgumentError, "at least one of target or targets is required" unless target || targets
154
+ return if suppressed_turbo_broadcasts?
155
+
156
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
157
+ *streamables,
158
+ action: :after,
159
+ target:,
160
+ targets:,
161
+ **hotsock_broadcast_rendering_with_defaults(rendering, target: nil)
162
+ )
163
+ end
164
+
165
+ def hotsock_broadcast_append_to(*streamables, target: hotsock_broadcast_target_default, **rendering)
166
+ unless suppressed_turbo_broadcasts?
167
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
168
+ *streamables,
169
+ action: :append,
170
+ **hotsock_broadcast_rendering_with_defaults(rendering, target: target)
171
+ )
172
+ end
173
+ end
174
+
175
+ def hotsock_broadcast_append(target: hotsock_broadcast_target_default, **rendering)
176
+ hotsock_broadcast_append_to(self, target: target, **rendering)
177
+ end
178
+
179
+ def hotsock_broadcast_prepend_to(*streamables, target: hotsock_broadcast_target_default, **rendering)
180
+ unless suppressed_turbo_broadcasts?
181
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
182
+ *streamables,
183
+ action: :prepend,
184
+ **hotsock_broadcast_rendering_with_defaults(rendering, target: target)
185
+ )
186
+ end
187
+ end
188
+
189
+ def hotsock_broadcast_prepend(target: hotsock_broadcast_target_default, **rendering)
190
+ hotsock_broadcast_prepend_to(self, target: target, **rendering)
191
+ end
192
+
193
+ def hotsock_broadcast_refresh_to(*streamables, **attributes)
194
+ Hotsock::Turbo::StreamsChannel.broadcast_refresh_to(*streamables, **attributes) unless suppressed_turbo_broadcasts?
195
+ end
196
+
197
+ def hotsock_broadcast_refresh
198
+ hotsock_broadcast_refresh_to(self)
199
+ end
200
+
201
+ def hotsock_broadcast_action_to(*streamables, action:, target: hotsock_broadcast_target_default, attributes: {}, **rendering)
202
+ unless suppressed_turbo_broadcasts?
203
+ Hotsock::Turbo::StreamsChannel.broadcast_action_to(
204
+ *streamables,
205
+ action:,
206
+ attributes:,
207
+ **hotsock_broadcast_rendering_with_defaults(rendering, target: target)
208
+ )
209
+ end
210
+ end
211
+
212
+ def hotsock_broadcast_action(action, target: hotsock_broadcast_target_default, attributes: {}, **rendering)
213
+ hotsock_broadcast_action_to(self, action:, target:, attributes:, **rendering)
214
+ end
215
+
216
+ def hotsock_broadcast_render_to(*streamables, **rendering)
217
+ unless suppressed_turbo_broadcasts?
218
+ Hotsock::Turbo::StreamsChannel.broadcast_render_to(
219
+ *streamables,
220
+ **hotsock_broadcast_rendering_with_defaults(rendering)
221
+ )
222
+ end
223
+ end
224
+
225
+ def hotsock_broadcast_render(**rendering)
226
+ hotsock_broadcast_render_to(self, **rendering)
227
+ end
228
+
229
+ # ==================
230
+ # Async methods
231
+ # ==================
232
+
233
+ def hotsock_broadcast_replace_later_to(*streamables, **rendering)
234
+ unless suppressed_turbo_broadcasts?
235
+ Hotsock::Turbo::StreamsChannel.broadcast_replace_later_to(
236
+ *streamables,
237
+ **hotsock_broadcast_rendering_with_defaults(rendering)
238
+ )
239
+ end
240
+ end
241
+
242
+ def hotsock_broadcast_replace_later(**rendering)
243
+ hotsock_broadcast_replace_later_to(self, **rendering)
244
+ end
245
+
246
+ def hotsock_broadcast_update_later_to(*streamables, **rendering)
247
+ unless suppressed_turbo_broadcasts?
248
+ Hotsock::Turbo::StreamsChannel.broadcast_update_later_to(
249
+ *streamables,
250
+ **hotsock_broadcast_rendering_with_defaults(rendering)
251
+ )
252
+ end
253
+ end
254
+
255
+ def hotsock_broadcast_update_later(**rendering)
256
+ hotsock_broadcast_update_later_to(self, **rendering)
257
+ end
258
+
259
+ def hotsock_broadcast_append_later_to(*streamables, target: hotsock_broadcast_target_default, **rendering)
260
+ unless suppressed_turbo_broadcasts?
261
+ Hotsock::Turbo::StreamsChannel.broadcast_append_later_to(
262
+ *streamables,
263
+ **hotsock_broadcast_rendering_with_defaults(rendering, target:)
264
+ )
265
+ end
266
+ end
267
+
268
+ def hotsock_broadcast_append_later(target: hotsock_broadcast_target_default, **rendering)
269
+ hotsock_broadcast_append_later_to(self, target:, **rendering)
270
+ end
271
+
272
+ def hotsock_broadcast_prepend_later_to(*streamables, target: hotsock_broadcast_target_default, **rendering)
273
+ unless suppressed_turbo_broadcasts?
274
+ Hotsock::Turbo::StreamsChannel.broadcast_prepend_later_to(
275
+ *streamables,
276
+ **hotsock_broadcast_rendering_with_defaults(rendering, target:)
277
+ )
278
+ end
279
+ end
280
+
281
+ def hotsock_broadcast_prepend_later(target: hotsock_broadcast_target_default, **rendering)
282
+ hotsock_broadcast_prepend_later_to(self, target:, **rendering)
283
+ end
284
+
285
+ def hotsock_broadcast_refresh_later_to(*streamables, **attributes)
286
+ unless suppressed_turbo_broadcasts?
287
+ Hotsock::Turbo::StreamsChannel.broadcast_refresh_later_to(
288
+ *streamables,
289
+ request_id: hotsock_turbo_current_request_id,
290
+ **attributes
291
+ )
292
+ end
293
+ end
294
+
295
+ def hotsock_broadcast_refresh_later
296
+ hotsock_broadcast_refresh_later_to(self)
297
+ end
298
+
299
+ def hotsock_broadcast_action_later_to(*streamables, action:, target: hotsock_broadcast_target_default, attributes: {}, **rendering)
300
+ unless suppressed_turbo_broadcasts?
301
+ Hotsock::Turbo::StreamsChannel.broadcast_action_later_to(
302
+ *streamables,
303
+ action:,
304
+ attributes:,
305
+ **hotsock_broadcast_rendering_with_defaults(rendering, target:)
306
+ )
307
+ end
308
+ end
309
+
310
+ def hotsock_broadcast_action_later(action:, target: hotsock_broadcast_target_default, attributes: {}, **rendering)
311
+ hotsock_broadcast_action_later_to(self, action:, target:, attributes: attributes, **rendering)
312
+ end
313
+
314
+ def hotsock_broadcast_render_later_to(*streamables, **rendering)
315
+ unless suppressed_turbo_broadcasts?
316
+ Hotsock::Turbo::StreamsChannel.broadcast_render_later_to(
317
+ *streamables,
318
+ **hotsock_broadcast_rendering_with_defaults(rendering)
319
+ )
320
+ end
321
+ end
322
+
323
+ def hotsock_broadcast_render_later(**rendering)
324
+ hotsock_broadcast_render_later_to(self, **rendering)
325
+ end
326
+
327
+ private
328
+
329
+ def hotsock_broadcast_target_default
330
+ self.class.hotsock_broadcast_target_default
331
+ end
332
+
333
+ def hotsock_extract_target(target)
334
+ if target.respond_to?(:to_key)
335
+ ActionView::RecordIdentifier.dom_id(target)
336
+ else
337
+ target
338
+ end
339
+ end
340
+
341
+ def hotsock_broadcast_rendering_with_defaults(options, target: self)
342
+ options = options.dup
343
+ options[:target] = hotsock_extract_target(target) if target && !options.key?(:target) && !options.key?(:targets)
344
+ options[:locals] = (options[:locals] || {}).reverse_merge(model_name.element.to_sym => self)
345
+ options[:partial] ||= to_partial_path unless options[:html] || options[:template] || options[:renderable]
346
+ options
347
+ end
348
+
349
+ def hotsock_turbo_current_request_id
350
+ Turbo.current_request_id if defined?(Turbo) && Turbo.respond_to?(:current_request_id)
351
+ end
352
+
353
+ # Override module that aliases standard Turbo::Broadcastable method names
354
+ # to Hotsock equivalents, enabling drop-in replacement.
355
+ module TurboBroadcastableOverride
356
+ extend ActiveSupport::Concern
357
+
358
+ module ClassMethods
359
+ def broadcasts_to(stream, inserts_by: :append, target: hotsock_broadcast_target_default, **rendering)
360
+ hotsock_broadcasts_to(stream, inserts_by:, target:, **rendering)
361
+ end
362
+
363
+ def broadcasts(stream = model_name.plural, inserts_by: :append, target: hotsock_broadcast_target_default, **rendering)
364
+ hotsock_broadcasts(stream, inserts_by:, target:, **rendering)
365
+ end
366
+
367
+ def broadcasts_refreshes_to(stream)
368
+ hotsock_broadcasts_refreshes_to(stream)
369
+ end
370
+
371
+ def broadcasts_refreshes(stream = model_name.plural)
372
+ hotsock_broadcasts_refreshes(stream)
373
+ end
374
+
375
+ def broadcast_target_default
376
+ hotsock_broadcast_target_default
377
+ end
378
+ end
379
+
380
+ # Sync instance methods
381
+ def broadcast_remove_to(*streamables, **opts)
382
+ hotsock_broadcast_remove_to(*streamables, **opts)
383
+ end
384
+
385
+ def broadcast_remove(**opts)
386
+ hotsock_broadcast_remove(**opts)
387
+ end
388
+
389
+ def broadcast_replace_to(*streamables, **opts)
390
+ hotsock_broadcast_replace_to(*streamables, **opts)
391
+ end
392
+
393
+ def broadcast_replace(**opts)
394
+ hotsock_broadcast_replace(**opts)
395
+ end
396
+
397
+ def broadcast_update_to(*streamables, **opts)
398
+ hotsock_broadcast_update_to(*streamables, **opts)
399
+ end
400
+
401
+ def broadcast_update(**opts)
402
+ hotsock_broadcast_update(**opts)
403
+ end
404
+
405
+ def broadcast_before_to(*streamables, **opts)
406
+ hotsock_broadcast_before_to(*streamables, **opts)
407
+ end
408
+
409
+ def broadcast_after_to(*streamables, **opts)
410
+ hotsock_broadcast_after_to(*streamables, **opts)
411
+ end
412
+
413
+ def broadcast_append_to(*streamables, **opts)
414
+ hotsock_broadcast_append_to(*streamables, **opts)
415
+ end
416
+
417
+ def broadcast_append(**opts)
418
+ hotsock_broadcast_append(**opts)
419
+ end
420
+
421
+ def broadcast_prepend_to(*streamables, **opts)
422
+ hotsock_broadcast_prepend_to(*streamables, **opts)
423
+ end
424
+
425
+ def broadcast_prepend(**opts)
426
+ hotsock_broadcast_prepend(**opts)
427
+ end
428
+
429
+ def broadcast_refresh_to(*streamables, **opts)
430
+ hotsock_broadcast_refresh_to(*streamables, **opts)
431
+ end
432
+
433
+ def broadcast_refresh
434
+ hotsock_broadcast_refresh
435
+ end
436
+
437
+ def broadcast_action_to(*streamables, **opts)
438
+ hotsock_broadcast_action_to(*streamables, **opts)
439
+ end
440
+
441
+ def broadcast_action(action, **opts)
442
+ hotsock_broadcast_action(action, **opts)
443
+ end
444
+
445
+ def broadcast_render_to(*streamables, **opts)
446
+ hotsock_broadcast_render_to(*streamables, **opts)
447
+ end
448
+
449
+ def broadcast_render(**opts)
450
+ hotsock_broadcast_render(**opts)
451
+ end
452
+
453
+ # Async instance methods
454
+ def broadcast_replace_later_to(*streamables, **opts)
455
+ hotsock_broadcast_replace_later_to(*streamables, **opts)
456
+ end
457
+
458
+ def broadcast_replace_later(**opts)
459
+ hotsock_broadcast_replace_later(**opts)
460
+ end
461
+
462
+ def broadcast_update_later_to(*streamables, **opts)
463
+ hotsock_broadcast_update_later_to(*streamables, **opts)
464
+ end
465
+
466
+ def broadcast_update_later(**opts)
467
+ hotsock_broadcast_update_later(**opts)
468
+ end
469
+
470
+ def broadcast_append_later_to(*streamables, **opts)
471
+ hotsock_broadcast_append_later_to(*streamables, **opts)
472
+ end
473
+
474
+ def broadcast_append_later(**opts)
475
+ hotsock_broadcast_append_later(**opts)
476
+ end
477
+
478
+ def broadcast_prepend_later_to(*streamables, **opts)
479
+ hotsock_broadcast_prepend_later_to(*streamables, **opts)
480
+ end
481
+
482
+ def broadcast_prepend_later(**opts)
483
+ hotsock_broadcast_prepend_later(**opts)
484
+ end
485
+
486
+ def broadcast_refresh_later_to(*streamables, **opts)
487
+ hotsock_broadcast_refresh_later_to(*streamables, **opts)
488
+ end
489
+
490
+ def broadcast_refresh_later
491
+ hotsock_broadcast_refresh_later
492
+ end
493
+
494
+ def broadcast_action_later_to(*streamables, **opts)
495
+ hotsock_broadcast_action_later_to(*streamables, **opts)
496
+ end
497
+
498
+ def broadcast_action_later(**opts)
499
+ hotsock_broadcast_action_later(**opts)
500
+ end
501
+
502
+ def broadcast_render_later_to(*streamables, **opts)
503
+ hotsock_broadcast_render_later_to(*streamables, **opts)
504
+ end
505
+
506
+ def broadcast_render_later(**opts)
507
+ hotsock_broadcast_render_later(**opts)
508
+ end
509
+
510
+ private
511
+
512
+ def broadcast_target_default
513
+ hotsock_broadcast_target_default
514
+ end
515
+ end
516
+ end
517
+ end
518
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotsock
4
+ module Turbo
5
+ class Config
6
+ attr_accessor :parent_controller, :connect_token_path, :wss_url, :log_level, :override_turbo_broadcastable
7
+
8
+ def initialize
9
+ @parent_controller = "ApplicationController"
10
+ @connect_token_path = nil
11
+ @wss_url = nil
12
+ @log_level = "warn"
13
+ @override_turbo_broadcastable = false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotsock
4
+ module Turbo
5
+ class Engine < Rails::Engine
6
+ isolate_namespace Hotsock::Turbo
7
+
8
+ initializer "hotsock.turbo.helpers" do
9
+ ActiveSupport.on_load(:action_view) do
10
+ include Hotsock::Turbo::StreamsHelper
11
+ end
12
+ end
13
+
14
+ initializer "hotsock.turbo.assets.precompile" do |app|
15
+ if app.config.respond_to?(:assets)
16
+ app.config.assets.precompile += %w[hotsock-turbo.js]
17
+ end
18
+ end
19
+
20
+ initializer "hotsock.turbo.broadcastable" do
21
+ ActiveSupport.on_load(:active_record) do
22
+ include Hotsock::Turbo::Broadcastable
23
+
24
+ if Hotsock::Turbo.config.override_turbo_broadcastable
25
+ # Use prepend so our methods take precedence over Turbo::Broadcastable
26
+ # (which may be included later by turbo-rails)
27
+ prepend Hotsock::Turbo::Broadcastable::TurboBroadcastableOverride
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end