turbo-train 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 033a0817bbb8ada60d3658f9fbc5edfc34cf96a4da5f4db2d968ca07efa5b3f4
4
- data.tar.gz: c232fd0c8f51fe97e473db23ef9180e629f4aca46bdddbb085f33d561701f3cc
3
+ metadata.gz: 848a9382896a69a3cb1d57a569c0fd2fc3908a3599dc5338cd26acf732ee9992
4
+ data.tar.gz: 1672959f0bbae5254a6f97904db3322607080ffa7da940f1553732b883d96fcb
5
5
  SHA512:
6
- metadata.gz: 770537fbb431a7a735cb49a7af09b3cb9d0470e5c48189068650456e13efcbd5dc3b486e68036f30cb5cfcbb1b8ea1d58d84e01be25b8dcb3180e27c89907dba
7
- data.tar.gz: c41705c378a6425c581f16d925b3e1ee649d745f64899965c7cbcf0942fc6ba464ed0437ac539b628246f8507844514eb42b054875dbe5dde57489712e4789ae
6
+ metadata.gz: 245d52890779c61021ac9b5e3877b164e93535dae1211e351dc883c1c024bd429b1ed4f48d09401d204de16cdf971ccaba1b9daea403c43ff38e47dc874039f9
7
+ data.tar.gz: a66cebab47ba27702446128492dfceac83bc6ae2382bf072547681de7989f9a98c81e2e20482d3d9fa9509c4c5d01a421b443b7349afc734b326bdfe5099327a
data/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  # Turbo::Train
4
4
 
5
- <img align="right" width="160" title="Turbo::Train logo"
6
- src="./logo.svg">
5
+ <img align="right" width="220" title="Turbo::Train logo"
6
+ src="https://user-images.githubusercontent.com/3010927/210603861-4b265489-a4a7-4d2a-bceb-40ceccebcd96.jpg">
7
+
7
8
 
