inertia_cable 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25d0d8cfcadeb38db163745b520998196f61fa48865253227abe0e3ee22d5686
|
|
4
|
+
data.tar.gz: 7f7f8affa983fa0c0a1543d1118cf67eb3abd609606b1c16fac076b8e5767ad4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 504d3453823e7cf2887da87a0aa9d56839292e2d8fbe2504e7ca76648d0addafef076e2c655eb3d4a1a88d6f688c7709b60237c3c9ed320676dd9fc7a8544a9f
|
|
7
|
+
data.tar.gz: aba9941405aea7ca05100162c5f7555c0257a9aee672662614167b30d4d75b3e32dd418f8e24d1ce8d014e2e81c4638d3e7d2ab172714d14cc677b51f3bac3a3
|
data/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Inertia HTTP request → controller re-evaluates props → React re-renders
|
|
|
19
19
|
- [Model DSL](#model-dsl)
|
|
20
20
|
- [Controller Helper](#controller-helper)
|
|
21
21
|
- [React Hook](#react-hook)
|
|
22
|
+
- [Direct Messages](#direct-messages)
|
|
22
23
|
- [Suppressing Broadcasts](#suppressing-broadcasts)
|
|
23
24
|
- [Server-Side Debounce](#server-side-debounce)
|
|
24
25
|
- [Testing](#testing)
|
|
@@ -181,11 +182,16 @@ post.broadcast_refresh_later_to(board, debounce: 2.0)
|
|
|
181
182
|
|
|
182
183
|
# With inline condition (block — skips broadcast if falsy)
|
|
183
184
|
post.broadcast_refresh_to(board) { published? }
|
|
185
|
+
|
|
186
|
+
# Direct messages (ephemeral data, no prop reload)
|
|
187
|
+
post.broadcast_message_to(board, data: { progress: 50 })
|
|
188
|
+
post.broadcast_message_later_to(board, data: { progress: 50 })
|
|
189
|
+
post.broadcast_message_to(board, data: { progress: 50 }) { running? }
|
|
184
190
|
```
|
|
185
191
|
|
|
186
192
|
### Broadcast payload
|
|
187
193
|
|
|
188
|
-
|
|
194
|
+
Refresh broadcasts send this JSON:
|
|
189
195
|
|
|
190
196
|
```json
|
|
191
197
|
{
|
|
@@ -200,6 +206,17 @@ Every broadcast sends this JSON:
|
|
|
200
206
|
|
|
201
207
|
The `action` field is `"create"`, `"update"`, or `"destroy"`. The `extra` field contains data from the `extra:` option (empty object if not set).
|
|
202
208
|
|
|
209
|
+
Message broadcasts send a minimal payload:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"type": "message",
|
|
214
|
+
"data": { "progress": 50, "total": 200 }
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Messages are ephemeral — no `model`, `id`, `action`, or `timestamp` fields.
|
|
219
|
+
|
|
203
220
|
---
|
|
204
221
|
|
|
205
222
|
## Controller Helper
|
|
@@ -234,6 +251,9 @@ const { connected } = useInertiaCable(cable_stream, {
|
|
|
234
251
|
console.log(`${data.model} #${data.id} was ${data.action}`)
|
|
235
252
|
if (data.extra?.priority === 'high') toast.warn('Priority update!')
|
|
236
253
|
},
|
|
254
|
+
onMessage: (data) => { // receive direct messages (no reload)
|
|
255
|
+
setProgress(data.progress)
|
|
256
|
+
},
|
|
237
257
|
onConnected: () => {}, // subscription connected
|
|
238
258
|
onDisconnected: () => {}, // connection dropped
|
|
239
259
|
debounce: 200, // client-side debounce in ms (default: 100)
|
|
@@ -246,6 +266,7 @@ const { connected } = useInertiaCable(cable_stream, {
|
|
|
246
266
|
| `only` | `string[]` | — | Only reload these props |
|
|
247
267
|
| `except` | `string[]` | — | Reload all props except these |
|
|
248
268
|
| `onRefresh` | `(data) => void` | — | Callback before each reload |
|
|
269
|
+
| `onMessage` | `(data) => void` | — | Receive direct message data (no reload) |
|
|
249
270
|
| `onConnected` | `() => void` | — | Called when subscription connects |
|
|
250
271
|
| `onDisconnected` | `() => void` | — | Called when connection drops |
|
|
251
272
|
| `debounce` | `number` | `100` | Debounce delay in ms |
|
|
@@ -297,7 +318,70 @@ createInertiaApp({
|
|
|
297
318
|
|
|
298
319
|
`getConsumer()` and `setConsumer()` are also exported for low-level access to the ActionCable consumer singleton.
|
|
299
320
|
|
|
300
|
-
TypeScript types (`RefreshPayload`, `UseInertiaCableOptions`, `UseInertiaCableReturn`, `InertiaCableProviderProps`) are exported from `@inertia-cable/react`.
|
|
321
|
+
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.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Direct Messages
|
|
326
|
+
|
|
327
|
+
Push ephemeral data directly into React state over the same signed stream — no prop reload, no extra hook.
|
|
328
|
+
|
|
329
|
+
### Job progress example
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# app/jobs/csv_import_job.rb
|
|
333
|
+
class CsvImportJob < ApplicationJob
|
|
334
|
+
def perform(import)
|
|
335
|
+
rows = CSV.read(import.file.path)
|
|
336
|
+
rows.each_with_index do |row, i|
|
|
337
|
+
process_row(row)
|
|
338
|
+
import.broadcast_message_to(import.user, data: { progress: i + 1, total: rows.size })
|
|
339
|
+
end
|
|
340
|
+
import.broadcast_refresh_to(import.user) # final reload with completed data
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import { useState } from 'react'
|
|
347
|
+
import { useInertiaCable } from '@inertia-cable/react'
|
|
348
|
+
|
|
349
|
+
export default function ImportShow({ import_record, cable_stream }) {
|
|
350
|
+
const [progress, setProgress] = useState<{ progress: number; total: number } | null>(null)
|
|
351
|
+
|
|
352
|
+
useInertiaCable(cable_stream, {
|
|
353
|
+
only: ['import_record'],
|
|
354
|
+
onMessage: (data) => setProgress({ progress: data.progress as number, total: data.total as number }),
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<div>
|
|
359
|
+
<h1>Import #{import_record.id}</h1>
|
|
360
|
+
{progress && <p>Processing {progress.progress} / {progress.total}</p>}
|
|
361
|
+
</div>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Usage patterns
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
// Prop reload only (unchanged)
|
|
370
|
+
useInertiaCable(stream, { only: ['messages'] })
|
|
371
|
+
|
|
372
|
+
// Direct data only (no reload)
|
|
373
|
+
useInertiaCable(stream, {
|
|
374
|
+
onMessage: (data) => setProgress(data.progress)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// Both — progress during job, final reload on completion
|
|
378
|
+
useInertiaCable(stream, {
|
|
379
|
+
only: ['imports'],
|
|
380
|
+
onMessage: (data) => setProgress(data.progress)
|
|
381
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Messages are delivered immediately with no debouncing. Each `broadcast_message_to` call triggers exactly one `onMessage` callback.
|
|
301
385
|
|
|
302
386
|
---
|
|
303
387
|
|
|
@@ -1,40 +1,105 @@
|
|
|
1
1
|
module InertiaCable
|
|
2
2
|
module Generators
|
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
|
4
|
-
source_root File.expand_path("templates", __dir__)
|
|
5
|
-
|
|
6
4
|
desc "Install InertiaCable into your Rails application"
|
|
7
5
|
|
|
8
|
-
def
|
|
9
|
-
|
|
6
|
+
def patch_inertia_entrypoint
|
|
7
|
+
entrypoint = detect_entrypoint
|
|
8
|
+
unless entrypoint
|
|
9
|
+
say "Could not find Inertia entrypoint — you'll need to add InertiaCableProvider manually.", :yellow
|
|
10
|
+
return
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
say "Patching #{entrypoint}...", :green
|
|
14
|
+
|
|
15
|
+
# Add import after the createInertiaApp import
|
|
16
|
+
if File.read(entrypoint).include?("@inertia-cable/react")
|
|
17
|
+
say " Import already present, skipping", :yellow
|
|
18
|
+
else
|
|
19
|
+
inject_into_file entrypoint, after: /^import \{ createInertiaApp \} from .+\n/ do
|
|
20
|
+
"import { InertiaCableProvider } from '@inertia-cable/react'\n"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
content = File.read(entrypoint)
|
|
25
|
+
|
|
26
|
+
if content.include?("InertiaCableProvider")
|
|
27
|
+
say " InertiaCableProvider already present, skipping", :yellow
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Pattern 1: createElement style
|
|
32
|
+
# createRoot(el).render(createElement(App, props))
|
|
33
|
+
if content.match?(/createRoot\(el\)\.render\(\s*createElement\(App,\s*props\)\s*\)/)
|
|
34
|
+
gsub_file entrypoint,
|
|
35
|
+
/createRoot\(el\)\.render\(\s*createElement\(App,\s*props\)\s*\)/,
|
|
36
|
+
"createRoot(el).render(\n createElement(InertiaCableProvider, null, createElement(App, props)),\n )"
|
|
37
|
+
say " Wrapped render in InertiaCableProvider (createElement style)", :green
|
|
38
|
+
|
|
39
|
+
# Pattern 2: JSX with StrictMode
|
|
40
|
+
# createRoot(el).render(<StrictMode><App {...props} /></StrictMode>)
|
|
41
|
+
elsif content.match?(/createRoot\(el\)\.render\(\s*\n?\s*<StrictMode>\s*\n?\s*<App\s+\{\.\.\.props\}\s*\/>\s*\n?\s*<\/StrictMode>/)
|
|
42
|
+
gsub_file entrypoint,
|
|
43
|
+
/<StrictMode>\s*\n?\s*<App\s+\{\.\.\.props\}\s*\/>\s*\n?\s*<\/StrictMode>/,
|
|
44
|
+
"<StrictMode>\n <InertiaCableProvider>\n <App {...props} />\n </InertiaCableProvider>\n </StrictMode>"
|
|
45
|
+
say " Wrapped render in InertiaCableProvider (JSX + StrictMode style)", :green
|
|
46
|
+
|
|
47
|
+
# Pattern 3: JSX without StrictMode
|
|
48
|
+
# createRoot(el).render(<App {...props} />)
|
|
49
|
+
elsif content.match?(/createRoot\(el\)\.render\(\s*\n?\s*<App\s+\{\.\.\.props\}\s*\/>/)
|
|
50
|
+
gsub_file entrypoint,
|
|
51
|
+
/<App\s+\{\.\.\.props\}\s*\/>/,
|
|
52
|
+
"<InertiaCableProvider>\n <App {...props} />\n </InertiaCableProvider>"
|
|
53
|
+
say " Wrapped render in InertiaCableProvider (JSX style)", :green
|
|
54
|
+
|
|
55
|
+
else
|
|
56
|
+
say " Could not detect render pattern — add InertiaCableProvider manually.", :yellow
|
|
57
|
+
say " See: https://github.com/cole-robertson/inertia_cable#inertiaCableProvider"
|
|
58
|
+
end
|
|
10
59
|
end
|
|
11
60
|
|
|
12
|
-
def
|
|
61
|
+
def show_next_steps
|
|
13
62
|
say ""
|
|
14
63
|
say "InertiaCable installed!", :green
|
|
15
64
|
say ""
|
|
16
65
|
say "Next steps:"
|
|
17
|
-
say " 1. Add the npm package to your frontend:"
|
|
18
|
-
say " npm install @inertia-cable/react @rails/actioncable"
|
|
19
66
|
say ""
|
|
20
|
-
say "
|
|
21
|
-
say "
|
|
67
|
+
say " 1. Install the npm package:"
|
|
68
|
+
say " npm install @inertia-cable/react @rails/actioncable"
|
|
22
69
|
say ""
|
|
23
|
-
say "
|
|
70
|
+
say " 2. Add broadcasts to your models:"
|
|
24
71
|
say " class Message < ApplicationRecord"
|
|
25
72
|
say " belongs_to :chat"
|
|
26
|
-
say "
|
|
73
|
+
say " broadcasts_to :chat"
|
|
27
74
|
say " end"
|
|
28
75
|
say ""
|
|
29
|
-
say "
|
|
76
|
+
say " 3. Pass cable_stream prop from your controller:"
|
|
30
77
|
say " render inertia: 'Chats/Show', props: {"
|
|
31
78
|
say " cable_stream: inertia_cable_stream(chat)"
|
|
32
79
|
say " }"
|
|
33
80
|
say ""
|
|
34
|
-
say "
|
|
35
|
-
say "
|
|
81
|
+
say " 4. Use the hook in your React component:"
|
|
82
|
+
say " import { useInertiaCable } from '@inertia-cable/react'"
|
|
83
|
+
say ""
|
|
84
|
+
say " const { connected } = useInertiaCable(cable_stream, { only: ['messages'] })"
|
|
36
85
|
say ""
|
|
37
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def detect_entrypoint
|
|
91
|
+
candidates = %w[
|
|
92
|
+
app/frontend/entrypoints/inertia.ts
|
|
93
|
+
app/frontend/entrypoints/inertia.tsx
|
|
94
|
+
app/frontend/entrypoints/inertia.js
|
|
95
|
+
app/frontend/entrypoints/inertia.jsx
|
|
96
|
+
app/javascript/entrypoints/inertia.ts
|
|
97
|
+
app/javascript/entrypoints/inertia.tsx
|
|
98
|
+
app/javascript/entrypoints/inertia.js
|
|
99
|
+
app/javascript/entrypoints/inertia.jsx
|
|
100
|
+
]
|
|
101
|
+
candidates.find { |f| File.exist?(Rails.root.join(f)) }
|
|
102
|
+
end
|
|
38
103
|
end
|
|
39
104
|
end
|
|
40
105
|
end
|
|
@@ -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
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inertia_cable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cole Robertson
|
|
@@ -75,7 +75,6 @@ files:
|
|
|
75
75
|
- README.md
|
|
76
76
|
- app/channels/inertia_cable/stream_channel.rb
|
|
77
77
|
- lib/generators/inertia_cable/install/install_generator.rb
|
|
78
|
-
- lib/generators/inertia_cable/install/templates/cable_setup.ts
|
|
79
78
|
- lib/inertia_cable.rb
|
|
80
79
|
- lib/inertia_cable/broadcast_job.rb
|
|
81
80
|
- lib/inertia_cable/broadcastable.rb
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// InertiaCable - ActionCable setup for Inertia.js
|
|
2
|
-
//
|
|
3
|
-
// This file is auto-generated by `rails g inertia_cable:install`.
|
|
4
|
-
// You can customize the cable URL below if needed.
|
|
5
|
-
|
|
6
|
-
export { useInertiaCable } from '@inertia-cable/react'
|
|
7
|
-
export { InertiaCableProvider } from '@inertia-cable/react'
|