Google’s Firebase is a database tool that (among many other things) pushes updated data to all instances of a browser. I think that is pretty nifty. I also think that it’s a bit unreasonable to restructure an existing Rails app’s entire Postgres situation just for this one feature. For this reason, I started to explore Rails’ Action Cable.

The Rails docs docs say:

Action Cable seamlessly integrates WebSockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being performant and scalable. It’s a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice.

As a personal challenge, I built an app that tracks the status of vehicles on a car lot. It pushes updates to users’ browsers via Action Cable when the status_id field of a car changes. It works both for browser-initiated and console-initiated updates. Normally in an app, the browser-initiated updates would suffice, but I wanted to go the extra mile. In the case of ETL-style database updates, there wouldn’t be a user sending changes from the DOM to the controller. I needed my app to be able to response with Action Cable even if the database were being updated in some other way.

The Rules for My Challenge

  • When a DOM user updates a car’s status via the browser, they get the normal Rails notice and all other users get a jQuery alert via Action Cable.
  • If an update happens via the console, all users get an alert via Action Cable.
  • Only send an alert when the car.status_id field is updated
  • Do not send an alert when a Car record is created

The App Stats

  • Ruby 2.5.0
  • Rails ~> 5.0.1
  • Postgres ~> 0.18
  • Redis 3.3.1

How it Works

Instead of giving details of a full Action Cable implementation, I’ll just hit the highlights needed to make both the DOM/controller-driven and console-driven approaches work in the same app. If you’d like a full tutorial on implementing Action Cable, here are 2 suggestions:

The Action Cable Channel

Action Cable needs a channel for objects to subscribe to, so we set up a status_notifications_channel for our specific car status-related notifications.

# app/channels/status_notifications_channel.rb

class StatusNotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "status_notifications_channel"
  end
end

The Cars Controller

At some point, we need to initiate a broadcast and it seems like the cars_controller would be a good place for that. But knowing when and what to broadcast is not the cars_controller’s job. All it cares about is passing data between the car views and data source. So we give it an object that already knows the broadcasting info, which lets the controller stick to its normal duties.

# app/controllers/cars_controller.rb

def update
  # Pass the @car and its updated params to the `CarWithBroadcast` class
  @car_with_broadcast = CarWithBroadcast.new(@car, car_params)

  respond_to do |format|
    # And use that class's object just as we would have used the @car object under default circumstances
    if @car_with_broadcast.save
      format.html { redirect_to cars_url, notice: "The #{@car_with_broadcast.car.display} was successfully updated." }
      format.json { render cars_url, status: :ok, location: @car_with_broadcast.car }
    else
      format.html { render :edit }
      format.json { render json: @car_with_broadcast.car.errors, status: :unprocessable_entity }
    end
  end
end

The Broadcasting Class

The CarWithBroadcast class holds the what/when logic of broadcasting a car’s status. So we give it a @car and let it initiate a broadcast to the status_notifications_channel.

# app/models/car_with_broadcast.rb

class CarWithBroadcast
  attr_reader :car

  def initialize(car, params)
    @car = car
    @car.assign_attributes(params)
    @status_changed = @car.status_id_changed?
  end

  def save
    if car.valid?
      car.save
      # Here we broadcast a message to the `status_notifications_channel` via ActionCable's methods
      ActionCable.server.broadcast('status_notifications_channel', car.to_broadcast) if @status_changed
      true
    else
      false
    end
  end
end

The Car class knows how it wants to talk about itself in a broadcast. Hence the car.to_broadcast above.

# app/models/car.rb

class Car < ApplicationRecord
  ...
  def to_broadcast
    {
      status: status.with_id,
      car_id: id,
      message: "The #{display} is now #{status.display}."
    }
  end

  def display
    "#{color.display} #{make} #{model}"
  end
end

The Alert Coffee Script

When the status_notifications_channel receives the broadcast data, we use Coffee Script and jQuery to insert the HTML for the alert message.

# app/assets/javascripts/channels/status_notifications.coffee

App.status_notifications = App.cable.subscriptions.create "StatusNotificationsChannel",
  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    $('#messages').html("<p class='alert'>#{data.message} <span class='dismiss'>X</span></p>")
    $("##{data.car_id}").text("#{data.status}")
    $("##{data.car_id}").parent().addClass('highlight')

    $('.dismiss').on 'click', ->
      $('.alert').remove()
      $("##{data.car_id}").parent().removeClass('highlight')

Sending Alerts

From the DOM

  • Go to localhost:3000 in 2 browser windows, the second one as an incognito window.
  • Edit & Save a car via the web interface.
  • See the normal notice in User 1’s browser and the alert message in User 2’s browser. Clear the alerts.

From the Console

  • Update a car’s status in the console (as below) and see the both users’ browsers receive the alert.
# rails console

CarWithBroadcast.new(Car.first, {status_id: Status.last.id}).save

Why Didn’t You…?

So why not use an after_update callback in the Car model instead of creating the CarWithBroadcast class? With a model callback, we’re inextricably tied to having a broadcast happen every time a car object is updated. The CarWithBroadcast class gives us the flexibility to choose where and when we want to broadcast.

For example, updating a car in the console like this does not produce the alert,

# rails console

Car.first.update!(status_id: 2)

and that makes for a better user experience if you need to make changes to the production database and you don’t need users to witness the whole process. If we had used after_update the broadcast would have been sent – and possibly without our realizing it.

The CarWithBroadcast class is not a perfect solution. It feels a little clunky and the naming seems a touch off. But it’s the solution for right now and when the code grows, a better name and purpose may reveal itself.