hotsock-turbo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.standard.yml +1 -0
- data/Gemfile +16 -0
- data/README.md +323 -0
- data/Rakefile +10 -0
- data/app/assets/javascripts/hotsock-turbo.js +142 -0
- data/app/controllers/hotsock/turbo/tokens_controller.rb +21 -0
- data/app/jobs/hotsock/turbo/action_broadcast_job.rb +23 -0
- data/app/jobs/hotsock/turbo/broadcast_job.rb +16 -0
- data/app/jobs/hotsock/turbo/broadcast_refresh_job.rb +15 -0
- data/config/routes.rb +5 -0
- data/hotsock-turbo.gemspec +33 -0
- data/lib/hotsock/turbo/broadcastable.rb +518 -0
- data/lib/hotsock/turbo/config.rb +17 -0
- data/lib/hotsock/turbo/engine.rb +33 -0
- data/lib/hotsock/turbo/streams_channel.rb +228 -0
- data/lib/hotsock/turbo/streams_helper.rb +47 -0
- data/lib/hotsock/turbo/version.rb +7 -0
- data/lib/hotsock-turbo.rb +23 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: acb980991920b0cf9f918c2eb1415f7236cd68a1b7d32ef32fb23e61ef123ec1
|
|
4
|
+
data.tar.gz: 501175af602730032c80b888b419050d3dd9f95258e9c6d3b96c850c91b583e9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d4f52b687ea9e85db973edad805dac02761ec3f0c1ec602c36473c13a7c38dafd51f6b864004cad85f9db5dd6230d11d0236bf3445e97f6b5ba7b20a74405017
|
|
7
|
+
data.tar.gz: 509e133c9a296026acaf7ed40ed161075c688c08da13a9a6379208dbe5317c18cb9932fcf4a561d6feff8285e5f2958f15a55c6f0895dda5f2895236890b3dca
|
data/.standard.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby_version: 3.2
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Hotsock Turbo
|
|
2
|
+
|
|
3
|
+
Turbo Streams integration for [Hotsock](https://www.hotsock.io), enabling real-time updates in Rails applications using Hotsock's WebSocket infrastructure.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Hotsock Turbo provides a seamless way to broadcast Turbo Stream updates to your Rails application using Hotsock instead of Action Cable. It offers:
|
|
8
|
+
|
|
9
|
+
- Real-time page updates via WebSockets
|
|
10
|
+
- Model callbacks for automatic broadcasting on create, update, and destroy
|
|
11
|
+
- Support for Turbo Stream refresh (morphing) broadcasts
|
|
12
|
+
- Drop-in replacement mode for existing `Turbo::Broadcastable` usage
|
|
13
|
+
- Both synchronous and asynchronous (ActiveJob) broadcasting
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Ruby >= 3.2
|
|
18
|
+
- Rails with [Turbo](https://github.com/hotwired/turbo-rails)
|
|
19
|
+
- [Hotsock](https://rubygems.org/gems/hotsock) gem (>= 1.0)
|
|
20
|
+
- [@hotsock/hotsock-js](https://www.npmjs.com/package/@hotsock/hotsock-js) npm package
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add the gem to your Gemfile:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "turbo-rails"
|
|
28
|
+
gem "hotsock-turbo"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### JavaScript Setup
|
|
38
|
+
|
|
39
|
+
#### Using Importmap
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bin/importmap pin @hotsock/hotsock-js
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then in your `application.js`:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import "@hotsock/hotsock-js"
|
|
49
|
+
import "hotsock-turbo"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Using npm/yarn
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @hotsock/hotsock-js
|
|
56
|
+
# or
|
|
57
|
+
yarn add @hotsock/hotsock-js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then in your JavaScript entry point:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
import "@hotsock/hotsock-js"
|
|
64
|
+
import "hotsock-turbo"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
Create an initializer at `config/initializers/hotsock_turbo.rb`:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Hotsock::Turbo.configure do |config|
|
|
73
|
+
# Required: Path to an endpoint that returns Hotsock connection tokens
|
|
74
|
+
config.connect_token_path = "/hotsock/connect_token"
|
|
75
|
+
|
|
76
|
+
# Required: Your Hotsock WebSocket URL
|
|
77
|
+
config.wss_url = "wss://your-hotsock-instance.example.com"
|
|
78
|
+
|
|
79
|
+
# Optional: Log level for the JavaScript client (default: "warn")
|
|
80
|
+
config.log_level = "warn"
|
|
81
|
+
|
|
82
|
+
# Optional: Parent controller for any generated routes (default: "ApplicationController")
|
|
83
|
+
config.parent_controller = "ApplicationController"
|
|
84
|
+
|
|
85
|
+
# Optional: Enable drop-in replacement for Turbo::Broadcastable (default: false)
|
|
86
|
+
config.override_turbo_broadcastable = false
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Configuration Options
|
|
91
|
+
|
|
92
|
+
| Option | Default | Description |
|
|
93
|
+
| ------------------------------ | ------------------------- | ---------------------------------------------------------------------- |
|
|
94
|
+
| `connect_token_path` | `nil` | Path to your endpoint that issues Hotsock connection tokens |
|
|
95
|
+
| `wss_url` | `nil` | Your Hotsock WebSocket server URL |
|
|
96
|
+
| `log_level` | `"warn"` | JavaScript client log level (`"debug"`, `"info"`, `"warn"`, `"error"`) |
|
|
97
|
+
| `parent_controller` | `"ApplicationController"` | Base controller for generated routes |
|
|
98
|
+
| `override_turbo_broadcastable` | `false` | When `true`, overrides standard Turbo broadcast methods to use Hotsock |
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
### View Helpers
|
|
103
|
+
|
|
104
|
+
#### Meta Tags
|
|
105
|
+
|
|
106
|
+
Add the required meta tags to your layout (typically in `<head>`):
|
|
107
|
+
|
|
108
|
+
```erb
|
|
109
|
+
<%= hotsock_turbo_meta_tags %>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This renders the configuration needed by the JavaScript client:
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<meta name="hotsock:connect-token-path" content="/hotsock/connect_token" />
|
|
116
|
+
<meta name="hotsock:log-level" content="warn" />
|
|
117
|
+
<meta
|
|
118
|
+
name="hotsock:wss-url"
|
|
119
|
+
content="wss://your-hotsock-instance.example.com"
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
You can override configuration per-page:
|
|
124
|
+
|
|
125
|
+
```erb
|
|
126
|
+
<%= hotsock_turbo_meta_tags wss_url: "wss://other.example.com", log_level: "debug" %>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Stream Subscriptions
|
|
130
|
+
|
|
131
|
+
Subscribe to streams in your views:
|
|
132
|
+
|
|
133
|
+
```erb
|
|
134
|
+
<%= hotsock_turbo_stream_from @board %>
|
|
135
|
+
<%= hotsock_turbo_stream_from @board, :messages %>
|
|
136
|
+
<%= hotsock_turbo_stream_from "custom_stream_name" %>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This renders a custom element that manages the WebSocket subscription:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<hotsock-turbo-stream-source
|
|
143
|
+
data-channel="..."
|
|
144
|
+
data-token="..."
|
|
145
|
+
data-user-id="..."
|
|
146
|
+
>
|
|
147
|
+
</hotsock-turbo-stream-source>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Model Broadcasting
|
|
151
|
+
|
|
152
|
+
Hotsock Turbo automatically includes broadcasting methods in all ActiveRecord models.
|
|
153
|
+
|
|
154
|
+
#### Broadcasting Refreshes
|
|
155
|
+
|
|
156
|
+
For models that should trigger page refreshes (works great with Turbo's morphing):
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class Board < ApplicationRecord
|
|
160
|
+
# Broadcasts refresh to "boards" stream on create/update/destroy
|
|
161
|
+
hotsock_broadcasts_refreshes
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class Column < ApplicationRecord
|
|
165
|
+
belongs_to :board
|
|
166
|
+
|
|
167
|
+
# Broadcasts refresh to the board's stream on create/update/destroy
|
|
168
|
+
hotsock_broadcasts_refreshes_to :board
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Broadcasting DOM Updates
|
|
173
|
+
|
|
174
|
+
For fine-grained DOM updates (append, replace, remove):
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class Message < ApplicationRecord
|
|
178
|
+
belongs_to :board
|
|
179
|
+
|
|
180
|
+
# Broadcasts append on create, replace on update, remove on destroy
|
|
181
|
+
hotsock_broadcasts_to :board
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class Comment < ApplicationRecord
|
|
185
|
+
# Broadcasts to "comments" stream by default
|
|
186
|
+
hotsock_broadcasts
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Options
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
class Message < ApplicationRecord
|
|
194
|
+
belongs_to :board
|
|
195
|
+
|
|
196
|
+
# Customize the insert action and target
|
|
197
|
+
hotsock_broadcasts_to :board,
|
|
198
|
+
inserts_by: :prepend, # :append (default), :prepend
|
|
199
|
+
target: "board_messages", # DOM ID to target
|
|
200
|
+
partial: "messages/card" # Custom partial
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Instance Methods
|
|
205
|
+
|
|
206
|
+
Broadcast from anywhere in your application:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
message = Message.find(1)
|
|
210
|
+
|
|
211
|
+
# Sync broadcasts (immediate)
|
|
212
|
+
message.hotsock_broadcast_refresh
|
|
213
|
+
message.hotsock_broadcast_refresh_to(board)
|
|
214
|
+
message.hotsock_broadcast_replace
|
|
215
|
+
message.hotsock_broadcast_replace_to(board)
|
|
216
|
+
message.hotsock_broadcast_append_to(board, target: "messages")
|
|
217
|
+
message.hotsock_broadcast_prepend_to(board, target: "messages")
|
|
218
|
+
message.hotsock_broadcast_remove
|
|
219
|
+
message.hotsock_broadcast_remove_to(board)
|
|
220
|
+
|
|
221
|
+
# Async broadcasts (via ActiveJob)
|
|
222
|
+
message.hotsock_broadcast_refresh_later
|
|
223
|
+
message.hotsock_broadcast_refresh_later_to(board)
|
|
224
|
+
message.hotsock_broadcast_replace_later
|
|
225
|
+
message.hotsock_broadcast_replace_later_to(board)
|
|
226
|
+
message.hotsock_broadcast_append_later_to(board, target: "messages")
|
|
227
|
+
message.hotsock_broadcast_prepend_later_to(board, target: "messages")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Direct Channel Broadcasting
|
|
231
|
+
|
|
232
|
+
Broadcast without a model instance:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
Hotsock::Turbo::StreamsChannel.broadcast_refresh_to(board)
|
|
236
|
+
Hotsock::Turbo::StreamsChannel.broadcast_append_to(
|
|
237
|
+
board,
|
|
238
|
+
target: "messages",
|
|
239
|
+
partial: "messages/message",
|
|
240
|
+
locals: { message: message }
|
|
241
|
+
)
|
|
242
|
+
Hotsock::Turbo::StreamsChannel.broadcast_remove_to(board, target: "message_123")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Drop-in Turbo Replacement
|
|
246
|
+
|
|
247
|
+
If you have existing code using `Turbo::Broadcastable` methods, you can enable drop-in replacement mode:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# config/initializers/hotsock_turbo.rb
|
|
251
|
+
Hotsock::Turbo.configure do |config|
|
|
252
|
+
config.override_turbo_broadcastable = true
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Now standard Turbo method names work with Hotsock:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
class Message < ApplicationRecord
|
|
260
|
+
belongs_to :board
|
|
261
|
+
|
|
262
|
+
# These now use Hotsock instead of Action Cable
|
|
263
|
+
broadcasts_refreshes_to :board
|
|
264
|
+
broadcasts_to :board
|
|
265
|
+
broadcasts
|
|
266
|
+
broadcasts_refreshes
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Instance methods also work
|
|
270
|
+
message.broadcast_refresh
|
|
271
|
+
message.broadcast_replace_later_to(board)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Suppressing Broadcasts
|
|
275
|
+
|
|
276
|
+
Temporarily disable broadcasts within a block:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
Message.suppressing_turbo_broadcasts do
|
|
280
|
+
# No broadcasts will be sent
|
|
281
|
+
Message.create!(content: "Silent message", board: board)
|
|
282
|
+
message.update!(content: "Silent update")
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Check suppression status:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
Message.suppressed_turbo_broadcasts? # => false
|
|
290
|
+
|
|
291
|
+
Message.suppressing_turbo_broadcasts do
|
|
292
|
+
Message.suppressed_turbo_broadcasts? # => true
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Customization
|
|
297
|
+
|
|
298
|
+
### Custom User Identification
|
|
299
|
+
|
|
300
|
+
By default, subscriptions use `session.id` as the user identifier. Override this by defining `hotsock_uid` in your helper or controller:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
# app/helpers/application_helper.rb
|
|
304
|
+
module ApplicationHelper
|
|
305
|
+
private
|
|
306
|
+
|
|
307
|
+
def hotsock_uid
|
|
308
|
+
current_user&.id&.to_s || session.id.to_s
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## How It Works
|
|
314
|
+
|
|
315
|
+
1. **Meta tags** provide configuration to the JavaScript client
|
|
316
|
+
2. **`<hotsock-turbo-stream-source>`** elements connect to Hotsock via WebSocket
|
|
317
|
+
3. **Model callbacks** or direct calls trigger broadcasts via `Hotsock::Turbo::StreamsChannel`
|
|
318
|
+
4. **Hotsock** delivers messages to subscribed clients
|
|
319
|
+
5. **JavaScript client** receives messages and calls `Turbo.renderStreamMessage()` to update the DOM
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { HotsockClient } from "@hotsock/hotsock-js"
|
|
2
|
+
|
|
3
|
+
function createHotsockClient() {
|
|
4
|
+
return new HotsockClient(
|
|
5
|
+
document
|
|
6
|
+
.querySelector('meta[name="hotsock:wss-url"]')
|
|
7
|
+
?.getAttribute("content"),
|
|
8
|
+
{
|
|
9
|
+
connectTokenFn: async () => {
|
|
10
|
+
const connectTokenPath = document.querySelector(
|
|
11
|
+
'meta[name="hotsock:connect-token-path"]'
|
|
12
|
+
).content
|
|
13
|
+
const csrfToken = document.querySelector(
|
|
14
|
+
'meta[name="csrf-token"]'
|
|
15
|
+
).content
|
|
16
|
+
const response = await fetch(connectTokenPath, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
"x-csrf-token": csrfToken,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
const data = await response.json()
|
|
23
|
+
return data.token
|
|
24
|
+
},
|
|
25
|
+
lazyConnection: true,
|
|
26
|
+
logLevel: document.querySelector('meta[name="hotsock:log-level"]')
|
|
27
|
+
?.content,
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hotsockClient = window.Hotsock || createHotsockClient()
|
|
33
|
+
window.Hotsock = hotsockClient
|
|
34
|
+
|
|
35
|
+
// Track active subscriptions by channel
|
|
36
|
+
const subscriptions = new Map() // channel -> { binding, elements: Set, unsubscribeTimer }
|
|
37
|
+
const UNSUBSCRIBE_DELAY_MS = 250
|
|
38
|
+
|
|
39
|
+
class HotsockTurboStreamSourceElement extends HTMLElement {
|
|
40
|
+
#channel = null
|
|
41
|
+
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
this.#subscribe()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disconnectedCallback() {
|
|
47
|
+
this.#unsubscribe()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#subscribe() {
|
|
51
|
+
const { channel, token } = this.dataset
|
|
52
|
+
|
|
53
|
+
if (!channel || !token) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.#channel = channel
|
|
58
|
+
|
|
59
|
+
if (typeof window === "undefined" || !window.Turbo) {
|
|
60
|
+
console.warn("hotsock-turbo-stream-source: Turbo is not available")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let sub = subscriptions.get(channel)
|
|
65
|
+
|
|
66
|
+
if (sub) {
|
|
67
|
+
// Cancel unsubscribe if pending
|
|
68
|
+
if (sub.unsubscribeTimer) {
|
|
69
|
+
clearTimeout(sub.unsubscribeTimer)
|
|
70
|
+
sub.unsubscribeTimer = null
|
|
71
|
+
}
|
|
72
|
+
// Add this element to the subscription's element set
|
|
73
|
+
sub.elements.add(this)
|
|
74
|
+
} else {
|
|
75
|
+
const { Turbo } = window
|
|
76
|
+
const binding = hotsockClient.bind(
|
|
77
|
+
"turbo_stream",
|
|
78
|
+
({ data }) => {
|
|
79
|
+
if (!data?.html) return
|
|
80
|
+
try {
|
|
81
|
+
Turbo.renderStreamMessage(data.html)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Failed to render Turbo Stream message:", error)
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{ channel, subscribeTokenFn: () => token }
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
sub = { binding, elements: new Set([this]), unsubscribeTimer: null }
|
|
90
|
+
subscriptions.set(channel, sub)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.subscriptionConnected()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#unsubscribe() {
|
|
97
|
+
const channel = this.#channel
|
|
98
|
+
if (!channel) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sub = subscriptions.get(channel)
|
|
103
|
+
if (!sub) {
|
|
104
|
+
this.subscriptionDisconnected()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Remove this element from the subscription
|
|
109
|
+
sub.elements.delete(this)
|
|
110
|
+
|
|
111
|
+
// If no elements remain, schedule delayed unsubscribe
|
|
112
|
+
if (sub.elements.size === 0 && !sub.unsubscribeTimer) {
|
|
113
|
+
sub.unsubscribeTimer = setTimeout(() => {
|
|
114
|
+
// Double-check no elements have reconnected
|
|
115
|
+
if (sub.elements.size === 0) {
|
|
116
|
+
sub.binding.unbind()
|
|
117
|
+
subscriptions.delete(channel)
|
|
118
|
+
}
|
|
119
|
+
}, UNSUBSCRIBE_DELAY_MS)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.subscriptionDisconnected()
|
|
123
|
+
this.#channel = null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
subscriptionConnected() {
|
|
127
|
+
this.setAttribute("connected", "")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
subscriptionDisconnected() {
|
|
131
|
+
this.removeAttribute("connected")
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (customElements.get("hotsock-turbo-stream-source") === undefined) {
|
|
136
|
+
customElements.define(
|
|
137
|
+
"hotsock-turbo-stream-source",
|
|
138
|
+
HotsockTurboStreamSourceElement
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { hotsockClient }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotsock
|
|
4
|
+
module Turbo
|
|
5
|
+
class TokensController < Hotsock::Turbo.config.parent_controller.constantize
|
|
6
|
+
def connect
|
|
7
|
+
render json: {token: connect_token}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def connect_token
|
|
13
|
+
uid = respond_to?(:hotsock_uid, true) ? hotsock_uid : session.id.to_s
|
|
14
|
+
umd = respond_to?(:hotsock_umd, true) ? hotsock_umd : nil
|
|
15
|
+
|
|
16
|
+
claims = {scope: "connect", keepAlive: true, uid:, umd:}
|
|
17
|
+
Hotsock.issue_token(claims)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
|
|
5
|
+
module Hotsock
|
|
6
|
+
module Turbo
|
|
7
|
+
# The job that powers all the broadcast_$action_later broadcasts.
|
|
8
|
+
class ActionBroadcastJob < ActiveJob::Base
|
|
9
|
+
discard_on ActiveJob::DeserializationError
|
|
10
|
+
|
|
11
|
+
def perform(stream, action:, target:, targets: nil, attributes: {}, **rendering)
|
|
12
|
+
Hotsock::Turbo::StreamsChannel.broadcast_action_to(
|
|
13
|
+
stream,
|
|
14
|
+
action:,
|
|
15
|
+
target:,
|
|
16
|
+
targets:,
|
|
17
|
+
attributes:,
|
|
18
|
+
**rendering
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
|
|
5
|
+
module Hotsock
|
|
6
|
+
module Turbo
|
|
7
|
+
# The job that powers broadcast_render_later_to for rendering turbo stream templates.
|
|
8
|
+
class BroadcastJob < ActiveJob::Base
|
|
9
|
+
discard_on ActiveJob::DeserializationError
|
|
10
|
+
|
|
11
|
+
def perform(stream, **rendering)
|
|
12
|
+
Hotsock::Turbo::StreamsChannel.broadcast_render_to(stream, **rendering)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
|
|
5
|
+
module Hotsock
|
|
6
|
+
module Turbo
|
|
7
|
+
class BroadcastRefreshJob < ActiveJob::Base
|
|
8
|
+
discard_on ActiveJob::DeserializationError
|
|
9
|
+
|
|
10
|
+
def perform(stream, request_id: nil)
|
|
11
|
+
Hotsock::Turbo::StreamsChannel.broadcast_refresh_to(stream, request_id:)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
|
|
4
|
+
|
|
5
|
+
require "hotsock/turbo/version"
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = "hotsock-turbo"
|
|
9
|
+
spec.version = Hotsock::Turbo::VERSION
|
|
10
|
+
spec.authors = ["James Miller"]
|
|
11
|
+
spec.email = ["support@hotsock.io"]
|
|
12
|
+
spec.homepage = "https://www.hotsock.io"
|
|
13
|
+
spec.summary = "Turbo Streams integration for Hotsock"
|
|
14
|
+
spec.description = "Provides Turbo Streams integration for Hotsock, enabling real-time updates in Rails applications using Hotsock's WebSocket infrastructure."
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.required_ruby_version = ">= 3.2"
|
|
17
|
+
|
|
18
|
+
spec.metadata = {
|
|
19
|
+
"homepage_uri" => "https://www.hotsock.io",
|
|
20
|
+
"bug_tracker_uri" => "https://github.com/hotsock/hotsock-turbo/issues",
|
|
21
|
+
"documentation_uri" => "https://github.com/hotsock/hotsock-turbo/blob/main/README.md",
|
|
22
|
+
"changelog_uri" => "https://github.com/hotsock/hotsock-turbo/releases",
|
|
23
|
+
"source_code_uri" => "https://github.com/hotsock/hotsock-turbo"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ignored = Regexp.union(
|
|
27
|
+
/\A\.git/,
|
|
28
|
+
/\Atest/
|
|
29
|
+
)
|
|
30
|
+
spec.files = `git ls-files`.split("\n").reject { |f| ignored.match(f) }
|
|
31
|
+
|
|
32
|
+
spec.add_dependency "hotsock", "~> 1.0"
|
|
33
|
+
end
|