8
9
  Real-time page updates for your Rails app over SSE with [Mercure](https://mercure.rocks) and [Hotwire Turbo](https://turbo.hotwired.dev/handbook/streams#integration-with-server-side-frameworks).
9
10
 
@@ -57,12 +58,11 @@ Now you are ready to run 🚀
57
58
  caddy run
58
59
  ```
59
60
 
60
-
61
61
  ## Usage
62
62
 
63
63
  If you are familiar with broadcasting from ActionCable, usage would be extremely familiar:
64
64
 
65
- ```
65
+ ```erb
66
66
  <%# app/views/chat_messages/index.html.erb %>
67
67
  <%= turbo_train_from "chat_messages" %>
68
68
 
@@ -71,15 +71,27 @@ If you are familiar with broadcasting from ActionCable, usage would be extremely
71
71
 
72
72
  And then you can send portions of HTML from your Rails backend to deliver live to all currently open browsers:
73
73
 
74
- ```
75
- Turbo::Train.broadcast_action_to('chat_messages', action: :append, target:'append_new_messages_here', html: '<span>Test!</span>')
74
+ ```ruby
75
+ Turbo::Train.broadcast_action_to(
76
+ 'chat_messages',
77
+ action: :append,
78
+ target:'append_new_messages_here',
79
+ html: '<span>Test!</span>'
80
+ )
76
81
  ```
77
82
 
78
83
  or in real world you'd probably have something like
79
84
 
80
- ```
85
+ ```ruby
81
86
  # app/models/chat_message.rb
82
- after_create_commit { Turbo::Train.broadcast_action_to('chat_messages', action: :append, target:'append_new_messages_here', partial: 'somepath/message') }
87
+ after_create_commit do
88
+ Turbo::Train.broadcast_action_to(
89
+ 'chat_messages',
90
+ action: :append,
91
+ target: 'append_new_messages_here',
92
+ partial: 'somepath/message'
93
+ )
94
+ end
83
95
  ```
84
96
 
85
97
  You have the same options as original Rails Turbo helpers: rendering partials, pure html, [same actions](https://turbo.hotwired.dev/reference/streams).
@@ -88,7 +100,7 @@ You have the same options as original Rails Turbo helpers: rendering partials, p
88
100
 
89
101
  To specify different Mercure server settings, please adjust the generated `config/initializers/turbo_train.rb` file:
90
102
 
91
- ```
103
+ ```ruby
92
104
  Turbo::Train.configure do |config|
93
105
  config.mercure_domain = ...
94
106
  config.publisher_key = ...
@@ -103,5 +115,8 @@ By default, these are set to `localhost`/`test`/`testing` to match the configura
103
115
 
104
116
  ***
105
117
 
118
+ <img width="80" title="Turbo::Train logo"
119
+ src="https://user-images.githubusercontent.com/3010927/210604381-4b715322-55f8-4db8-8bb8-660be734704d.jpg">
120
+
106
121
  ## License
107
122
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,7 +2,7 @@ module Turbo::Train::StreamsHelper
2
2
  def turbo_train_from(*streamables, **attributes)
3
3
  attributes[:name] = Turbo::Train.signed_stream_name(streamables)
4
4
  attributes[:session] = Turbo::Train.encode({ platform: "web" })
5
- attributes[:href] = Turbo::Train.url
5
+ attributes[:href] = Turbo::Train.configuration.url
6
6
  tag.turbo_train_stream_source(**attributes)
7
7
  end
8
8
  end
@@ -0,0 +1,16 @@
1
+ module Turbo
2
+ module Train
3
+ class ActionBroadcastJob < ActiveJob::Base
4
+ discard_on ActiveJob::DeserializationError
5
+
6
+ def perform(stream, action:, target:, **rendering)
7
+ Turbo::Train.broadcast_action_to(
8
+ stream,
9
+ action: action,
10
+ target: target,
11
+ **rendering
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Turbo
2
+ module Train
3
+ class BroadcastJob < ActiveJob::Base
4
+ discard_on ActiveJob::DeserializationError
5
+
6
+ def perform(stream, **rendering)
7
+ Turbo::Train.broadcast_render_to(stream, **rendering)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,300 @@
1
+ # Based on: Turbo::Broadcastable
2
+ module Turbo::Train::Broadcastable
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
7
+ # <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
8
+ # the model's plural name. The insertion can also be made to be a prepend by overwriting <tt>inserts_by</tt> and
9
+ # the target dom id overwritten by passing <tt>target</tt>. Examples:
10
+ #
11
+ # class Message < ApplicationRecord
12
+ # belongs_to :board
13
+ # broadcasts_to :board
14
+ # end
15
+ #
16
+ # class Message < ApplicationRecord
17
+ # belongs_to :board
18
+ # train_broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages"
19
+ # end
20
+ #
21
+ # class Message < ApplicationRecord
22
+ # belongs_to :board
23
+ # train_broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
24
+ # end
25
+ def train_broadcasts_to(stream, inserts_by: :append, target: train_broadcast_target_default, **rendering)
26
+ after_create_commit -> { train_broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering) }
27
+ after_update_commit -> { train_broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
28
+ after_destroy_commit -> { train_broadcast_remove_to(stream.try(:call, self) || send(stream)) }
29
+ end
30
+
31
+ # Same as <tt>#train_broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
32
+ # the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
33
+ def train_broadcasts(stream = model_name.plural, inserts_by: :append, target: train_broadcast_target_default, **rendering)
34
+ after_create_commit -> { train_broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering) }
35
+ after_update_commit -> { train_broadcast_replace_later(**rendering) }
36
+ after_destroy_commit -> { train_broadcast_remove }
37
+ end
38
+
39
+ # All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
40
+ def train_broadcast_target_default
41
+ model_name.plural
42
+ end
43
+ end
44
+
45
+ # Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
46
+ # Example:
47
+ #
48
+ # # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
49
+ # clearance.train_broadcast_remove_to examiner.identity, :clearances
50
+ def train_broadcast_remove_to(*streamables, target: self)
51
+ Turbo::Train.broadcast_remove_to(*streamables, target: target)
52
+ end
53
+
54
+ # Same as <tt>#train_broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
55
+ def train_broadcast_remove
56
+ train_broadcast_remove_to self
57
+ end
58
+
59
+ # Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed
60
+ # <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
61
+ #
62
+ # # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
63
+ # # to the stream named "identity:2:clearances"
64
+ # clearance.train_broadcast_replace_to examiner.identity, :clearances
65
+ #
66
+ # # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
67
+ # # to the stream named "identity:2:clearances"
68
+ # clearance.train_broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
69
+ def train_broadcast_replace_to(*streamables, **rendering)
70
+ Turbo::Train.broadcast_replace_to(*streamables, target: self, **train_broadcast_rendering_with_defaults(rendering))
71
+ end
72
+
73
+ # Same as <tt>#train_broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
74
+ def train_broadcast_replace(**rendering)
75
+ train_broadcast_replace_to self, **rendering
76
+ end
77
+
78
+ # Update this broadcastable model in the dom for subscribers of the stream name identified by the passed
79
+ # <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
80
+ #
81
+ # # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
82
+ # # to the stream named "identity:2:clearances"
83
+ # clearance.train_broadcast_update_to examiner.identity, :clearances
84
+ #
85
+ # # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
86
+ # # to the stream named "identity:2:clearances"
87
+ # clearance.train_broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
88
+ def train_broadcast_update_to(*streamables, **rendering)
89
+ Turbo::Train.broadcast_update_to(*streamables, target: self, **train_broadcast_rendering_with_defaults(rendering))
90
+ end
91
+
92
+ # Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
93
+ def train_broadcast_update(**rendering)
94
+ train_broadcast_update_to self, **rendering
95
+ end
96
+
97
+ # Insert a rendering of this broadcastable model before the target identified by it's dom id passed as <tt>target</tt>
98
+ # for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
99
+ # appending named arguments to the call. Examples:
100
+ #
101
+ # # Sends <turbo-stream action="before" target="clearance_5"><template><div id="clearance_4">My Clearance</div></template></turbo-stream>
102
+ # # to the stream named "identity:2:clearances"
103
+ # clearance.train_broadcast_before_to examiner.identity, :clearances, target: "clearance_5"
104
+ #
105
+ # # Sends <turbo-stream action="before" target="clearance_5"><template><div id="clearance_4">Other partial</div></template></turbo-stream>
106
+ # # to the stream named "identity:2:clearances"
107
+ # clearance.train_broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
108
+ # partial: "clearances/other_partial", locals: { a: 1 }
109
+ def train_broadcast_before_to(*streamables, target:, **rendering)
110
+ Turbo::Train.broadcast_before_to(*streamables, target: target, **train_broadcast_rendering_with_defaults(rendering))
111
+ end
112
+
113
+ # Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
114
+ # for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
115
+ # appending named arguments to the call. Examples:
116
+ #
117
+ # # Sends <turbo-stream action="after" target="clearance_5"><template><div id="clearance_6">My Clearance</div></template></turbo-stream>
118
+ # # to the stream named "identity:2:clearances"
119
+ # clearance.train_broadcast_after_to examiner.identity, :clearances, target: "clearance_5"
120
+ #
121
+ # # Sends <turbo-stream action="after" target="clearance_5"><template><div id="clearance_6">Other partial</div></template></turbo-stream>
122
+ # # to the stream named "identity:2:clearances"
123
+ # clearance.train_broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
124
+ # partial: "clearances/other_partial", locals: { a: 1 }
125
+ def train_broadcast_after_to(*streamables, target:, **rendering)
126
+ Turbo::Train.broadcast_after_to(*streamables, target: target, **train_broadcast_rendering_with_defaults(rendering))
127
+ end
128
+
129
+ # Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
130
+ # for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
131
+ # appending named arguments to the call. Examples:
132
+ #
133
+ # # Sends <turbo-stream action="append" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
134
+ # # to the stream named "identity:2:clearances"
135
+ # clearance.train_broadcast_append_to examiner.identity, :clearances, target: "clearances"
136
+ #
137
+ # # Sends <turbo-stream action="append" target="clearances"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
138
+ # # to the stream named "identity:2:clearances"
139
+ # clearance.train_broadcast_append_to examiner.identity, :clearances, target: "clearances",
140
+ # partial: "clearances/other_partial", locals: { a: 1 }
141
+ def train_broadcast_append_to(*streamables, target: train_broadcast_target_default, **rendering)
142
+ Turbo::Train.broadcast_append_to(*streamables, target: target, **train_broadcast_rendering_with_defaults(rendering))
143
+ end
144
+
145
+ # Same as <tt>#train_broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
146
+ def train_broadcast_append(target: train_broadcast_target_default, **rendering)
147
+ train_broadcast_append_to self, target: target, **rendering
148
+ end
149
+
150
+ # Prepend a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
151
+ # for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
152
+ # appending named arguments to the call. Examples:
153
+ #
154
+ # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
155
+ # # to the stream named "identity:2:clearances"
156
+ # clearance.train_broadcast_prepend_to examiner.identity, :clearances, target: "clearances"
157
+ #
158
+ # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
159
+ # # to the stream named "identity:2:clearances"
160
+ # clearance.train_broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
161
+ # partial: "clearances/other_partial", locals: { a: 1 }
162
+ def train_broadcast_prepend_to(*streamables, target: train_broadcast_target_default, **rendering)
163
+ Turbo::Train.broadcast_prepend_to(*streamables, target: target, **train_broadcast_rendering_with_defaults(rendering))
164
+ end
165
+
166
+ # Same as <tt>#train_broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
167
+ def train_broadcast_prepend(target: train_broadcast_target_default, **rendering)
168
+ train_broadcast_prepend_to self, target: target, **rendering
169
+ end
170
+
171
+ # Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
172
+ #
173
+ # # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
174
+ # # to the stream named "identity:2:clearances"
175
+ # clearance.train_broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
176
+ def train_broadcast_action_to(*streamables, action:, target: train_broadcast_target_default, **rendering)
177
+ Turbo::Train.broadcast_action_to(*streamables, action: action, target: target, **train_broadcast_rendering_with_defaults(rendering))
178
+ end
179
+
180
+ # Same as <tt>#train_broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
181
+ def train_broadcast_action(action, target: train_broadcast_target_default, **rendering)
182
+ train_broadcast_action_to self, action: action, target: target, **rendering
183
+ end
184
+
185
+
186
+ # Same as <tt>train_broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
187
+ def train_broadcast_replace_later_to(*streamables, **rendering)
188
+ Turbo::Train.broadcast_replace_later_to(*streamables, target: self, **train_broadcast_rendering_with_defaults(rendering))
189
+ end
190
+
191
+ # Same as <tt>#train_broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
192
+ def train_broadcast_replace_later(**rendering)
193
+ train_broadcast_replace_later_to self, **rendering
194
+ end
195
+
196
+ # Same as <tt>train_broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
197
+ def train_broadcast_update_later_to(*streamables, **rendering)
198
+ Turbo::Train.broadcast_update_later_to(*streamables, target: self, **train_broadcast_rendering_with_defaults(rendering))
199
+ end
200
+
201
+ # Same as <tt>#train_broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
202
+ def train_broadcast_update_later(**rendering)
203
+ train_broadcast_update_later_to self, **rendering
204
+ end
205
+
206
+ # Same as <tt>train_broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
207
+ def train_broadcast_append_later_to(*streamables, target: train_broadcast_target_default, **rendering)
208
+ Turbo::Train.broadcast_append_later_to(*streamables, target: target, **train_broadcast_rendering_with_defaults(rendering))
209
+ end
210
+
211
+ # Same as <tt>#train_broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
212
+ def train_broadcast_append_later(target: train_broadcast_target_default, **rendering)
213
+ train_broadcast_append_later_to self, target: target, **rendering
214
+ end
215
+
216
+ # Same as <tt>train_broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
217
+ def train_broadcast_prepend_later_to(*streamables, target: train_broadcast_target_default, **rendering)
218
+ Turbo::Train.broadcast_prepend_later_to(*streamables, target: target, **train_broadcast_rendering_with_defaults(rendering))
219
+ end
220
+
221
+ # Same as <tt>#train_broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
222
+ def train_broadcast_prepend_later(target: train_broadcast_target_default, **rendering)
223
+ train_broadcast_prepend_later_to self, target: target, **rendering
224
+ end
225
+
226
+ # Same as <tt>train_broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
227
+ def train_broadcast_action_later_to(*streamables, action:, target: train_broadcast_target_default, **rendering)
228
+ Turbo::Train.broadcast_action_later_to(*streamables, action: action, target: target, **train_broadcast_rendering_with_defaults(rendering))
229
+ end
230
+
231
+ # Same as <tt>#train_broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
232
+ def train_broadcast_action_later(action:, target: train_broadcast_target_default, **rendering)
233
+ train_broadcast_action_later_to self, action: action, target: target, **rendering
234
+ end
235
+
236
+ # Render a turbo stream template with this broadcastable model passed as the local variable. Example:
237
+ #
238
+ # # Template: entries/_entry.turbo_stream.erb
239
+ # <%= turbo_stream.remove entry %>
240
+ #
241
+ # <%= turbo_stream.append "entries", entry if entry.active? %>
242
+ #
243
+ # Sends:
244
+ #
245
+ # <turbo-stream action="remove" target="entry_5"></turbo-stream>
246
+ # <turbo-stream action="append" target="entries"><template><div id="entry_5">My Entry</div></template></turbo-stream>
247
+ #
248
+ # ...to the stream named "entry:5".
249
+ #
250
+ # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
251
+ # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
252
+ # be using `train_broadcast_render_later`, unless you specifically know why synchronous rendering is needed.
253
+ def train_broadcast_render(**rendering)
254
+ train_broadcast_render_to self, **rendering
255
+ end
256
+
257
+ # Same as <tt>train_broadcast_render</tt> but run with the added option of naming the stream using the passed
258
+ # <tt>streamables</tt>.
259
+ #
260
+ # Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
261
+ # desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
262
+ # be using `train_broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
263
+ def train_broadcast_render_to(*streamables, **rendering)
264
+ Turbo::Train.broadcast_render_to(*streamables, **train_broadcast_rendering_with_defaults(rendering))
265
+ end
266
+
267
+ # Same as <tt>train_broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
268
+ def train_broadcast_render_later(**rendering)
269
+ train_broadcast_render_later_to self, **rendering
270
+ end
271
+
272
+ # Same as <tt>train_broadcast_render_later</tt> but run with the added option of naming the stream using the passed
273
+ # <tt>streamables</tt>.
274
+ def train_broadcast_render_later_to(*streamables, **rendering)
275
+ Turbo::Train.broadcast_render_later_to(*streamables, **train_broadcast_rendering_with_defaults(rendering))
276
+ end
277
+
278
+
279
+ private
280
+ def train_broadcast_target_default
281
+ self.class.train_broadcast_target_default
282
+ end
283
+
284
+ def train_broadcast_rendering_with_defaults(options)
285
+ options.tap do |o|
286
+ # Add the current instance into the locals with the element name (which is the un-namespaced name)
287
+ # as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
288
+ o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
289
+
290
+ if o[:html] || o[:partial]
291
+ return o
292
+ elsif o[:template]
293
+ o[:layout] = false
294
+ else
295
+ # if none of these options are passed in, it will set a partial from #to_partial_path
296
+ o[:partial] ||= to_partial_path
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,91 @@
1
+ module Turbo::Train::Broadcasts
2
+ def broadcast(streamables, content:)
3
+ topics = if streamables.is_a?(Array)
4
+ streamables.map { |s| signed_stream_name(s) }
5
+ else
6
+ [signed_stream_name(streamables)]
7
+ end
8
+
9
+ data = {
10
+ topic: topics,
11
+ data: content
12
+ }
13
+
14
+ Turbo::Train.server.publish(topics: topics, data: data)
15
+ end
16
+
17
+ def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
18
+ broadcast(streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
19
+ rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil)
20
+ ))
21
+ end
22
+
23
+ def broadcast_render_to(*streamables, **rendering)
24
+ broadcast(*streamables, content: render_format(:turbo_stream, **rendering))
25
+ end
26
+
27
+ def broadcast_remove_to(*streamables, **opts)
28
+ broadcast_action_to(*streamables, action: :remove, **opts)
29
+ end
30
+
31
+ def broadcast_replace_to(*streamables, **opts)
32
+ broadcast_action_to(*streamables, action: :replace, **opts)
33
+ end
34
+
35
+ def broadcast_update_to(*streamables, **opts)
36
+ broadcast_action_to(*streamables, action: :update, **opts)
37
+ end
38
+
39
+ def broadcast_before_to(*streamables, **opts)
40
+ broadcast_action_to(*streamables, action: :before, **opts)
41
+ end
42
+
43
+ def broadcast_after_to(*streamables, **opts)
44
+ broadcast_action_to(*streamables, action: :after, **opts)
45
+ end
46
+
47
+ def broadcast_append_to(*streamables, **opts)
48
+ broadcast_action_to(*streamables, action: :append, **opts)
49
+ end
50
+
51
+ def broadcast_prepend_to(*streamables, **opts)
52
+ broadcast_action_to(*streamables, action: :prepend, **opts)
53
+ end
54
+
55
+ # later
56
+ def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering)
57
+ Turbo::Train::ActionBroadcastJob.perform_later streamables, action: action, target: target, targets: targets, **rendering
58
+ end
59
+
60
+ def broadcast_replace_later_to(*streamables, **opts)
61
+ broadcast_action_later_to(*streamables, action: :replace, **opts)
62
+ end
63
+
64
+ def broadcast_remove_later_to(*streamables, **opts)
65
+ broadcast_action_later_to(*streamables, action: :remove, **opts)
66
+ end
67
+
68
+ def broadcast_update_later_to(*streamables, **opts)
69
+ broadcast_action_later_to(*streamables, action: :update, **opts)
70
+ end
71
+
72
+ def broadcast_before_later_to(*streamables, **opts)
73
+ broadcast_action_later_to(*streamables, action: :before, **opts)
74
+ end
75
+
76
+ def broadcast_after_later_to(*streamables, **opts)
77
+ broadcast_action_later_to(*streamables, action: :after, **opts)
78
+ end
79
+
80
+ def broadcast_append_later_to(*streamables, **opts)
81
+ broadcast_action_later_to(*streamables, action: :append, **opts)
82
+ end
83
+
84
+ def broadcast_prepend_later_to(*streamables, **opts)
85
+ broadcast_action_later_to(*streamables, action: :prepend, **opts)
86
+ end
87
+
88
+ def broadcast_render_later_to(*streamables, **rendering)
89
+ Turbo::Train::BroadcastJob.perform_later streamables, **rendering
90
+ end
91
+ end
@@ -9,6 +9,10 @@ module Turbo
9
9
  @subscriber_key = 'testing'
