Finally Trying ActionCable

Hired is a predominately Rails based application – the “majestic monolith” as DHH would call it.  It’s five years old, but we take staying up to date very seriously. Up to date software makes the Hired app faster and more secure. We updated our application to Rails 5 earlier this year, but haven’t gotten around to taking advantage of many of the new features.  The biggest addition in Rails 5 was integrated WebSockets via ActionCable, so given that it had been 15 months or so since the initial release, I decided it was probably time to check out ActionCable and see if I could quickly hack together a new feature using it.

What are WebSockets?

For those new to WebSockets, a WebSocket is a communication protocol that allows for back and forth communication over a single TCP connection.  Before WebSockets, you needed to continuously poll the server to see if there was any new information available for the user.  This generated unnecessary constant requests to your app and realistically only allowed for one-way communication.

Around 2010, the WebSocket spec was published which defined an API for creating persistent connections to a server where the client and/or server could sent data at any time.  Rails 5 added a new framework called ActionCable that provides developers a client-side Javascript framework and server-side Ruby framework to make writing real-time features in Rails easy and fun.  So let’s take a look at how it works!

ActionCable in Action

So first off what should we build?  For those who haven’t used Hired yet, we have two types of users: Employers and Candidates.  Candidates apply to the platform and then choose when they want to go live and start accepting interview requests from Employers.  The Employers get matched to Candidates and can search and find Candidates they want to request an interview with.  There are a few views we work with: a search style list interface showing matching Candidates, and the individual profile pages for each Candidate.  With that in mind, let’s build something super simple into the interface: “online now” indicators so Employers can see when Candidates they’re viewing are online.

Generating Your First Channel

First off, we’ll assume you already have a Rails 5 project you’re working with, so I won’t go into installing or starting a project.  To get started with ActionCable, we can use the Rails generator to get us moving:

`$ > rails generate channel OnlineIndicator`

This will create some files in your project:

create app/channels/
create app/assets/javascripts/cable.js
create app/assets/javascripts/channels/online_indicator.coffee
create app/channels/application_cable/channel.rb
create app/channels/application_cable/connection.rb

Let’s have a look here and see what we get:

// app/assets/javascripts/cable.js

//...
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
 this.App || (this.App = {});

 App.cable = ActionCable.createConsumer();

}).call(this);

This file just includes the action cable js files and readies a consumer to connect to the cable on your server once you’ve specified a subscription.

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

App.online_indicator = App.cable.subscriptions.create "OnlineIndicatorChannel",
 connected: ->
   # Called when the subscription is ready for use on the server

 disconnected: ->
   # Called when the subscription has been terminated by the server

 received: (data) ->
   # Called when there's incoming data on the WebSocket for this channe

This file prepares some callback methods for the three main interactions the consumer can have with the channel: connecting, disconnecting, and receiving data.

# app/channels/online_indicator_channel.rb

class OnlineIndicatorChannel < ApplicationCable::Channel
 def subscribed
   # stream_from "some_channel"
 end

 def unsubscribed
   # Any cleanup needed when channel is unsubscribed
 end
end

This class creates the OnlineIndicator channel and prepares two methods that allow us to execute code when the subscription is created and destroyed.

The `ApplicationCable::Channel` and `ApplicationCable::Connection` classes that are created are empty for now.

Let’s Write Some Code

But first, let’s think about how something like this might work.  An Employer views a list of Candidates and wants to be informed whenever one of those Candidates connects or disconnects from the application.  So we’ll probably need to subscribe to a channel that broadcasts when a user comes online or goes offline.  We’ll probably also need to store some state for the initial page loads so that we don’t need to re-broadcast events any time a client connects.

Let’s start to fill in the Ruby side here.   The first thing we need to do is not intuitive but we need to tell ApplicationCable how to identify a user, and if you’re using Devise like we are, you also need to do a few little tricks to find your user using signed cookies.  I had to do some digging to figure this out so consider yourself saved.

# app/channels/application_cable/connection.rb

