inertia_cable 0.1.1 → 0.2.1
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 +98 -3
- data/lib/inertia_cable/broadcastable.rb +31 -0
- data/lib/inertia_cable/version.rb +1 -1
- data/lib/inertia_cable.rb +9 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 467488f15b5926bd5b36c68bf873f04d80a9d1d3196f05b8c610dd7e5bbfed1c
|
|
4
|
+
data.tar.gz: 3d26e273740a16678aea2b6491afe4af7463c7fa492974d2bdccc393787cdb09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86b16433049aeedf34d478a398531666dec745556bdc21a2090e0d0023658a11aecab505a02ca7727f9e7d60442079a47e009e78cb778c01a37462b862578fc9
|
|
7
|
+
data.tar.gz: 3001e9c9acc8cfaf12d1287caf84700bf43fb4c4a7a28cbec30a9fa4a51ffdb890da1b856859bbe6a73d1ddcf504dd5e9feb23dabda698e61647ad8c5ca46dfc
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ActionCable broadcast DSL for [Inertia.js](https://inertiajs.com/) Rails applications. Three lines of code to get real-time updates.
|
|
4
4
|
|
|
5
|
-
InertiaCable broadcasts lightweight JSON signals over ActionCable. The client receives them and calls `router.reload()` to re-fetch props through Inertia's normal HTTP flow
|
|
5
|
+
InertiaCable broadcasts lightweight JSON signals over ActionCable. The client receives them and calls `router.reload()` to re-fetch props through Inertia's normal HTTP flow — your controller stays the single source of truth. For ephemeral data like job progress or notifications, [direct messages](#direct-messages) stream data over the WebSocket without triggering a reload.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
Model save → after_commit → ActionCable broadcast (signal)
|
|
@@ -12,6 +12,8 @@ React hook subscribes → receives signal → router.reload({ only: ['messages']
|
|
|
12
12
|
Inertia HTTP request → controller re-evaluates props → React re-renders
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
> **Coming from Turbo Streams?** `broadcasts_to` replaces `broadcasts_refreshes_to`, and `broadcast_message_to` covers use cases where you'd reach for `broadcast_append_to` or `broadcast_replace_to` — but without HTML partials, since Inertia reloads props from your controller instead.
|
|
16
|
+
|
|
15
17
|
## Table of Contents
|
|
16
18
|
|
|
17
19
|
- [Installation](#installation)
|
|
@@ -19,6 +21,7 @@ Inertia HTTP request → controller re-evaluates props → React re-renders
|
|
|
19
21
|
- [Model DSL](#model-dsl)
|
|
20
22
|
- [Controller Helper](#controller-helper)
|
|
21
23
|
- [React Hook](#react-hook)
|
|
24
|
+
- [Direct Messages](#direct-messages)
|
|
22
25
|
- [Suppressing Broadcasts](#suppressing-broadcasts)
|
|
23
26
|
- [Server-Side Debounce](#server-side-debounce)
|
|
24
27
|
- [Testing](#testing)
|
|
@@ -181,11 +184,16 @@ post.broadcast_refresh_later_to(board, debounce: 2.0)
|
|
|
181
184
|
|
|
182
185
|
# With inline condition (block — skips broadcast if falsy)
|
|
183
186
|
post.broadcast_refresh_to(board) { published? }
|
|
187
|
+
|
|
188
|
+
# Direct messages (ephemeral data, no prop reload)
|
|
189
|
+
post.broadcast_message_to(board, data: { progress: 50 })
|
|
190
|
+
post.broadcast_message_later_to(board, data: { progress: 50 })
|
|
191
|
+
post.broadcast_message_to(board, data: { progress: 50 }) { running? }
|
|
184
192
|
```
|
|
185
193
|
|
|
186
194
|
### Broadcast payload
|
|
187
195
|
|
|
188
|
-
|
|
196
|
+
Refresh broadcasts send this JSON:
|
|
189
197
|
|
|
190
198
|
```json
|
|
191
199
|
{
|
|
@@ -200,6 +208,17 @@ Every broadcast sends this JSON:
|
|
|
200
208
|
|
|
201
209
|
The `action` field is `"create"`, `"update"`, or `"destroy"`. The `extra` field contains data from the `extra:` option (empty object if not set).
|
|
202
210
|
|
|
211
|
+
Message broadcasts send a minimal payload:
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"type": "message",
|
|
216
|
+
"data": { "progress": 50, "total": 200 }
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Messages are ephemeral — no `model`, `id`, `action`, or `timestamp` fields.
|
|
221
|
+
|
|
203
222
|
---
|
|
204
223
|
|
|
205
224
|
## Controller Helper
|
|
@@ -234,6 +253,9 @@ const { connected } = useInertiaCable(cable_stream, {
|
|
|
234
253
|
console.log(`${data.model} #${data.id} was ${data.action}`)
|
|
235
254
|
if (data.extra?.priority === 'high') toast.warn('Priority update!')
|
|
236
255
|
},
|
|
256
|
+
onMessage: (data) => { // receive direct messages (no reload)
|
|
257
|
+
setProgress(data.progress)
|
|
258
|
+
},
|
|
237
259
|
onConnected: () => {}, // subscription connected
|
|
238
260
|
onDisconnected: () => {}, // connection dropped
|
|
239
261
|
debounce: 200, // client-side debounce in ms (default: 100)
|
|
@@ -246,6 +268,7 @@ const { connected } = useInertiaCable(cable_stream, {
|
|
|
246
268
|
| `only` | `string[]` | — | Only reload these props |
|
|
247
269
|
| `except` | `string[]` | — | Reload all props except these |
|
|
248
270
|
| `onRefresh` | `(data) => void` | — | Callback before each reload |
|
|
271
|
+
| `onMessage` | `(data) => void` | — | Receive direct message data (no reload) |
|
|
249
272
|
| `onConnected` | `() => void` | — | Called when subscription connects |
|
|
250
273
|
| `onDisconnected` | `() => void` | — | Called when connection drops |
|
|
251
274
|
| `debounce` | `number` | `100` | Debounce delay in ms |
|
|
@@ -297,7 +320,79 @@ createInertiaApp({
|
|
|
297
320
|
|
|
298
321
|
`getConsumer()` and `setConsumer()` are also exported for low-level access to the ActionCable consumer singleton.
|
|
299
322
|
|
|
300
|
-
TypeScript types (`RefreshPayload`, `UseInertiaCableOptions`, `UseInertiaCableReturn`, `InertiaCableProviderProps`) are exported from `@inertia-cable/react`.
|
|
323
|
+
TypeScript types (`RefreshPayload`, `MessagePayload`, `CablePayload`, `UseInertiaCableOptions`, `UseInertiaCableReturn`, `InertiaCableProviderProps`) are exported from `@inertia-cable/react`. `CablePayload` is a discriminated union of `RefreshPayload | MessagePayload` for type-safe handling of raw payloads.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Direct Messages
|
|
328
|
+
|
|
329
|
+
Push ephemeral data directly into React state over the same signed stream — no prop reload, no extra hook.
|
|
330
|
+
|
|
331
|
+
### Job progress example
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
# app/jobs/csv_import_job.rb
|
|
335
|
+
class CsvImportJob < ApplicationJob
|
|
336
|
+
def perform(import)
|
|
337
|
+
rows = CSV.read(import.file.path)
|
|
338
|
+
rows.each_with_index do |row, i|
|
|
339
|
+
process_row(row)
|
|
340
|
+
import.broadcast_message_to(import.user, data: { progress: i + 1, total: rows.size })
|
|
341
|
+
end
|
|
342
|
+
import.broadcast_refresh_to(import.user) # final reload with completed data
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
import { useState } from 'react'
|
|
349
|
+
import { useInertiaCable } from '@inertia-cable/react'
|
|
350
|
+
|
|
351
|
+
export default function ImportShow({ import_record, cable_stream }) {
|
|
352
|
+
const [progress, setProgress] = useState<{ progress: number; total: number } | null>(null)
|
|
353
|
+
|
|
354
|
+
useInertiaCable(cable_stream, {
|
|
355
|
+
only: ['import_record'],
|
|
356
|
+
onMessage: (data) => setProgress({ progress: data.progress as number, total: data.total as number }),
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div>
|
|
361
|
+
<h1>Import #{import_record.id}</h1>
|
|
362
|
+
{progress && <p>Processing {progress.progress} / {progress.total}</p>}
|
|
363
|
+
</div>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Usage patterns
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
// Prop reload only (unchanged)
|
|
372
|
+
useInertiaCable(stream, { only: ['messages'] })
|
|
373
|
+
|
|
374
|
+
// Direct data only (no reload)
|
|
375
|
+
useInertiaCable(stream, {
|
|
376
|
+
onMessage: (data) => setProgress(data.progress)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// Both — progress during job, final reload on completion
|
|
380
|
+
useInertiaCable(stream, {
|
|
381
|
+
only: ['imports'],
|
|
382
|
+
onMessage: (data) => setProgress(data.progress)
|
|
383
|
+
})
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Broadcasting without a model instance
|
|
387
|
+
|
|
388
|
+
Use `InertiaCable.broadcast_message_to` from anywhere — jobs, services, controllers — without needing a model instance:
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
InertiaCable.broadcast_message_to("dashboard", data: { alert: "Deployment complete" })
|
|
392
|
+
InertiaCable.broadcast_message_to(user, :notifications, data: { count: 5 })
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Messages are delivered immediately with no debouncing. Each `broadcast_message_to` call triggers exactly one `onMessage` callback.
|
|
301
396
|
|
|
302
397
|
---
|
|
303
398
|
|
|
@@ -128,6 +128,33 @@ module InertiaCable
|
|
|
128
128
|
broadcast_refresh_later_to(self.class.model_name.plural, &block)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# Broadcast a direct message synchronously to explicit stream(s).
|
|
132
|
+
#
|
|
133
|
+
# post.broadcast_message_to(board, data: { progress: 50 })
|
|
134
|
+
# post.broadcast_message_to(board, :posts, data: { progress: 50 })
|
|
135
|
+
# post.broadcast_message_to(board, data: { progress: 50 }) { running? }
|
|
136
|
+
#
|
|
137
|
+
def broadcast_message_to(*streamables, data:, &block)
|
|
138
|
+
return if self.class.suppressed_inertia_cable_broadcasts?
|
|
139
|
+
return if block && !instance_exec(&block)
|
|
140
|
+
|
|
141
|
+
InertiaCable.broadcast(streamables, message_payload(data: data))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Broadcast a direct message asynchronously to explicit stream(s).
|
|
145
|
+
#
|
|
146
|
+
# post.broadcast_message_later_to(board, data: { progress: 50 })
|
|
147
|
+
# post.broadcast_message_later_to(board, :posts, data: { progress: 50 })
|
|
148
|
+
# post.broadcast_message_later_to(board, data: { progress: 50 }) { running? }
|
|
149
|
+
#
|
|
150
|
+
def broadcast_message_later_to(*streamables, data:, &block)
|
|
151
|
+
return if self.class.suppressed_inertia_cable_broadcasts?
|
|
152
|
+
return if block && !instance_exec(&block)
|
|
153
|
+
|
|
154
|
+
resolved = InertiaCable::Streams::StreamName.stream_name_from(streamables)
|
|
155
|
+
InertiaCable::BroadcastJob.perform_later(resolved, message_payload(data: data))
|
|
156
|
+
end
|
|
157
|
+
|
|
131
158
|
private
|
|
132
159
|
|
|
133
160
|
def resolve_stream(stream)
|
|
@@ -168,5 +195,9 @@ module InertiaCable
|
|
|
168
195
|
payload[:extra] = extra if extra.present?
|
|
169
196
|
payload
|
|
170
197
|
end
|
|
198
|
+
|
|
199
|
+
def message_payload(data:)
|
|
200
|
+
{ type: "message", data: data }
|
|
201
|
+
end
|
|
171
202
|
end
|
|
172
203
|
end
|
data/lib/inertia_cable.rb
CHANGED
|
@@ -28,6 +28,15 @@ module InertiaCable
|
|
|
28
28
|
ActionCable.server.broadcast(resolved, payload)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Broadcast a direct message without a model instance.
|
|
32
|
+
#
|
|
33
|
+
# InertiaCable.broadcast_message_to("dashboard", data: { alert: "done" })
|
|
34
|
+
# InertiaCable.broadcast_message_to(user, :notifications, data: { count: 5 })
|
|
35
|
+
#
|
|
36
|
+
def self.broadcast_message_to(*streamables, data:)
|
|
37
|
+
broadcast(streamables, { type: "message", data: data })
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
def self.suppressing_broadcasts(&block)
|
|
32
41
|
InertiaCable::Suppressor.suppressing(&block)
|
|
33
42
|
end
|