10
10
  @skip_ssl_verification = Rails.env.development? || Rails.env.test?
11
11
  end
12
+
13
+ def url
14
+ "https://#{mercure_domain}/.well-known"
15
+ end
12
16
  end
13
17
 
14
18
  class << self
@@ -0,0 +1,35 @@
1
+ module Turbo
2
+ module Train
3
+ class Server
4
+ attr_reader :configuration
5
+
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ end
9
+
10
+ def publish(topics:, data:)
11
+ payload = { mercure: { publish: topics } }
12
+ token = JWT.encode payload, configuration.publisher_key, ALGORITHM
13
+
14
+ uri = URI("#{configuration.url}/mercure")
15
+
16
+ req = Net::HTTP::Post.new(uri)
17
+ req['Content-Type'] = 'application/x-www-form-urlencoded'
18
+ req['Authorization'] = "Bearer #{token}"
19
+
20
+ req.body = URI.encode_www_form(data)
21
+ opts = {
22
+ use_ssl: uri.scheme == 'https'
23
+ }
24
+
25
+ if configuration.skip_ssl_verification
26
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
27
+ end
28
+
29
+ Net::HTTP.start(uri.host, uri.port, opts) do |http|
30
+ http.request(req)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # Based on: ActionCable::TestHelper
2
+ module Turbo
3
+ module Train
4
+ module TestHelper
5
+ def before_setup
6
+ Turbo::Train.instance_variable_set(:@server, Turbo::Train::TestServer.new(Turbo::Train.configuration))
7
+ super
8
+ end
9
+
10
+ def after_teardown
11
+ super
12
+ Turbo::Train.instance_variable_set(:@server, Turbo::Train::Server.new(Turbo::Train.configuration))
13
+ end
14
+
15
+ def assert_broadcast_on(stream, data, &block)
16
+ signed_stream_name = Turbo::Train.signed_stream_name(stream)
17
+
18
+ new_messages = Turbo::Train.server.broadcasts(signed_stream_name)
19
+ if block_given?
20
+ old_messages = new_messages
21
+ Turbo::Train.server.clear_messages(signed_stream_name)
22
+
23
+ assert_nothing_raised(&block)
24
+ new_messages = Turbo::Train.server.broadcasts(signed_stream_name)
25
+ Turbo::Train.server.clear_messages(signed_stream_name)
26
+
27
+ # Restore all sent messages
28
+ (old_messages + new_messages).each { |m| Turbo::Train.server.broadcasts(signed_stream_name) << m }
29
+ end
30
+
31
+ message = new_messages.find { |msg| msg == data }
32
+ if message.nil?
33
+ puts "signed_stream_name => #{signed_stream_name}"
34
+ puts "channels_data: #{Turbo::Train.server.channels_data.inspect}}"
35
+ end
36
+
37
+ assert message, "No messages sent with #{data} to #{Turbo::Train.stream_name_from(stream)}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ module Turbo
2
+ module Train
3
+ class TestServer < Server
4
+ attr_reader :configuration, :channels_data
5
+
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ @channels_data = {}
9
+ end
10
+
11
+ def publish(topics:, data:)
12
+ Array(topics).each do |topic|
13
+ @channels_data[topic] ||= []
14
+ @channels_data[topic] << data[:data]
15
+ end
16
+
17
+ super
18
+ end
19
+
20
+ def broadcasts(channel)
21
+ channels_data[channel] ||= []
22
+ end
23
+
24
+ def clear_messages(channel)
25
+ channels_data[channel] = []
26
+ end
27
+
28
+ def clear
29
+ @channels_data = []
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,14 +4,11 @@ require 'net/http'
4
4
  module Turbo