module ApplicationCable
 class Connection < ActionCable::Connection::Base
   identified_by :current_user

   def connect
     self.current_user = find_verified_user
     logger.add_tags 'ActionCable', current_user.email
   end

   private
     def find_verified_user
       verified_user = User.find_by(id: cookies.signed['user.id'])
       if verified_user
         verified_user
       else
         reject_unauthorized_connection
       end
     end
 end

And we should set those cookies somewhere, so let’s modify our Warden initializer:

# config/initializers/warden.rb

Warden::Manager.after_set_user do |user, auth, opts|
 scope = opts[:scope]
 auth.cookies.signed["#{scope}.id"] = user.id
 auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now
end

Warden::Manager.before_logout do |user, auth, opts|
 scope = opts[:scope]
 auth.cookies.signed["#{scope}.id"] = nil
 auth.cookies.signed["#{scope}.expires_at"] = nil
end

This sets or destroys the signed cookie when a user logs in or out.

We also need to modify our `config/cable.yml` file slightly if we want this to work in development.  By default, Rails sets the adapter to `async`, which only operates in the same process, so you can’t easily check whether things are working by broadcasting messages from console or another browser window.  So let’s change our adapter to redis:

# config/cable.yml

development:
 adapter: redis

Now that that’s out of the way, we can start writing some real code!

When a User connects to our app, we need to store their status somewhere so we can know whether to show the indicator on page load, so let’s just use Redis to store that.  We also need to broadcast that they’ve connected to the channel and subscribe them so they will be notified when others come and go:

# app/channels/online_indicator_channel.rb

class OnlineIndicatorChannel < ApplicationCable::Channel
 def subscribed
   # store in redis that they’re online
   $redis.set “users:online:#{ current_user.id }”, 1
   # broadcast to the channel that they've appeared
   OnlineIndicatorChannel.broadcast('online_indicator', { userId: current_user.id, action: ‘appeared’ })
   # subscribe to the stream
   stream_from 'online_indicator'
 end

 def unsubscribed
   # remove the redis key indicating they’re online
   $redis.del “users:online:#{ current_user.id }”
   # broadcast to the users channel that they've disappeared
   OnlineIndicatorChannel.broadcast_to('online_indicator', { userId: current_user.id, action: ‘disappeared’ })
 end
end

In our `user.rb` file, we can now add a helper method to check whether they’re marked as online.

# app/models/user.rb

def online_now?
 $redis.get(“users:online:#{ id }”).present?
end

We can use that when we initially load the page to show or/hide the indicator.  Let’s see what a quick mock of our view might look like:

/ app/views/users/_user.haml

= div_for(user) do
 = image_tag user.image_url
 = user.name
 .online-indicator{ class: !user.online_now? && 'xs-none' } Online Now!

Now that we have our view, we can use Javascript to handle the live updates.  When the User connects/disconnects, we’ll broadcast that they’ve come online or gone offline.  Let’s go fill in the functions in our channel coffee file.  Since we don’t really have anything we need to do in the view when a client connects or disconnects, we’ll just log the event.  We’re only concerned when other users have come on or offline and it’s streamed to other connected Users.  The received method is what really matters here:

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

App.online_indicator = App.cable.subscriptions.create "OnlineIndicatorChannel",
 connected: ->
   console.log 'User subscribed to channel'

 disconnected: ->
   console.log 'User unsubscribed to channel'

 received: (data) ->
   $indicator = $('#user-' + data.userId + ' .online-indicator')
   if data.action == 'appeared'
     $indicator.removeClass('xs-none');
   else if data.action == 'disappeared'
     $indicator.addClass('xs-none');
   else
     console.log 'unknown action'

When our client receives the message on the channel that a User has come online or gone offline, we find the online indicator DOM element if the User is on the page and hide or show it with CSS.  Pretty simple!

via GIPHY

And there you have it.  With a little HTML/CSS styling you have just implemented a simple way to show when users go online and offline with WebSockets!

candidate-banner-2

About the Author

Brad Herman

I'm an engineer on the Acquisition team at Hired, helping to build systems and test ideas that bring more people to the platform. I've got over 10 years experience building things with Ruby and Rails and previously founded the international on-demand massage company, Soothe. In my free time, I play gypsy jazz guitar and spend too much time traveling around the world to see jam bands.