turbo-train 0.1.1 → 0.3.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: d8f44643f00dc3a0c248fd990ffc95631a63bf9df53768ecb8e740a5dda4bc45
4
+ data.tar.gz: 92d303ac07e6402c9dc9ce4f71dcb9c56345c78980184c59692b7c7508daaf4d
5
5
  SHA512:
6
- metadata.gz: 770537fbb431a7a735cb49a7af09b3cb9d0470e5c48189068650456e13efcbd5dc3b486e68036f30cb5cfcbb1b8ea1d58d84e01be25b8dcb3180e27c89907dba
7
- data.tar.gz: c41705c378a6425c581f16d925b3e1ee649d745f64899965c7cbcf0942fc6ba464ed0437ac539b628246f8507844514eb42b054875dbe5dde57489712e4789ae
6
+ metadata.gz: 229de6bb9a249571e03daddb1b1288aa9138ddee9a1711538961bce90a6bc02e92ba2b83dd4f0062a78c75bb27cf92202126a40034303c10ae127bb9786eb34d
7
+ data.tar.gz: f2cd4ebef097ffc2d66fd6ec081537ae57b54c7318c132ed0cf508f26dfe03cdc2b13d3386839c209269062c344f9eb127ff8728593a7b82807db8407cb70dd2
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).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uscreentv/turbo-train",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "",
5
5
  "main": "turbo-train.js",
6
6
  "type": "module",
@@ -2,7 +2,7 @@ import { Turbo } from "@hotwired/turbo-rails"
2
2
 