5
5
  module Train
6
6
  extend Turbo::Streams::ActionHelper
7
+ extend Broadcasts
7
8
 
8
9
  ALGORITHM = "HS256"
9
10
 
10
11
  class << self
11
- def url
12
- "https://#{configuration.mercure_domain}/.well-known"
13
- end
14
-
15
12
  def encode(payload)
16
13
  structured_payload = { mercure: { payload: payload } }
17
14
  JWT.encode structured_payload, configuration.subscriber_key, ALGORITHM
@@ -21,94 +18,27 @@ module Turbo
21
18
  Turbo.signed_stream_verifier.generate stream_name_from(streamables)
22
19
  end
23
20
 
24
- def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
25
- broadcast(streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
26
- rendering.delete(:content) || rendering.delete(:html) || (rendering.any? ? render_format(:html, **rendering) : nil)
27
- ))
28
- end
29
-
30
- def broadcast_render_to(*streamables, **rendering)
31
- broadcast(*streamables, content: render_format(:turbo_stream, **rendering))
32
- end
33
-
34
- def broadcast_remove_to(*streamables, **opts)
35
- broadcast_action_to(*streamables, action: :remove, **opts)
36
- end
37
-
38
- def broadcast_replace_to(*streamables, **opts)
39
- broadcast_action_to(*streamables, action: :replace, **opts)
40
- end
41
-
42
- def broadcast_update_to(*streamables, **opts)
43
- broadcast_action_to(*streamables, action: :update, **opts)
21
+ def server
22
+ @server ||= Server.new(configuration)
44
23
  end
