Guru on Rails

if you don’t sacrifice for your dream then your dream becomes your sacrifice.
Will Nguyen
Rails 5 websockets with Action Cable
Thu 04 Oct 2018

Rails 5 finally provided an easy way to add realtime communication between Rails 5 server and web browser. Rails 5 Action Cable integrates the web sockets to the the rest of Rails application realtime features along with an implementation of a Publish-Subscribe message queue pattern based on Redis. In order to understand how it works, we need to understand what publish-subscribe patterns is.

Publish-Subscribe Pattern

As we know that a realtime feature using sockets needs a mechanism which supports the communication between web browser and server side. Hence, we have Pushers and Subscribers. Pusher pushes message and Subscribers receive this message then. They are not individual so that we call them subscribers. This pattern models a way for applications to broadcast events to interested listeners.

Connnection

This is the client-server relationship. If the server opens a web socket, a connection object will be instantiated to serve all the channel subscriptions that are created from there on. 

The client of web socket is called the consumer. An individual user will create one consumer per browser tab, window etc.

The connection I mentioned above is ApplicationCable::Connection.

ApplicationCable::Connection
  • Authorize the incoming connection.
  • Proceed to establish if user can be identified.
# 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
    end
 
    private
      def find_verified_user
        if verified_user = User.find_by(id: cookies.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

Above code defines a class Connection which extends ActionCable::Connection::Base. 

identified_by is used to declare the property of the connection instance so that we can find this connection later.if necessary.

We use current_user as the identify so that we can later retrieve all open connections by a given user.

Channels

It's like a role player. We encapsulate a unit of work. Grouping the same things. 

ApplicationCable::Channel

What we need to do is extending the base class to create our own channel.

module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

We have an implicit interface method subscribed.

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  # Called when the consumer has successfully
  # become a subscriber to this channel.
  def subscribed
  end
end

Subscriptions

The javascript code will be automatically generated by Action Cable to create consumer.

// 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 is default lazy consumer. If this consumer becomes a subscriber by creating a subscription to a channel, the connection will happen.

App.cable.subscriptions.create {
      channel: 'BackgroundJobsNotificationsChannel'
      type: 'Vessels'
      id: jobId
    }, received: (data) ->

This is how browser push parameters to server side.

Streams

A mechanism to route published content (broadcasts) to subscribers. Using stream_for method to establish a connection to a stream.

class CommentsChannel < ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:id])
    stream_for post
  end
end

This stream is related to model so we can broadcast to subscribers by model instance.

CommentsChannel.broadcast_to(@post, @comment)

Super easy.

Rebroadcasting 

We expect client send a message to server and then it will be broadcasted to other clients (chatting). 

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
 
  def receive(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", data)
  end
end

Hook method receive will be involved. We don't use model instance for stream.

# app/assets/javascripts/cable/subscriptions/chat.coffee
App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
  received: (data) ->
    # data => { sent_by: "Paul", body: "This is a cool chat app." }
 
App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })

Note that we have stream_from, stream_for, broadcast and broadcast_to. They are different.