3
3
  export default class TurboTrain extends HTMLElement {
4
4
  static get observedAttributes() {
5
- return [ 'href', 'session', 'name' ];
5
+ return [ 'href' ];
6
6
  }
7
7
 
8
8
  constructor() {
@@ -10,7 +10,7 @@ export default class TurboTrain extends HTMLElement {
10
10
  }
11
11
 
12
12
  connectedCallback() {
13
- this.eventSource = new EventSource(`${this.href}/mercure?topic=${this.name}&authorization=${this.session}`);
13
+ this.eventSource = new EventSource(this.href);
14
14
  Turbo.connectStreamSource(this.eventSource);
15
15
  }
16
16
 
@@ -21,14 +21,6 @@ export default class TurboTrain extends HTMLElement {
21
21
  get href() {
22
22
  return this.getAttribute('href');
23
23
  }
24
-
25
- get session() {
26
- return this.getAttribute('session');
27
- }
28
-
29
- get name() {
30
- return this.getAttribute('name');
31
- }
32
24
  }
33
25
 
34
26
  if (
@@ -1 +1 @@
1
- import{Turbo as e}from"@hotwired/turbo-rails";export default class t extends HTMLElement{static get observedAttributes(){return["href","session","name"]}constructor(){super()}connectedCallback(){this.eventSource=new EventSource(`${this.href}/mercure?topic=${this.name}&authorization=${this.session}`),e.connectStreamSource(this.eventSource)}disconnectedCallback(){e.disconnectStreamSource(this.eventSource)}get href(){return this.getAttribute("href")}get session(){return this.getAttribute("session")}get name(){return this.getAttribute("name")}};"undefined"==typeof window||window.customElements.get("turbo-train-stream-source")||window.customElements.define("turbo-train-stream-source",t);
1
+ import{Turbo as e}from"@hotwired/turbo-rails";export default class t extends HTMLElement{static get observedAttributes(){return["href"]}constructor(){super()}connectedCallback(){this.eventSource=new EventSource(this.href),e.connectStreamSource(this.eventSource)}disconnectedCallback(){e.disconnectStreamSource(this.eventSource)}get href(){return this.getAttribute("href")}};"undefined"==typeof window||window.customElements.get("turbo-train-stream-source")||window.customElements.define("turbo-train-stream-source",t);
@@ -1,8 +1,6 @@
1
1
  module Turbo::Train::StreamsHelper
2
2
  def turbo_train_from(*streamables, **attributes)
3
- attributes[:name] = Turbo::Train.signed_stream_name(streamables)
4
- attributes[:session] = Turbo::Train.encode({ platform: "web" })
5
- attributes[:href] = Turbo::Train.url
3
+ attributes[:href] = Turbo::Train.server(attributes[:server]&.to_sym).listen_url(streamables, platform: "web")
6
4
  tag.turbo_train_stream_source(**attributes)
7
5
  end
8
6
  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,27 @@
1
+ module Turbo
2
+ module Train
3
+ class BaseServer
4
+ attr_reader :configuration
5
+
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ end
9
+
10
+ def publish(topics:, data:)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def server_config
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def publish_url
19
+ server_config.publish_url
20
+ end
21
+
22
+ def listen_url(topic, **options)
23
+ server_config.listen_url(topic, **options)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,91 @@
1
+ module Turbo::Train::Broadcasts
2
+ def broadcast(streamables, content:, server: nil)
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(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
+ ), server: rendering.delete(:server))
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
@@ -1,13 +1,73 @@
1
1
  module Turbo
2
2
  module Train
3
- class Configuration
4
- attr_accessor :mercure_domain, :publisher_key, :subscriber_key, :skip_ssl_verification
3
+ class MercureConfiguration
4
+ attr_accessor :mercure_domain, :publisher_key, :subscriber_key
5
5
 
6
6
  def initialize
7
+ super
7
8
  @mercure_domain = 'localhost'
8
9
  @publisher_key = 'test'
9
10
  @subscriber_key = 'testing'
11
+ end
12
+
13
+ def url
14
+ "https://#{mercure_domain}/.well-known"
15
+ end
16
+
17
+ def publish_url
18
+ "#{url}/mercure"
19
+ end
20
+
21
+ def listen_url(topic, platform: 'web')
22
+ "#{url}/mercure?topic=#{Turbo::Train.signed_stream_name(topic)}&authorization=#{jwt_auth_token({ platform: platform })}"
23
+ end
24
+
25
+ def jwt_auth_token(payload)
26
+ structured_payload = { mercure: { payload: payload } }
27
+ JWT.encode structured_payload, subscriber_key, ALGORITHM
28
+ end
29
+ end
30
+
31
+ class FanoutConfiguration
32
+ attr_accessor :fastly_api_url, :service_url, :fastly_key, :service_id
33
+
34
+ def initialize
35
+ super
36
+ @fastly_api_url = 'https://api.fastly.com'
37
+ @service_url = 'https://johnny-cage-fake-url.edgecompute.app'
38
+ @fastly_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
39
+ end
40
+
41
+ def publish_url
42
+ "#{@fastly_api_url}/service/#{@service_id}/publish/"
43
+ end
44
+
45
+ def listen_url(topic, **)
46
+ "#{service_url}/stream/sse?topic=#{Turbo::Train.signed_stream_name(topic)}"
47
+ end
48
+ end
49
+
50
+ class Configuration
51
+ attr_accessor :skip_ssl_verification, :mercure, :fanout, :default_server
52
+
53
+ def initialize
10
54
  @skip_ssl_verification = Rails.env.development? || Rails.env.test?
55
+ @mercure = nil
56
+ @fanout = nil
57
+ @default_server = :mercure
58
+ end
59
+
60
+ def server(server_name)
61
+ case server_name
62
+ when :mercure
63
+ @mercure ||= MercureConfiguration.new
64
+ yield(@mercure)
65
+ when :fanout
66
+ @fanout ||= FanoutConfiguration.new
67
+ yield(@fanout)
68
+ else
69
+ raise ArgumentError, "Unknown server name: #{server_name}"
70
+ end
11
71
  end
12
72
  end
13
73
 
@@ -0,0 +1,36 @@
1
+ module Turbo
2
+ module Train
3
+ class FanoutServer < BaseServer
4
+ def publish(topics:, data:)
5
+ uri = URI(server_config.publish_url)
6
+ req = Net::HTTP::Post.new(uri)
7
+ req['Fastly-Key'] = server_config.fastly_key
8
+
9
+ message = data[:data].gsub("\n", "")
10
+ payload = {items: []}
11
+
12
+ Array(topics).each do |topic|
13
+ payload[:items] << {channel: topic, formats: { 'http-stream': { content: "event: message\ndata: #{message}\n\n" } } }
14
+ end
15
+
16
+ req.body = payload.to_json
17
+
18
+ opts = {
19
+ use_ssl: uri.scheme == 'https'
20
+ }
21
+
22
+ if configuration.skip_ssl_verification
23
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
24
+ end
25
+
26
+ Net::HTTP.start(uri.host, uri.port, opts) do |http|
27
+ http.request(req)
28
+ end
29
+ end
30
+
31
+ def server_config
32
+ configuration.fanout
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ module Turbo
2
+ module Train
3
+ class MercureServer < BaseServer
4
+ def publish(topics:, data:)
5
+ payload = { mercure: { publish: topics } }
6
+ token = JWT.encode payload, server_config.publisher_key, ALGORITHM
7
+
8
+ uri = URI(publish_url)
9
+
10
+ req = Net::HTTP::Post.new(uri)
11
+ req['Content-Type'] = 'application/x-www-form-urlencoded'
12
+ req['Authorization'] = "Bearer #{token}"
13
+
14
+ req.body = URI.encode_www_form(data)
15
+ opts = {
16
+ use_ssl: uri.scheme == 'https'
17
+ }
18
+
19
+ if configuration.skip_ssl_verification
20
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
21
+ end
22
+
23
+ Net::HTTP.start(uri.host, uri.port, opts) do |http|
24
+ http.request(req)
25
+ end
26
+ end
27
+
28
+ def server_config
29
+ configuration.mercure
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,79 @@
1
+ # Based on: ActionCable::TestHelper
2
+ module Turbo
3
+ module Train
4
+ module TestHelper
5
+ def before_setup
6
+ test_server = case ENV.fetch('TURBO_TRAIN_TEST_SERVER', 'mercure').to_sym
7
+ when :mercure
8
+ Turbo::Train::TestServer.new(Turbo::Train.mercure_server, Turbo::Train.configuration)
9
+ when :fanout
10
+ Turbo::Train::TestServer.new(Turbo::Train.fanout_server, Turbo::Train.configuration)
11
+ else
12
+ raise "Unknown test server: #{ENV['TURBO_TRAIN_TEST_SERVER']}"
13
+ end
14
+
15
+ Turbo::Train.instance_variable_set(:@server, test_server)
16
+ super
17
+ end
18
+
19
+ def after_teardown
20
+ test_server = case ENV.fetch('TURBO_TRAIN_TEST_SERVER', 'mercure').to_sym
21
+ when :mercure
22
+ Turbo::Train.mercure_server
23
+ when :fanout
24
+ Turbo::Train.fanout_server
25
+ else
26
+ raise "Unknown test server: #{ENV['TURBO_TRAIN_TEST_SERVER']}"
27
+ end
28
+
29
+ super
30
+ Turbo::Train.instance_variable_set(:@server, test_server)
31
+ end
32
+
33
+ def assert_broadcast_on(stream, data, &block)
34
+ signed_stream_name = Turbo::Train.signed_stream_name(stream)
35
+
36
+ new_messages = Turbo::Train.server.broadcasts(signed_stream_name)
37
+ if block_given?
38
+ old_messages = new_messages
39
+ Turbo::Train.server.clear_messages(signed_stream_name)
40
+
41
+ assert_nothing_raised(&block)
42
+ new_messages = Turbo::Train.server.broadcasts(signed_stream_name)
43
+ Turbo::Train.server.clear_messages(signed_stream_name)
44
+
45
+ # Restore all sent messages
46
+ (old_messages + new_messages).each { |m| Turbo::Train.server.broadcasts(signed_stream_name) << m }
47
+ end
48
+
49
+ message = new_messages.find { |msg| msg == data }
50
+ if message.nil?
51
+ puts "signed_stream_name => #{signed_stream_name}"
52
+ puts "channels_data: #{Turbo::Train.server.channels_data.inspect}}"
53
+ end
54
+
55
+ assert message, "No messages sent with #{data} to #{Turbo::Train.stream_name_from(stream)}"
56
+ end
57
+
58
+ def assert_body_match(r)
59
+ if Turbo::Train.server.real_server.is_a?(Turbo::Train::FanoutServer)
60
+ assert_match "Published\n", r.body
61
+ elsif Turbo::Train.server.real_server.is_a?(Turbo::Train::MercureServer)
62
+ assert_match /urn:uuid:.*/, r.body
63
+ else
64
+ raise "Unknown server type"
65
+ end
66
+ end
67
+
68
+ def assert_response_from_mercure_server(r)
69
+ assert_equal r.code, '200'
70
+ assert_match /urn:uuid:.*/, r.body
71
+ end
72
+
73
+ def assert_response_from_fanout_server(r)
74
+ assert_equal r.code, '200'
75
+ assert_match "Published\n", r.body
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ module Turbo
2
+ module Train
3
+ class TestServer < BaseServer
4
+ attr_reader :configuration, :channels_data, :real_server
5
+
6
+ def initialize(real_server, configuration)
7
+ @configuration = configuration
8
+ @channels_data = {}
9
+ @real_server = real_server
10
+ end
11
+
12
+ def publish(topics:, data:)
13
+ Array(topics).each do |topic|
14
+ @channels_data[topic] ||= []
15
+ @channels_data[topic] << data[:data]
16
+ end
17
+
18
+ real_server.publish(topics: topics, data: data) if real_server
19
+ end
20
+
21
+ def broadcasts(channel)
22
+ channels_data[channel] ||= []
23
+ end
24
+
25
+ def clear_messages(channel)
26
+ channels_data[channel] = []
27
+ end
28
+
29
+ def clear
30
+ @channels_data = []
31
+ end
32
+ end
33
+ end
34
+ 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,85 +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)
44
- end
45
-
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)
56
- end
57
-
58
- def broadcast_prepend_to(*streamables, **opts)
59
- broadcast_action_to(*streamables, action: :prepend, **opts)
60
- end
61
-
62
- private
63
-
64
- def domain
65
- Rails.configuration.turbo_train.mercure_domain
66
- end
67
-
68
- def render_format(format, **rendering)
69
- ApplicationController.render(formats: [ format ], **rendering)
70
- end
71
-
72
- def broadcast(streamables, content:)
73
- topics = if streamables.is_a?(Array)
74
- streamables.map { |s| signed_stream_name(s) }
21
+ def server(server = nil)
22
+ @server ||= case server || configuration.default_server
23
+ when :mercure
24
+ mercure_server
25
+ when :fanout
26
+ fanout_server
75
27
  else