45
24
 
46
- def broadcast_before_to(*streamables, **opts)
47
- broadcast_action_to(*streamables, action: :before, **opts)
48
- end
49
-
50
- def broadcast_after_to(*streamables, **opts)
51
- broadcast_action_to(*streamables, action: :after, **opts)
52
- end
53
-
54
- def broadcast_append_to(*streamables, **opts)
55
- broadcast_action_to(*streamables, action: :append, **opts)
25
+ def stream_name_from(streamables)
26
+ if streamables.is_a?(Array)
27
+ streamables.map { |streamable| stream_name_from(streamable) }.join(":")
28
+ else
29
+ streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
30
+ end
56
31
  end
57
32
 
58
- def broadcast_prepend_to(*streamables, **opts)
59
- broadcast_action_to(*streamables, action: :prepend, **opts)
33
+ def url
34
+ configuration.url
60
35
  end
61
36
 
62
37
  private
63
38
 
64
- def domain
65
- Rails.configuration.turbo_train.mercure_domain
66
- end
67
-
68
39
  def render_format(format, **rendering)
69
40
  ApplicationController.render(formats: [ format ], **rendering)
70
41
  end
71
-
72
- def broadcast(streamables, content:)
73
- topics = if streamables.is_a?(Array)
74
- streamables.map { |s| signed_stream_name(s) }
75
- else
76
- [signed_stream_name(streamables)]
77
- end
78
-
79
- data = {
80
- topic: topics,
81
- data: content
82
- }
83
- payload = { mercure: { publish: topics } }
84
- token = JWT.encode payload, configuration.publisher_key, ALGORITHM
85
-
86
- uri = URI("#{url}/mercure")
87
-
88
- req = Net::HTTP::Post.new(uri)
89
- req['Content-Type'] = 'application/x-www-form-urlencoded'
90
- req['Authorization'] = "Bearer #{token}"
91
- req.body = URI.encode_www_form(data)
92
- opts = {
93
- use_ssl: uri.scheme == 'https'
94
- }
95
-
96
- if configuration.skip_ssl_verification
97
- opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
98
- end
99
-
100
- Net::HTTP.start(uri.host, uri.port, opts) do |http|
101
- http.request(req)
102
- end
103
- end
104
-
105
- def stream_name_from(streamables)
106
- if streamables.is_a?(Array)
107
- streamables.map { |streamable| stream_name_from(streamable) }.join(":")
108
- else
109
- streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
110
- end
111
- end
112
42
  end
