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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e8633ccfd90aba7b7ae8a00203730d29755a65c1fc2f825ad9bde1225445387
4
- data.tar.gz: 43f041337383fd933d2e7a6b2b9bafa17e8876f22b0bccc9a0b48ffd3b8bd007
3
+ metadata.gz: 467488f15b5926bd5b36c68bf873f04d80a9d1d3196f05b8c610dd7e5bbfed1c
4
+ data.tar.gz: 3d26e273740a16678aea2b6491afe4af7463c7fa492974d2bdccc393787cdb09
5
5
  SHA512:
6
- metadata.gz: ae7f293c9a1988f85bb2589119dd9cca2752ad4a81ebabc4c3bc2e656e6ee74d6808cc1068c7c026d8a9fa3aa47b95f55cf49e31681a4612b485d2d6633988f4
7
- data.tar.gz: 239981663473b24453c8029787f2f869e9f39da6af222943b36adc1d6d58084d8c7f17fa5e5ca153e2635efe496d1c7415e2b8bdc25e59db07b7475d857d35be
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. No data travels over the WebSocket — your controller stays the single source of truth.
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
- Every broadcast sends this JSON:
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
@@ -1,3 +1,3 @@
1
1
  module InertiaCable
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.1"
3
3
  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
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.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cole Robertson