Skip to content
Episode 4 4:15

Rails Turbo Streams from a Background Job (No JavaScript)

Push a change from a background job straight into an open page with Turbo Streams and Solid Cable, no JavaScript needed. And see why the async adapter never works for that.

Shownotes

After this episode the viewer can push a change from a background job straight into an open page with Turbo Streams and Solid Cable, and knows why the async adapter never works for that.

Prerequisites: demo-shop on episode-004-before, Rails 8.1, episode 003 (Solid Queue)

Not covered: tests, authorisation of streams, Mission Control

Source: github.com/webmatze/demo-shop (branches episode-004-before / episode-004-after)

Chapters

  • 0:00 The problem
  • 0:43 The solution at a glance
  • 1:01 Setup
  • 1:37 Building it
  • 2:36 Refactoring
  • 3:10 Gotchas
  • 3:29 Things to remember
  • 3:54 Wrap-up
Transcript

The problem

Every product carries a catalog status now. We add a Ruby Notebook, nine euros, category Stickers. The list comes back and the new product says pending. We wait. Still pending. And pending. Only when we reload does it say synced, with a timestamp from several seconds ago. The server knew, the page did not. The data was there all along. The catalog log shows the sync, and the product carries the timestamp. Nothing was missing except a way to tell the browser. That is what we build in this episode.

The solution at a glance

The tool is Turbo Streams. The page subscribes to a stream, the server sends small fragments of HTML into it, and Turbo patches them into the DOM. No JSON, no client side rendering, and the same partial we already use for the product card.

Setup

First the part that costs everyone an evening. In development Action Cable uses the async adapter, and async means inside this process. Our job runs in a different process, so its broadcast would never reach the browser. We switch development to Solid Cable, exactly like production. Solid Cable wants its own database, the same way Solid Queue did last episode. Three lines in the development section. Db prepare creates it. Three SQLite files now: the app, the queue, and the cable.

Building it

Now the page has to say what it listens to. Turbo stream from products, one line above the list. From here on the browser holds a web socket and waits for fragments on the products stream. And the model has to send them. After a create we prepend the product to the list, after an update we replace its card. Both render the same partial as the index, so the markup can never drift apart. The same product, one more time: Ruby Poster, nine euros, Stickers. The list shows pending, and we do nothing at all. No reload, no click. A few seconds later the card changes by itself: synced, with the time. That is the job talking to an open page. In the log you can see what went over the wire: a turbo stream tag with an action and a target, and the rendered card inside it. That is the whole protocol.

Refactoring

This works, but the model now has two almost identical callbacks, and destroy is still missing. Turbo Rails has a macro for exactly this. Broadcasts to, with the stream name, and inserts by prepend. One line covers create, update and destroy. Same test, one more product: Ruby Cap. Pending, and again the card updates on its own. One line instead of two callbacks, and deleting a product would now remove its card everywhere too.

Gotchas

One gotcha worth a minute. Broadcasts hang on callbacks, and update all skips callbacks. We count the messages, run an update all over every product, and count again. Same number. Nothing was broadcast, and every open page still shows the old prices.

Things to remember

A few more things. The async adapter only reaches the process it runs in, which is never the worker. Update all and insert all skip the callbacks and broadcast nothing. Rendering happens outside a request, so link helpers need default url options. And a stream name is public: anyone who can guess it can listen, so scope it to a record or a user.

Wrap-up

That is Turbo Streams: one line in the view, one line in the model, and a cable that leaves the process. The code is on GitHub, branches before and after. Next episode we leave Rails for a moment and look at Jev: a model that answers with a typed value and a confidence score instead of text, and where that beats a prompt.