turbo-train 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +24 -9
- data/app/helpers/turbo/train/streams_helper.rb +1 -1
- data/app/jobs/turbo/train/action_broadcast_job.rb +16 -0
- data/app/jobs/turbo/train/broadcast_job.rb +11 -0
- data/app/models/concerns/turbo/train/broadcastable.rb +300 -0
- data/lib/turbo/train/broadcasts.rb +91 -0
- data/lib/turbo/train/config.rb +4 -0
- data/lib/turbo/train/server.rb +35 -0
- data/lib/turbo/train/test_helper.rb +41 -0
- data/lib/turbo/train/test_server.rb +33 -0
- data/lib/turbo/train/train.rb +12 -82
- data/lib/turbo/train/version.rb +1 -1
- data/lib/turbo/train.rb +4 -0
- metadata +13 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 848a9382896a69a3cb1d57a569c0fd2fc3908a3599dc5338cd26acf732ee9992
|
4
|
+
data.tar.gz: 1672959f0bbae5254a6f97904db3322607080ffa7da940f1553732b883d96fcb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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="
|
6
|
-
src="
|
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(
|
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
|
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,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
|
data/lib/turbo/train/config.rb
CHANGED
@@ -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
|
data/lib/turbo/train/train.rb
CHANGED
@@ -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
|
25
|
-
|
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
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
59
|
-
|
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
|
data/lib/turbo/train/version.rb
CHANGED
data/lib/turbo/train.rb
CHANGED
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.
|
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:
|
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.
|