turbo_cable 1.0.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/CHANGELOG.md +56 -0
- data/EXAMPLES.md +480 -0
- data/MIT-LICENSE +20 -0
- data/README.md +337 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/turbo_cable/application.css +15 -0
- data/app/controllers/turbo_cable/application_controller.rb +4 -0
- data/app/helpers/turbo_cable/application_helper.rb +4 -0
- data/app/helpers/turbo_cable/streams_helper.rb +16 -0
- data/app/jobs/turbo_cable/application_job.rb +4 -0
- data/app/jobs/turbo_cable/broadcast_job.rb +71 -0
- data/app/mailers/turbo_cable/application_mailer.rb +6 -0
- data/app/models/turbo_cable/application_record.rb +5 -0
- data/app/views/layouts/turbo_cable/application.html.erb +17 -0
- data/config/routes.rb +2 -0
- data/lib/generators/turbo_cable/install/install_generator.rb +47 -0
- data/lib/generators/turbo_cable/install/templates/turbo_streams_controller.js +184 -0
- data/lib/tasks/turbo_cable_tasks.rake +4 -0
- data/lib/turbo_cable/broadcastable.rb +161 -0
- data/lib/turbo_cable/engine.rb +24 -0
- data/lib/turbo_cable/rack_handler.rb +204 -0
- data/lib/turbo_cable/version.rb +3 -0
- data/lib/turbo_cable.rb +9 -0
- metadata +83 -0
data/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# TurboCable
|
|
2
|
+
|
|
3
|
+
Custom WebSocket-based Turbo Streams implementation for Rails. Provides significant memory savings (79-85% reduction) for single-server deployments.
|
|
4
|
+
|
|
5
|
+
## ⚠️ Important Limitations
|
|
6
|
+
|
|
7
|
+
**TurboCable is designed for specific use cases.** Read carefully before adopting:
|
|
8
|
+
|
|
9
|
+
### ✅ When to Use TurboCable
|
|
10
|
+
|
|
11
|
+
- **Single-server applications** - All users connect to one Rails instance
|
|
12
|
+
- **Development environments** - Great for local dev with live reloading
|
|
13
|
+
- **Single-tenant deployments** - Each customer/event runs independently
|
|
14
|
+
- **Resource-constrained environments** - Memory savings matter (VPS, embedded)
|
|
15
|
+
- **Simple real-time needs** - Basic live updates within one process
|
|
16
|
+
|
|
17
|
+
### ❌ When NOT to Use TurboCable
|
|
18
|
+
|
|
19
|
+
- **Horizontally scaled apps** - Multiple servers/dynos serving same application (Heroku, AWS ECS, Kubernetes with replicas)
|
|
20
|
+
- **Load-balanced production** - Multiple Rails instances behind a load balancer
|
|
21
|
+
- **Cross-server broadcasts** - Need to broadcast to users on different machines
|
|
22
|
+
- **High-availability setups** - Require Redis or Solid Cable backed pub/sub across instances
|
|
23
|
+
- **Bidirectional WebSocket communication** - Client→Server data flow over WebSockets (chat apps, collaborative editing, real-time drawing)
|
|
24
|
+
- **Action Cable channels** - Custom channels with server-side actions and the channels DSL
|
|
25
|
+
|
|
26
|
+
**If you need cross-server broadcasts or bidirectional WebSocket communication, stick with Action Cable + Redis/Solid Cable.** TurboCable only broadcasts within a single Rails process and only supports server→client Turbo Streams.
|
|
27
|
+
|
|
28
|
+
## Why TurboCable?
|
|
29
|
+
|
|
30
|
+
For applications that fit the constraints above, Action Cable's memory overhead may be unnecessary. TurboCable provides the same Turbo Streams functionality using a lightweight WebSocket implementation built on Rack hijack and RFC 6455, with zero external dependencies beyond Ruby's standard library.
|
|
31
|
+
|
|
32
|
+
**Memory Savings (single server):**
|
|
33
|
+
- Action Cable: ~169MB per process
|
|
34
|
+
- TurboCable: ~25-35MB per process
|
|
35
|
+
- **Savings: 134-144MB (79-85% reduction)**
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
For applications within the constraints above:
|
|
40
|
+
|
|
41
|
+
- **Turbo Streams API compatibility** - Same `turbo_stream_from` and `broadcast_*` methods
|
|
42
|
+
- **Zero dependencies** - Only Ruby stdlib (no Redis, no Solid Cable, no external services)
|
|
43
|
+
- **Hybrid async/sync** - Uses Active Job when available, otherwise synchronous (transparent)
|
|
44
|
+
- **Simple installation** - `rails generate turbo_cable:install`
|
|
45
|
+
- **All Turbo Stream actions** - replace, update, append, prepend, remove
|
|
46
|
+
- **Auto-reconnection** - Handles connection drops gracefully
|
|
47
|
+
- **Thread-safe** - Concurrent connections and broadcasts
|
|
48
|
+
- **RFC 6455 compliant** - Standard WebSocket protocol
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Add this line to your application's Gemfile:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
gem "turbo_cable"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Install the gem:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bundle install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run the installer:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
rails generate turbo_cable:install
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This will:
|
|
71
|
+
1. Copy the Stimulus controller to `app/javascript/controllers/turbo_streams_controller.js`
|
|
72
|
+
2. Add `data-controller="turbo-streams"` to your `<body>` tag
|
|
73
|
+
|
|
74
|
+
Restart your Rails server and you're done!
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
> **💡 Want real-world examples?** See [EXAMPLES.md](EXAMPLES.md) for patterns drawn from production applications: live scoring, progress tracking, background job output, and more.
|
|
79
|
+
|
|
80
|
+
### In Your Views
|
|
81
|
+
|
|
82
|
+
Use `turbo_stream_from` exactly as you would with Action Cable:
|
|
83
|
+
|
|
84
|
+
```erb
|
|
85
|
+
<div>
|
|
86
|
+
<%= turbo_stream_from "counter_updates" %>
|
|
87
|
+
|
|
88
|
+
<span id="counter-value"><%= @counter.value %></span>
|
|
89
|
+
</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### In Your Models
|
|
93
|
+
|
|
94
|
+
Use the same broadcast methods you're familiar with:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class Counter < ApplicationRecord
|
|
98
|
+
def broadcast_update
|
|
99
|
+
broadcast_replace_later_to "counter_updates",
|
|
100
|
+
target: "counter-value",
|
|
101
|
+
html: "<span id='counter-value'>#{value}</span>"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Available broadcast methods:**
|
|
107
|
+
- `broadcast_replace_later_to` / `broadcast_replace_to`
|
|
108
|
+
- `broadcast_update_later_to` / `broadcast_update_to`
|
|
109
|
+
- `broadcast_append_later_to` / `broadcast_append_to`
|
|
110
|
+
- `broadcast_prepend_later_to` / `broadcast_prepend_to`
|
|
111
|
+
- `broadcast_remove_to`
|
|
112
|
+
|
|
113
|
+
All methods support the same options as Turbo Streams:
|
|
114
|
+
- `target:` - DOM element ID
|
|
115
|
+
- `partial:` - Render a partial
|
|
116
|
+
- `html:` - Use raw HTML
|
|
117
|
+
- `locals:` - Pass locals to partial
|
|
118
|
+
|
|
119
|
+
### Example with Partial
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
class Score < ApplicationRecord
|
|
123
|
+
after_save do
|
|
124
|
+
broadcast_replace_later_to "live-scores",
|
|
125
|
+
partial: "scores/score",
|
|
126
|
+
target: dom_id(self)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Custom JSON Broadcasting
|
|
132
|
+
|
|
133
|
+
For use cases that need structured data instead of HTML (progress bars, charts, interactive widgets), use `TurboCable::Broadcastable.broadcast_json`:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class OfflinePlaylistJob < ApplicationJob
|
|
137
|
+
def perform(user_id)
|
|
138
|
+
stream_name = "playlist_progress_#{user_id}"
|
|
139
|
+
|
|
140
|
+
# Broadcast JSON updates
|
|
141
|
+
TurboCable::Broadcastable.broadcast_json(stream_name, {
|
|
142
|
+
status: 'processing',
|
|
143
|
+
progress: 50,
|
|
144
|
+
message: 'Processing files...'
|
|
145
|
+
})
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**JavaScript handling** (in a Stimulus controller):
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
connect() {
|
|
154
|
+
document.addEventListener('turbo:stream-message', this.handleMessage.bind(this))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
handleMessage(event) {
|
|
158
|
+
const { stream, data } = event.detail
|
|
159
|
+
if (stream === 'playlist_progress_123') {
|
|
160
|
+
console.log(data.progress) // 50
|
|
161
|
+
this.updateProgressBar(data.progress, data.message)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The Stimulus controller automatically dispatches `turbo:stream-message` CustomEvents when receiving JSON data (non-HTML strings). See [EXAMPLES.md](EXAMPLES.md#custom-json-broadcasting) for a complete working example with progress tracking.
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
### Broadcast URL (Optional)
|
|
171
|
+
|
|
172
|
+
By default, broadcasts use this port selection logic:
|
|
173
|
+
1. `ENV['TURBO_CABLE_PORT']` - if set, always use this port
|
|
174
|
+
2. `ENV['PORT']` - if set and TURBO_CABLE_PORT is not set, use this port
|
|
175
|
+
3. `3000` - default fallback
|
|
176
|
+
|
|
177
|
+
Configure these environment variables if needed:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# config/application.rb or initializer
|
|
181
|
+
|
|
182
|
+
# Override PORT when it's set to a proxy/foreman port (e.g., Thruster, foreman defaults to 5000)
|
|
183
|
+
ENV['TURBO_CABLE_PORT'] = '3000'
|
|
184
|
+
|
|
185
|
+
# Or specify the complete URL (overrides all port detection)
|
|
186
|
+
ENV['TURBO_CABLE_BROADCAST_URL'] = 'http://localhost:3000/_broadcast'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**When to set `TURBO_CABLE_PORT`:**
|
|
190
|
+
- **Foreman/Overmind**: These set `PORT=5000` by default, but Rails runs on a different port
|
|
191
|
+
- **Thruster/nginx proxy**: When `PORT` is set to the proxy port, not the Rails server port
|
|
192
|
+
- **Never needed**: When `PORT` correctly points to your Rails server (like with Navigator/configurator.rb)
|
|
193
|
+
|
|
194
|
+
## Migration from Action Cable
|
|
195
|
+
|
|
196
|
+
**⚠️ First, verify your deployment architecture supports TurboCable.** If you have multiple Rails instances serving the same app (Heroku dynos, AWS containers, Kubernetes pods, load-balanced VPS), TurboCable won't work for you. See "When NOT to Use" above.
|
|
197
|
+
|
|
198
|
+
**If you're on a single server:**
|
|
199
|
+
|
|
200
|
+
**Views:** No changes needed! `turbo_stream_from` works identically.
|
|
201
|
+
|
|
202
|
+
**Models:** No changes needed! All `broadcast_*` methods work identically.
|
|
203
|
+
|
|
204
|
+
**Infrastructure:** Just add the gem and run the installer. Action Cable, Redis, and Solid Cable can be removed.
|
|
205
|
+
|
|
206
|
+
## Protocol Specification
|
|
207
|
+
|
|
208
|
+
### WebSocket Messages (JSON)
|
|
209
|
+
|
|
210
|
+
**Client → Server:**
|
|
211
|
+
```json
|
|
212
|
+
{"type": "subscribe", "stream": "counter_updates"}
|
|
213
|
+
{"type": "unsubscribe", "stream": "counter_updates"}
|
|
214
|
+
{"type": "pong"}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Server → Client:**
|
|
218
|
+
```json
|
|
219
|
+
{"type": "subscribed", "stream": "counter_updates"}
|
|
220
|
+
{"type": "message", "stream": "counter_updates", "data": "<turbo-stream...>"}
|
|
221
|
+
{"type": "message", "stream": "progress", "data": {"status": "processing", "progress": 50}}
|
|
222
|
+
{"type": "ping"}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The `data` field can contain either:
|
|
226
|
+
- **String**: Turbo Stream HTML (automatically processed as DOM updates)
|
|
227
|
+
- **Object**: Custom JSON data (dispatched as `turbo:stream-message` event)
|
|
228
|
+
|
|
229
|
+
### Broadcast Endpoint
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
POST /_broadcast
|
|
233
|
+
Content-Type: application/json
|
|
234
|
+
|
|
235
|
+
# Turbo Stream HTML
|
|
236
|
+
{
|
|
237
|
+
"stream": "counter_updates",
|
|
238
|
+
"data": "<turbo-stream action=\"replace\" target=\"counter\">...</turbo-stream>"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Custom JSON
|
|
242
|
+
{
|
|
243
|
+
"stream": "progress_updates",
|
|
244
|
+
"data": {"status": "processing", "progress": 50, "message": "Processing..."}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## How It Works
|
|
249
|
+
|
|
250
|
+
1. **Rack Middleware**: Intercepts `/cable` requests and upgrades to WebSocket
|
|
251
|
+
2. **Stimulus Controller**: Discovers `turbo_stream_from` markers and subscribes
|
|
252
|
+
3. **Broadcast Endpoint**: Rails broadcasts via HTTP POST to `/_broadcast`
|
|
253
|
+
4. **WebSocket Distribution**: Middleware forwards updates to subscribed clients
|
|
254
|
+
|
|
255
|
+
**Critical architectural constraint:** All components (WebSocket server, Rails app, broadcast endpoint) run in the same process. This is why cross-server broadcasting isn't supported.
|
|
256
|
+
|
|
257
|
+
## Security
|
|
258
|
+
|
|
259
|
+
### Broadcast Endpoint Protection
|
|
260
|
+
|
|
261
|
+
The `/_broadcast` endpoint is **restricted to localhost only** (127.0.0.0/8 and ::1). This prevents external attackers from broadcasting arbitrary HTML to connected clients.
|
|
262
|
+
|
|
263
|
+
**Why this matters:** An unprotected broadcast endpoint would allow XSS attacks - anyone who could POST to `/_broadcast` could inject malicious HTML into user browsers.
|
|
264
|
+
|
|
265
|
+
**Why localhost-only is safe:** Since TurboCable runs in-process with your Rails app, all broadcasts originate from the same machine. External access is never needed and would indicate an attack.
|
|
266
|
+
|
|
267
|
+
**Network configuration:** Ensure your firewall/reverse proxy doesn't forward external requests to `/_broadcast`. This endpoint should never be exposed through nginx, Apache, or any proxy.
|
|
268
|
+
|
|
269
|
+
## Compatibility
|
|
270
|
+
|
|
271
|
+
- **Rails:** 7.0+ (tested with 8.0+)
|
|
272
|
+
- **Ruby:** 3.0+
|
|
273
|
+
- **Browsers:** All modern browsers with WebSocket support
|
|
274
|
+
- **Server:** Puma or any Rack server that supports `rack.hijack`
|
|
275
|
+
|
|
276
|
+
## Technical Details
|
|
277
|
+
|
|
278
|
+
### Action Cable Feature Differences
|
|
279
|
+
|
|
280
|
+
- **`stream_for` not supported** - Use `turbo_stream_from` instead
|
|
281
|
+
- **Client→Server communication** - Use standard HTTP requests (forms, fetch, Turbo Frames) instead of WebSocket channel actions
|
|
282
|
+
- **In-process WebSocket server** - Not a separate cable server; runs within Rails process
|
|
283
|
+
|
|
284
|
+
### Hybrid Async/Sync Behavior
|
|
285
|
+
|
|
286
|
+
TurboCable intelligently chooses between async and sync broadcasting:
|
|
287
|
+
|
|
288
|
+
**Methods with `_later_to` suffix** (e.g., `broadcast_replace_later_to`):
|
|
289
|
+
- ✅ **Async** - If Active Job is configured with a non-inline adapter (Solid Queue, Sidekiq, etc.), broadcasts are enqueued as jobs
|
|
290
|
+
- 🔄 **Sync fallback** - If no job backend exists, broadcasts happen synchronously via HTTP POST
|
|
291
|
+
|
|
292
|
+
**Methods without `_later_to`** (e.g., `broadcast_replace_to`):
|
|
293
|
+
- 🔄 **Always sync** - Broadcasts happen immediately, useful for callbacks like `before_destroy`
|
|
294
|
+
|
|
295
|
+
**Why hybrid?**
|
|
296
|
+
- **Zero dependencies** - Works out of the box without requiring a job backend
|
|
297
|
+
- **Performance** - Async when available prevents blocking HTTP responses
|
|
298
|
+
- **Flexibility** - Automatically adapts to your infrastructure
|
|
299
|
+
|
|
300
|
+
**Example:**
|
|
301
|
+
```ruby
|
|
302
|
+
# Development (no job backend) - synchronous
|
|
303
|
+
counter.broadcast_replace_later_to "updates" # HTTP POST happens now
|
|
304
|
+
|
|
305
|
+
# Production (with Solid Queue) - asynchronous
|
|
306
|
+
counter.broadcast_replace_later_to "updates" # Job enqueued, returns immediately
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### What IS Supported
|
|
310
|
+
|
|
311
|
+
- ✅ All Turbo Streams actions (replace, update, append, prepend, remove)
|
|
312
|
+
- ✅ Multiple concurrent connections per process
|
|
313
|
+
- ✅ Multiple streams per connection
|
|
314
|
+
- ✅ Partial rendering with locals
|
|
315
|
+
- ✅ Auto-reconnection on connection loss
|
|
316
|
+
- ✅ Thread-safe subscription management
|
|
317
|
+
|
|
318
|
+
## Development
|
|
319
|
+
|
|
320
|
+
After checking out the repo:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
bundle install
|
|
324
|
+
bundle exec rake test
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Contributing
|
|
328
|
+
|
|
329
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rubys/turbo_cable.
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
334
|
+
|
|
335
|
+
## Credits
|
|
336
|
+
|
|
337
|
+
Inspired by the memory optimization needs of multi-region Rails deployments. Built to prove that Action Cable's functionality can be achieved with minimal dependencies and maximum efficiency.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module TurboCable
|
|
2
|
+
module StreamsHelper
|
|
3
|
+
# Custom turbo_stream_from that works with our WebSocket implementation
|
|
4
|
+
# Drop-in replacement for Turbo Stream's turbo_stream_from helper
|
|
5
|
+
def turbo_stream_from(*stream_names)
|
|
6
|
+
# Create a marker element that JavaScript will find and subscribe to
|
|
7
|
+
tag.div(
|
|
8
|
+
data: {
|
|
9
|
+
turbo_stream: true,
|
|
10
|
+
streams: stream_names.join(",")
|
|
11
|
+
},
|
|
12
|
+
style: "display: none;"
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module TurboCable
|
|
2
|
+
class BroadcastJob < ApplicationJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(stream_name, action:, model_gid: nil, target: nil, partial: nil, html: nil, locals: {})
|
|
6
|
+
# Resolve model from GlobalID if provided
|
|
7
|
+
model = GlobalID::Locator.locate(model_gid) if model_gid
|
|
8
|
+
|
|
9
|
+
# Determine target
|
|
10
|
+
target ||= "#{model.class.name.underscore}_#{model.id}" if model
|
|
11
|
+
|
|
12
|
+
# Generate content HTML
|
|
13
|
+
content_html = if html
|
|
14
|
+
html
|
|
15
|
+
elsif partial
|
|
16
|
+
# Render partial with locals
|
|
17
|
+
ApplicationController.render(partial: partial, locals: locals.merge(model ? { model.class.name.underscore.to_sym => model } : {}))
|
|
18
|
+
elsif model
|
|
19
|
+
# Render default partial for this model
|
|
20
|
+
ApplicationController.render(partial: model, locals: locals)
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "Must provide html, partial, or model"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generate Turbo Stream HTML
|
|
26
|
+
turbo_stream_html = if action.to_sym == :remove
|
|
27
|
+
<<~HTML
|
|
28
|
+
<turbo-stream action="remove" target="#{target}">
|
|
29
|
+
</turbo-stream>
|
|
30
|
+
HTML
|
|
31
|
+
else
|
|
32
|
+
<<~HTML
|
|
33
|
+
<turbo-stream action="#{action}" target="#{target}">
|
|
34
|
+
<template>
|
|
35
|
+
#{content_html}
|
|
36
|
+
</template>
|
|
37
|
+
</turbo-stream>
|
|
38
|
+
HTML
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Broadcast via HTTP POST
|
|
42
|
+
broadcast_turbo_stream(stream_name, turbo_stream_html)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def broadcast_turbo_stream(stream_name, html)
|
|
48
|
+
require "net/http"
|
|
49
|
+
require "json"
|
|
50
|
+
|
|
51
|
+
# Get the actual Puma/Rails server port
|
|
52
|
+
# Priority: TURBO_CABLE_PORT > PORT > 3000
|
|
53
|
+
# Use TURBO_CABLE_PORT to override PORT when it's set to proxy/Thruster port
|
|
54
|
+
default_port = ENV["TURBO_CABLE_PORT"] || ENV["PORT"] || "3000"
|
|
55
|
+
uri = URI(ENV.fetch("TURBO_CABLE_BROADCAST_URL", "http://localhost:#{default_port}/_broadcast"))
|
|
56
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
57
|
+
http.open_timeout = 1
|
|
58
|
+
http.read_timeout = 1
|
|
59
|
+
|
|
60
|
+
request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
|
|
61
|
+
request.body = {
|
|
62
|
+
stream: stream_name,
|
|
63
|
+
data: html
|
|
64
|
+
}.to_json
|
|
65
|
+
|
|
66
|
+
http.request(request)
|
|
67
|
+
rescue => e
|
|
68
|
+
Rails.logger.error("Broadcast failed: #{e.message}") if defined?(Rails)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Turbo cable</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "turbo_cable/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module TurboCable
|
|
2
|
+
module Generators
|
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
|
5
|
+
|
|
6
|
+
def copy_javascript_controller
|
|
7
|
+
copy_file "turbo_streams_controller.js",
|
|
8
|
+
"app/javascript/controllers/turbo_streams_controller.js"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_controller_to_layout
|
|
12
|
+
layout_file = "app/views/layouts/application.html.erb"
|
|
13
|
+
|
|
14
|
+
if File.exist?(layout_file)
|
|
15
|
+
# Check if already added
|
|
16
|
+
content = File.read(layout_file)
|
|
17
|
+
unless content.include?('data-controller="turbo-streams"')
|
|
18
|
+
inject_into_file layout_file,
|
|
19
|
+
' data-controller="turbo-streams"',
|
|
20
|
+
after: "<body"
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
say "WARNING: Could not find #{layout_file}", :yellow
|
|
24
|
+
say "Please add data-controller=\"turbo-streams\" to your <body> tag manually", :yellow
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def show_readme
|
|
29
|
+
say "\n" + "=" * 70
|
|
30
|
+
say "TurboCable Installation Complete!", :green
|
|
31
|
+
say "=" * 70
|
|
32
|
+
say "\nNext steps:"
|
|
33
|
+
say " 1. Restart your Rails server"
|
|
34
|
+
say " 2. Use turbo_stream_from in your views"
|
|
35
|
+
say " 3. Use broadcast_* methods in your models"
|
|
36
|
+
say "\nExample usage:"
|
|
37
|
+
say " # In your view"
|
|
38
|
+
say ' <%= turbo_stream_from "counter_updates" %>'
|
|
39
|
+
say "\n # In your model"
|
|
40
|
+
say ' broadcast_replace_later_to "counter_updates", target: "counter"'
|
|
41
|
+
say "\nFor more information, see the README at:"
|
|
42
|
+
say " https://github.com/rubys/turbo_cable"
|
|
43
|
+
say "\n" + "=" * 70 + "\n"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|