113
43
  end
114
- end
44
+ end
@@ -1,5 +1,5 @@
1
1
  module Turbo
2
2
  module Train
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/turbo/train.rb CHANGED
@@ -1,3 +1,7 @@
1
1
  require "turbo/train/version"
2
2
  require "turbo/train/config"
3
+ require 'turbo/train/broadcasts'
4
+ require 'turbo/train/server'
5
+ require 'turbo/train/test_server'
6
+ require 'turbo/train/test_helper'
3
7
  require "turbo/train/engine"
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-train
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Savrov
8
8
  - Dima Bondarenko
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-12-22 00:00:00.000000000 Z
12
+ date: 2023-05-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -53,7 +53,7 @@ dependencies:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
- description:
56
+ description:
57
57
  email:
58
58
  - nick@uscreen.tv
59
59
  - dmitry@uscreen.tv
@@ -70,8 +70,11 @@ files:
70
70
  - app/assets/javascripts/turbo-train.min.js
71
71
  - app/controllers/turbo/train/test/application_controller.rb
72
72
  - app/helpers/turbo/train/streams_helper.rb
73
+ - app/jobs/turbo/train/action_broadcast_job.rb
74
+ - app/jobs/turbo/train/broadcast_job.rb
73
75
  - app/jobs/turbo/train/test/application_job.rb
