CableReady and Rails Background Jobs in Views
Using CableReady inside of a Ruby on Rails app to dynamically update the page and view based on a Sidekiq background job’s progress.
In the world of Web Dev, a common practice, and often preferable approach for handling long-running methods, is to run the intensive action as a background job. It’s a simple and effective solution, but just like everything we web programmers face, there are pros and cons to background jobs. For example, the user won’t receive any feedback generally until this job is finished. For a particular feature I ran into, I needed to display some front-end feedback to the user and to change the page’s content contingent on the progress of this method. One traditional way to do this would be to create a dedicated route and controller#method
, while executing Ajax calls every few seconds; however there is an easier way. Considering how much of a Swiss army knife it is, CableReady is a perfect tool for this.
Before I go any further, let me clarify a bit about what CableReady really is. Here’s a quote from the official website:
CableReady is a Ruby gem that was first released in May 2017. It lets you create great real-time user experiences by triggering client-side DOM changes, events and notifications over ActionCable web sockets.
Besides the fact that it saves us from having to write too much Javascript in Rails apps 😈, the one aspect of CableReady that completely sold me is below, also from their documentation.
Unlike Ajax requests, operations are not always initiated by user activity - or even the user's browser.
What this means is CableReady can be used almost everywhere! In this circumstance, we are going to use it inside of a plain old ruby object — PORO.
Before we move on, I’d like to point out some things. CableReady is used together with StimulusReflex, or to be more precise, I use StimulusReflex with CableReady since it is a direct dependency. Because of this, CableReady can be called directly inside a Reflex class, without additional configuration. Reflex will take care of the channel CableReady will broadcast to. However, one thing to keep in mind is CableReady will broadcast to the current user when called inside a Reflex.
Let’s start with a CSV importer. A typical CSV importer in Ruby may look very similar to this:
Although this works, it’s boring, in the background, and hidden from the user’s view. They have no idea if their importing process is running or where they stand, so let’s show the user its progress in real-time. To start, I’ll include CableReady::Broadcaster
in our Importer
class.
Then we need to create an ActionCable
Channel class
…and a channel subscriber.
Now on every page load, a consumer creates a subscription to the UserChannel
. After the consumer subscribes to UserChannel
, the subscribed
method on UserChannel
is called. This will use stream_for
to subscribe to a broadcast like user:Z2lkOi8vYnJva2VyLXZpc2lvbi9Vc2VyOjpBZG1pbi8x, which is a name generated from the channel name (user
) and model instance (GlobalID of a current user). To exemplify this, in the logs we see the following:
[ActionCable] UserChannel is transmitting the subscription confirmation
[ActionCable] UserChannel is streaming from user:Z2lkOi8vYnJva2VyLXZpc2lvbi9Vc2VyOjpBZG1pbi8x
From now on we can use code like this:
cable_ready[UserChannel].remove(selector: 'some_selector').broadcast_to(current_user)
… to broadcast to the current user.
Now that we have CableReady inside our PORO and a channel set up, time to revisit our Importer
class; here, we should flesh out the method in order to keep track of how many CSV rows have already been processed.
Given that you have a div
with the selector id of processed-row-number-container
in your view, on each CSV iteration, the number will increment to reflect the counter
variable; all being updated in real-time via CableReady and the text_content
DOM manipulation (which is quite simply Javascript’s .textContent
property).
As a backend developer, this is good enough for me, and I would leave it as is! 😂 But thankfully, I have a great friend on my team who knows a thing or two about design and implemented it with a nice HTML progress bar.
This is a big improvement for the user, but how about we find out what else we can do? Say we want to update the view from a Sidekiq job.
For example, imagine a web app that displays a leaderboard of some sort. In the background, a rake job runs every few minutes that fetches the newest scores. It’s just as easy to update a view from that job using CableReady as well.
Assuming this leaderboard is accessible by all users, the leaderboard channel may look like this:
Instead of using current_user
as we did before, we use a string “scores”
. In the logs, it looks radically different from before when we used user
:
[ActionCable] LeaderboardChannel is transmitting the subscription confirmation
[ActionCable] LeaderboardChannel is streaming from leaderboard:scores
So before, when broadcasting a message to current_user
, it was meant for only one user to see that view, hence the massive string ID from users:user_global_id
. In this leaderboard example leaderboard:scores
, the broadcast will be published to all logged-in users, which is our goal here.
Just like before, a channel subscriber needs to be created, and then the only thing left is the job itself.
With merely a few lines of code, we implemented a background job that will update our view. How cool! And this is just the tip of the iceberg. As demonstrated, you can use CableReady pretty much anywhere, so you are limited only by your creativity. With CableReady, you can add an amazing amount of reactivity to your Rails monolithic app, while maintaining a concise and simple stack.
Interested in how we at eXp-Realty are embracing the Rails monolith? Benjamin Broestl (author of the aforementioned progress bar) started a series of blog posts about it.