76
- [signed_stream_name(streamables)]
28
+ raise ArgumentError, "Unknown server: #{server}"
77
29
  end
30
+ end
78
31
 
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")
32
+ def mercure_server
33
+ raise ArgumentError, "Mercure configuration is missing" unless configuration.mercure
87
34
 
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
- }
35
+ @mercure_server ||= MercureServer.new(configuration)
36
+ end
95
37
 
96
- if configuration.skip_ssl_verification
97
- opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
98
- end
38
+ def fanout_server
39
+ raise ArgumentError, "Fanout configuration is missing" unless configuration.fanout
99
40
 
100
- Net::HTTP.start(uri.host, uri.port, opts) do |http|
101
- http.request(req)
102
- end
41
+ @fanout_server ||= FanoutServer.new(configuration)
103
42
  end
104
43
 
105
44
  def stream_name_from(streamables)
@@ -109,6 +48,12 @@ module Turbo
109
48
  streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
110
49
  end
111
50
  end
51
+
52
+ private
53
+
54
+ def render_format(format, **rendering)
55
+ ApplicationController.render(formats: [ format ], **rendering)
56
+ end
112
57
  end
113
58
  end
114
- end
59
+ end
@@ -1,5 +1,5 @@
1
1
  module Turbo
2
2
  module Train