74
76
  - app/mailers/turbo/train/test/application_mailer.rb
77
+ - app/models/concerns/turbo/train/broadcastable.rb
75
78
  - app/models/turbo/train/test/application_record.rb
76
79
  - app/views/layouts/turbo/train/test/application.html.erb
77
80
  - config/routes.rb
@@ -81,8 +84,12 @@ files:
81
84
  - lib/install/install_node.rb
82
85
  - lib/tasks/install_tasks.rake
83
86
  - lib/turbo/train.rb
87
+ - lib/turbo/train/broadcasts.rb
84
88
  - lib/turbo/train/config.rb
85
89
  - lib/turbo/train/engine.rb
90
+ - lib/turbo/train/server.rb
91
+ - lib/turbo/train/test_helper.rb
92
+ - lib/turbo/train/test_server.rb
86
93
  - lib/turbo/train/train.rb
87
94
  - lib/turbo/train/version.rb
88
95
  homepage: https://github.com/Uscreen-video/turbo-train
@@ -91,7 +98,7 @@ licenses:
91
98
  metadata:
92
99
  homepage_uri: https://github.com/Uscreen-video/turbo-train
93
100
  source_code_uri: https://github.com/goodsign/turbo-train-test
94
- post_install_message:
101
+ post_install_message:
95
102
  rdoc_options: []
96
103
  require_paths:
97
104
  - lib
@@ -107,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
114
  version: '0'
108
115
  requirements: []
109
116
  rubygems_version: 3.1.6
110
- signing_key:
117
+ signing_key:
111
118
  specification_version: 4
112
119
  summary: Rails Turbo Stream broadcasting over SSE instead of WebSockets. Uses Mercure
113
120
  server.