Action Cable Configuration & Subscriptions in Rails
Tags: rails, javascript, postgres, redis
Published: 2020-10-21 02:15:53

# **Table Of Contents**
* [**Action Cable**](#action-cable)
    * [**WebSockets**](#web-sockets)
    * [**TCP/IP**](#tcpip)
    * [**Terminology**](#terminology)
    * [**Queue Data Structure**](#queue-data-structure)
* [**Rails/JavaScript Code**](#railsjavascript-code)
    * [**Server-Side Components**](#serverside-components)
    * [**Client-Side Components**](#clientside-components)
    * [**Client-Server Interactions**](#clientserver-interactions)
* [**Configuration**](#configuration)
    * [**Redis**](#redis)
    * [**Action Cable Server**](#action-cable-server)

NOTE: At the time this blog was written, the Rails version in use was 6.0. 

---

In this blog, I will walk through configuring and implementing [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html) into a Rails Application. As I'm writing, I don't know how Action Cable works and what underlying processes make it functional. 

This is why I love writing technical blogs. It is such a great way to learn and document processes to reference later. There's no way I'm going to remember everything, but as long as the fundamentals are there I will know where to look when I need to remember.

## **Action Cable**

Action Cable is a bundle of code providing a **client-side JavaScript framework** and a **server-side Ruby framework.**

It integrates **WebSockets** with the rest of a rails application. This allows the application to have certain real-time features to be written in Ruby.

For an example, I'm currently writing an application called **FilmPitch**, where filmmakers can fund their dream movies. A **Project** `has_many :comments`. When a user comments, the browser will update so that the comment will display in real-time.

So what are WebSockets and how do they make real-time features possible? 

### **Web Sockets**

There is a lot of Wikipedia information in this section. I wanted to piece together the important bits to know before going forward.

> A [**WebSocket**](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex communication channels over a single TCP connection.

> A [**communications protocol**](https://en.wikipedia.org/wiki/Communication_protocol) is a system of rules that allow two or more entities of a communication system to transmit information via any kind of variation of a physical quantity.

> A [**full-duplex**](https://en.wikipedia.org/wiki/Duplex_(telecommunications)#FULL-DUPLEX) (FDX) system, or sometimes called double-duplex, allows communication in both directions, and, unlike half-duplex, allows this to happen simultaneously.

---

The WebSocket protocol is ***different*** from HTTP, the Hypertext Transfer protocol, although it is compatible with HTTP. Essentially, the **WebSocket protocol facilitates real-time data transfer from and to the server.** 

HTTP is a request-response protocol. It doesn't keep a connection open. It only sends data when requested. The WebSocket protocol sends data back and forth between client and server **continuously**, without being requested by the client.

---

> This is made possible by providing a **standardized way for the server to send content to the client without being first requested by the client**, and allowing messages to be passed back and forth while keeping the connection open. In this way, a two-way ongoing conversation can take place between the client and the server.

For example, cell phones are full-duplex, as two callers are allowed to speak and hear the other at the same time. 

### **TCP/IP**

> The [**Transmission Control Protocol**](https://en.wikipedia.org/wiki/Transmission_Control_Protocol) (TCP) is one of the main protocols of the Internet protocol suite. TCP provides reliable, ordered, and error-checked delivery of a stream of octets (bytes) between applications running on hosts communicating via an IP network.

> TCP is connection-oriented, and **a connection between client and server is established before data can be sent.** The server must be listening (passive open) for connection requests from clients before a connection is established. Three-way handshake (active open), [retransmission](https://en.wikipedia.org/wiki/Retransmission_(data_networks)), and error-detection adds to reliability but lengthens latency. 

---

> Applications that do not require reliable data stream service may use the [User Datagram Protocol (UDP)](https://en.wikipedia.org/wiki/User_Datagram_Protocol), which provides a connectionless datagram service that prioritizes time over reliability. TCP employs network congestion avoidance. However, there are vulnerabilities to TCP including denial of service, connection hijacking, TCP veto, and reset attack. For network security, monitoring, and debugging, TCP traffic can be intercepted and logged with a packet sniffer.

The [**Network Function**](https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Network_function) section of the TCP Wiki will detail more on how the protocol functions.

---

> The [**Internet protocol suite**](https://en.wikipedia.org/wiki/Internet_protocol_suite) is the conceptual model and set of communications protocols used in the Internet and similar computer networks. It is commonly known as TCP/IP because the foundational protocols in the suite are the Transmission Control Protocol (TCP) and the Internet Protocol (IP).

> The Internet protocol suite provides *end-to-end data communication* **specifying how data should be packetized, addressed, transmitted, routed, and received.** This functionality is organized into four abstraction layers, which classify all related protocols according to the scope of networking involved. From lowest to highest, the layers are the link layer, containing communication methods for data that remains within a single network segment (link); the internet layer, providing internetworking between independent networks; the transport layer, handling host-to-host communication; and the application layer, providing process-to-process data exchange for applications.

### **Terminology**

The [Action Cable Terminology](https://guides.rubyonrails.org/action_cable_overview.html#terminology) section of the Ruby on Rails Guide will detail all of the terms I list below. I'll stitch together everything so that it makes more sense. If it doesn't, the configuration section will help make it clear.

Action Cable can handle many connection instances. There is one **connection instance** for every WebSocket. A user could have more than one tab open in their browser, which means that there can be more than one connection instance in a user's browser.

The client is referred to as the browser. The client of a WebSocket connection is called the ***consumer***.

Each ***consumer*** can ***subscribe*** to ***multiple*** cable ***channels***. When a consumer is subscribed to a channel, they act as a ***subscriber***.

> **Pub/Sub**, or Publish-Subscribe, refers to a **message queue paradigm** whereby senders of information (**publishers**), send data to an abstract class of recipients (**subscribers**), _without specifying individual recipients._ Action Cable uses this approach to communicate between the server and many clients.

Essentially, all users(consumers) subscribed to a channel will get updates without requesting them.

> In software architecture, publish-subscribe is a messaging pattern where senders of messages, called *publishers*, **do not program the messages to be sent directly to specific receivers, called subscribers**, but *instead categorize published messages into classes* **without knowledge of which subscribers, if any, there may be.** Similarly, subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers, if any, there are.

The connection between the subscriber and the channel is called a ***subscription***. A consumer could subscribe to multiple chat rooms at the same time.

> ***Each channel encapsulates a logical unit of work***, _similar to what a controller does_ **in a regular MVC setup.** For example, you could have a ChatChannel and an AppearancesChannel, and a **consumer could be subscribed to either or to both of these channels.** At the very least, a consumer should be subscribed to one channel.

Each **channel can stream zero or more broadcastings.** A broadcasting is a pubsub link where **anything transmitted by the broadcaster** is ***sent directly to the channel subscribers*** **who are streaming that named broadcasting.**

Controllers will work like normal. In my `Commentscontroller`, the `#create` action is what will create, save, and call the **job** that will broadcast the newly saved comment to the channel. **ActiveJob** will then handle broadcasting the information to channel subscribers.

### **Queue Data Structure**

The [Queue Data Structure](https://www.geeksforgeeks.org/queue-data-structure/) is just like the [Stack Data Structure](https://www.geeksforgeeks.org/stack-data-structure/). Stacks follow a LIFO (Last-in First-out) principle. Queues follow the FIFO (First-in First-out) principle. 

## **Rails/JavaScript Code**

This section details the purpose behind the files in `app/channels` and `app/javascript/channels`. Don't worry about configuration for now. 

A lot of it is from the Action Cable guide, and that is on purpose. The important bits are set in bold. The Terminology section introduces the terms, this section introduces what you'll be working with, and the configuration section pieces everything together in a linear fashion.

### **Server-Side Components**

#### **Connections**

> For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all channel subscriptions that are created from thereon.

> The connection itself **does not deal** with any specific application logic beyond ***authentication and authorization.***

---

> Connections are instances of `ApplicationCable::Connection.` In this class, **you authorize the incoming connection**, and proceed to establish it **if the user can be identified.**

```ruby
# 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
```

`identified_by` is a connection identifier that can be used to find this specific connection later.

The above example assumes that you authenticated your user somewhere else in your app and set a signed cookie with the `user_id`.

> The cookie is then automatically sent to the connection instance when a new connection is attempted, and you use that to set the `current_user`. By identifying the connection by this same current user, you're also ensuring that you can later retrieve all open connections by a given user (and potentially disconnect them all if the user is deleted or unauthorized).

#### **Channels**

> A channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup. By default, Rails creates a parent `ApplicationCable::Channel` class for encapsulating shared logic between your channels.

```ruby
# app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
```

This is the **parent channel.**

You don't have to adjust anything here. Any new channel you create will inherit from `ActionCable::Channel`.

`rails g channel --help` will detail the ways in which you can generate a new channel. I will be creating a comments channel, so my command will be `rails g channel Comments`.

#### **Subscriptions**

> Consumers subscribe to channels, acting as subscribers. Their connection is called a subscription. **Produced messages are then routed to these channel subscriptions** ***based on an identifier sent by the cable consumer.***

Data is broadcasted to this channel. 

```ruby
# app/channels/comments_channel.rb

class CommentsChannel < ApplicationCable::Channel
  def subscribed
    # this is called when the consumer has successfully
    # become a subscriber to this channel.
  end

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

### **Client-Side Components**

#### **Connections**

> Consumers require an instance of the connection on their side. This can be established using the following JavaScript, which is generated by default by Rails:

```js
// app/javascript/channels/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
```

From the Action Cable guide, `createConsumer` will connect to `"/cable"` automatically if you don't specify a URL argument for it. There's not much else to this file.

#### **Subscriber**

In order for a user to subscribe to a channel, you have to create a subscription in your channel -> `app/javascript/channels/${channel_name}.js.`

My comments channel was generated like this:

```js
import consumer from "./consumer";

// Generated with `rails g channel Comments`

consumer.subscriptions.create("CommentsChannel", {
  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 channel
  }
});
```

The channel name needs to match your rails channel name and/or room. If you've never seen the syntax above, it is a [Method Properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions) shorthand feature in ES6.

It's essentially the same as:

```js
{
  connected: function(){

  },
  disconnected: function(){

  },
  received: function(data){

  },
}
```

If you need to see the flow of their `Consumer`, `Subscription(s)` classes, you can find them [here](https://github.com/rails/rails/tree/master/actioncable/app/javascript/action_cable).

### **Client-Server Interactions**

#### **Streams**

Streams enable a channel to route broadcasts to subscribers. When new data is sent, the stream allows the channel to route that data to clients connected to the channel.

`stream_for` and `stream_from` basically do the same thing. Here is their [code](https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/channel/streams.rb).

`stream_for` is more used for a related model. It automatically generates broadcasting from the model and channel for you.

#### **Broadcasting**

> A broadcasting is a pub/sub link where anything transmitted by a publisher is routed directly to the channel subscribers who are streaming that named broadcasting.

> The default pubsub queue for Action Cable is `redis` in production and `async` in development and test environments.

I will show you how to use `ActiveJob` with rails so that Action Cable can use Redis in the configuration section. ActiveJob allows jobs to run in queueing backends.

#### **Subscriptions**

When a consumer subscribes to a channel, they become a subscriber. The connection between the two is a subscription. The **data sent by the rails channel will be available as an argument to the method properties objects in the channel js file.**

The `received(data)` method is called when there's incoming data on the WebSocket for a channel. In my `comments_channel.js` file, the `data` is an already rendered `erb` template. It's already in HTML, so I'm just appending it where I want it.

```js
received(data) {
    // console.log("Recieving...")
    console.log(data);
    // console.log("Appending...")
    this.appendComment(data);
    // console.log("I have appended!")
  },
```

#### **Passing Parameters to Channels**

If you're looking at your `${name}_channel.rb` `#subscribed` method confused about where the `params` are coming in from, they're coming from the `${name}_channel.js` file. If you start `byebug` when the `subscribed` method is called, the only params you will get is the channel name because it was defined where the subscription was created at the top: 

```js
consumer.subscriptions.create("CommentsChannel", {
  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 channel
  }
});

```

## **Configuration**

Note: I'm using Postgres & Devise in this application.

### **Redis**

I will be using [Redis](https://en.wikipedia.org/wiki/Redis) as the queueing backend.

> Redis is an in-memory data structure project implementing a distributed, in-memory key–value database with optional durability. Redis supports different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indexes.

If you don't have it installed on Mac, install it with `brew install redis`.

Install the Redis gem with `gem install redis`. In case this gem isn't in your `Gemfile`, add it and run `bundle install`.

In your `config/cable.yml` file, make sure the adapter for your environments is Redis. For some reason, Redis was having errors with the other adapters set with `async`, so I set them all to `redis`. Also set the URL, which should already be present in the environment file.

```ruby
development:
  adapter: redis
  url: redis://localhost:6379/1
```

**In order for Rails to connect to Redis, you have to start a server in another terminal.** Start a Redis server by running `redis-server`.

### **Action Cable Server**

The Action Cable Server can run either separately from or alongside your application. **I have it set so that it runs when I launch my Rails server.**

#### `config/application.rb`

In `config/application.rb`, you have to mount the path for Action Cable: `config.action_cable.mount_path = '/cable'`. **This is where it will listen for WebSocket requests.**

#### `views/layouts/application/html.erb`

In the `views/layouts/application/html.erb`, add an `action_cable_meta_tag` in the head. `ActionCable.createConsumer()` will connect the path from this `meta_tag` and use it as an argument.

```ruby
  <%= action_cable_meta_tag %>
```

> To configure the URL, add a call to `action_cable_meta_tag` in your HTML layout HEAD. **This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files.**

#### `config/environments/development`

In `config/environments/development`, add:

```ruby
config.action_cable.url = "ws:localhost:3000/cable"

  config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
  config.action_cable.worker_pool_size = 5
```

> Action Cable will only accept requests from specified origins, which are passed to the server config as an array.

Set the pool size equal to what you have in your `config/database.yml` file.

> ...your server must provide at least the same number of database connections as you have workers. The default worker pool size is set to 4, so that means you have to make at least 4 database connections available. You can change that in `config/database.yml` through the `pool` attribute.

#### `config/routes.rb`

I don't believe I saw this in the Action Cable guide nor the example application they had, but it is present in many other blog examples. Not sure why it's omitted in the guide, have to look into it later.

Mount the Action Cable Server in `config/routes.rb`:

```ruby
  mount ActionCable.server => '/cable'
```

#### `app/channels/application_cable`

In this directory, you'll find two files: `channel.rb` and `connection.rb`.

That channel is the parent channel, so you don't need to alter that file at all.

`connection.rb` is where you will authenticate and authorize your user for their connection. I'm using **Devise**, so my user is authenticated like so:

```ruby
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user
 
    def connect
      self.current_user = find_verified_user
    end

    def disconnect
      # Any cleanup work needed when the cable connection is cut.
      # close(reason: nil, reconnect: true)
    end
 
    private
      def find_verified_user
        if verified_user = env['warden'].user
          verified_user
        else
          # You can find the reject_unauthorized_connection method here -> https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/authorization.rb
          reject_unauthorized_connection
        end
      end
  end
end
```

Essentially, logged in users connect to the action cable server. They don't become a subscriber yet, though. The channel's `#subscribed` method will handle that portion. This class is all about authenticating and authorizing the user for this specific connection, allowing Action Cable to find the connection later.

`reject_unauthorized_connection` is a method given to you by [`ActionCable::Connection::Authorization`](https://api.rubyonrails.org/classes/ActionCable/Connection/Authorization.html#method-i-reject_unauthorized_connection). You can also find this method here in the [Github](https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/authorization.rb).

#### `comments_channel.rb`

I generated my comments channel with the `rails g channel` command.

```ruby
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    project = Project.find_by_id(params[:id])
    # in Rails 6.1, a new method for handling the below control structure is defined as
    # stream_or_reject_for(record), which houses this code:

    # if there is a record, subscribe the user and start a stream, else reject
    # the user and don't start a new stream.
    if project
      stream_for project
    else
      reject
    end
  end

  def receive(data)
    # Rebroadcast a message sent by one client to any other connected clients
    # ActionCable.server.broadcast(project, data)
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
    # stop_all_streams() -> Unsubscribes all streams associated with this channel from the pubsub queue
  end
end
```

Right now, **only** the `#subscribed` method is functional. The params `id` is given to me from javascript. If the URL doesn't have a project `id`, the subscription won't be set, and no stream will start.

#### `comments_channel.js`

```js
import consumer from "./consumer";

// Generated with `rails g channel Comments`

var url = window.location.href;
let id = url.slice(url.length - 1, url.length);

consumer.subscriptions.create({channel: "CommentsChannel", id: id}, {
  connected() {
    // Called when the subscription is ready for use on the server
    console.log("Connected to the comments channel!");
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    // console.log("Recieving...")
    console.log(data);

    // console.log("Appending...")
    this.appendComment(data);
    // console.log("I have appended!")
  },

  appendComment(data){
    const commentSection = document.getElementById("comments");
    commentSection.insertAdjacentHTML("afterbegin", data);
  }
})
```

For right now, the server gets the `id` from the URL. It sends it as a param to the rails channel subscribed method.

#### `ActiveJob` & Broadcasting

```ruby
class CommentBroadcastJob < ApplicationJob
  queue_as :default

  # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.

    # def broadcast_to(model, message)
    #   ActionCable.server.broadcast(broadcasting_for(model), message)
    # end

  # Active Job objects can be defined by creating a class that inherits from the 
  # ActiveJob::Base class. The only necessary method to implement is the “perform” method.
  

  def perform(project, partial)
    CommentsChannel.broadcast_to(project, partial)
  end
end
```

This class is used to send the broadcasts. What I'm doing here is having the project and partial broadcasted. It gets called in the `CommentsController`.

#### `comments_controller.rb`

```ruby
def create
    @comment = Comment.new(comment_params)

    if @comment.valid?

      @comment.save
      # You have to use methods found in ActiveJob::Core::ClassMethods -> 
      # https://edgeapi.rubyonrails.org/classes/ActiveJob/Core/ClassMethods.html

      # To enqueue a job to be performed as soon as the queuing system is free, use:
      # .perform_later(record)

      @obj = {
        id: @comment.id,
        description: @comment.description,
        user_id: @comment.user_id,
        project_id: @comment.project_id,
        display_name: @comment.user.display_name
      }.as_json

      CommentBroadcastJob.perform_later(
        @comment.project, 
        render_to_string(
          partial: 'comments/comment',
          locals: {
            comment: @obj
          } 
        )
      )

    else
      redirect_to project_path(comment.project)
    end
  end
```

This is all messy right now, but the data in my views are using a comments hash, so I'll end up refactoring this later. Either `render` or `render_to_string` works here. The partial will be created with the data you want while using rails helpers in the views: 

```ruby
<!-- views/comments/_comment.html.erb -->

<div>
  <div>
    <h4><%= comment['display_name'] %></h4>
    <p><%= comment['description'] %></p>
  </div>

  <% if current_user.id == comment['user_id'] %>
    <div>
      <button>Edit</button>
      <p>
        <%= link_to 'delete', 
        { controller: "comments", action: "destroy", id: comment['id'] }, 
        data: { confirm: 'Are you sure?' }, 
        method: :delete %>
      </p>
    </div>
  <% end %>
</div>
```

This has allowed two users to see comments in real-time. Here is a gif showing the process:

![Live Comments gif](https://drive.google.com/uc?export=view&id=1Pq3ZN3smndxoV3zOOX6i69Bf8R1_om8_)

I still have to figure out how I can stop displaying the edit/delete buttons for other users.

I figured it would be great to have this blog have the meat of everything. I spent a good amount of time going through many Wikipedia pages, rails guides, rails repos, blogs, and videos to figure out exactly how to get Action Cable to run. Hope it helps clear some confusion!

This is the project repo: [**FilmPitch**](https://github.com/GoodGuyGuf/FilmPitch)

If you have any questions or observations, please comment below. 🤩