3
- VERSION = "0.1.1"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/turbo/train.rb CHANGED
@@ -1,3 +1,9 @@
1
1
  require "turbo/train/version"
2
2
  require "turbo/train/config"
3
+ require 'turbo/train/broadcasts'
4
+ require 'turbo/train/base_server'
5
+ require 'turbo/train/mercure_server'
6
+ require 'turbo/train/fanout_server'
7
+ require 'turbo/train/test_server'
8
+ require 'turbo/train/test_helper'
3
9
  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.3.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-29 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,14 @@ 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/base_server.rb
88
+ - lib/turbo/train/broadcasts.rb
84
89
  - lib/turbo/train/config.rb
85
90
  - lib/turbo/train/engine.rb
91
+ - lib/turbo/train/fanout_server.rb
92
+ - lib/turbo/train/mercure_server.rb
93
+ - lib/turbo/train/test_helper.rb
94
+ - lib/turbo/train/test_server.rb
86
95
  - lib/turbo/train/train.rb
87
96
  - lib/turbo/train/version.rb
88
97
  homepage: https://github.com/Uscreen-video/turbo-train
@@ -91,7 +100,7 @@ licenses:
91
100
  metadata:
92
101
  homepage_uri: https://github.com/Uscreen-video/turbo-train
93
102
  source_code_uri: https://github.com/goodsign/turbo-train-test
94
- post_install_message:
103
+ post_install_message:
95
104
  rdoc_options: []
96
105
  require_paths:
97
106
  - lib
@@ -107,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
116
  version: '0'
108
117
  requirements: []
109
118
  rubygems_version: 3.1.6
110
- signing_key:
119
+ signing_key:
111
120
  specification_version: 4
112
121
  summary: Rails Turbo Stream broadcasting over SSE instead of WebSockets. Uses Mercure
113
122
  server.