Real-time App on Rails by Action Cable

Published: Aug 8, 2024

The previous blog post, WebSocket on Rails by Action Cable, focused on WebSocket as a protocol. As in the previous post, by default, Rails app responds to WebSocket connection requests without any hassle. However, other than connecting and sending ping frames, it doesn’t do anything. This blog post focuses on an application side and explains how we can create a full-duplex, bidirectional app.

Publish/Subscribe (Pub/Sub) architecture

WebSocket itself is the protocol, so it is independent from an application architecture or framework. In Rails, ActionCable::Connection::Base is an abstraction of WebSocket connection.

As an application framework on the full-duplex, bidirectional connection, Rails adapts Pub/Sub (Publish/Subscribe) architecture. The Pub/Sub architecture is a general event-driven, asynchronous model for a distributed system. The Pub/Sub architecture is independent from protocols, so it is not only for WebSocket. If we name the Pub/Sub frameworks, Apache Kafka, Akka, RabbitMQ, and many more are out there.

In general, the Pub/Sub framework consists of publishers, a broker with topics (or channels), and subscribers. The subscriber subscribes to a topic or topics to get updates. When the publisher send a message to a broker, the broker sends the message to the related topic. Then, the message will be distributed to subscribers who subscribed to the topic previously.

img: Pub/Sub architecture in general

Rails provides a bit simplified version of Pub/Sub architecture. For Rails, both publishers and subscribers are a web application client. The idea of consumer is introduced as an abstraction of publishers and subscribers. Using JavaScript library, a consumer is created tied to the specific channel. The publisher sends a message through the consumer. When the publisher wants to send the message to a specific method, it performs the corresponding action with the message. Once the message arrives to the channel, the message will be broadcast to subscribers. In the end, the broadcast message is received by the subscriber through the consumer.

img: Rails Pub/Sub architecture

Chat: Simple Real-time Application

It’s time to create a Rails app. The application here is a very simple chat app. The application needs a JavaScript library which has an ability to behave reactively when the broadcast message comes in. Here, the app uses Vue.js version 3 with composition API.

Create Rails app

As always, the first step is to create a Rails app. The application doesn’t need some libraries, so it uses the rc file below to skip those.

--skip-action-mailer
--skip-action-mailbox
--skip-action-text
--skip-active-job
--skip-active-storage
-J
-T

Save above to .railsrc file, or whatever the file name you like.

$ rails new action-cable-chat --rc=./.railsrc

Create a Vue app mount point

So that the root path shows Vue.js page, create a mount point.

$ cd action-cable-chat
$ bin/rails g controller home index

Edit app/views/home/index.html.erb to create a mount point, #app.

<%= content_tag(:div, "", id:"app") %>

Additionally, update config/routes.rb.

Rails.application.routes.draw do
  root 'home#index'
  # ...
  # ...
end

Install and setup vite_rails

Since the app uses Vue.js on the frontend side, vite_rails gem should be installed and setup. Try below:

$ bundle add vite_rails
$ bundle exec vite install

Install Vue.js

At this point, package.json and package-lock.json are created. Since we use yarn instead of npm, remove package-lock.json and run yarn install.

$ rm package-lock.json
$ yarn install

For now, we are ready to install Vue.js. Run blow to add two packages:

$ yarn add vue @vitejs/plugin-vue

Then, edit vite.config.ts to add Vue plugin.

// vite.config.ts
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import vue from '@vitejs/plugin-vue' // added

export default defineConfig({
    plugins: [
        RubyPlugin(),
        vue(),  // added
    ],
})

Create a starter script

When vite was installed, Profile.dev was updated as well to start two servers: one for backend, another for frontend. To avoid typing foreman start -f Procfile.dev everytime, create a bin/dev file with the content below:

#!/usr/bin/env sh

if gem list --no-installed --exact --silent foreman; then
  echo "Installing foreman..."
  gem install foreman
fi

# Default to port 3000 if not specified
export PORT="${PORT:-3000}"

exec foreman start -f Procfile.dev "$@"

Change the file permission to executable by chmod 755 bin/dev.

Create a Pub/Sub channel

So far, all settings were completed. Now, it’s time to write code for a chat app.

We start from creating a channel for Pub/Sub. Rails provides a generator to create a channel, so type below:

$ bin/rails g channel chat speak

Above command creates app/channels/chat_channel.rb file with a minimal implementation. Update subscribe and speak method as in below:

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    ActionCable.server.broadcast("chat_channel", data)
  end
end

The method, subscribed, defines the channel whose name is chat_channel. The method, speak, broadcast message to clients who subscribed to the chat_channel. That’s all for the server side.

Install @rails/actioncable package

Since WebSocket is a protocol, we may use any libraries for WebSocket, for example, Socket.IO. However, it’s much better to use Rails provided package to connect to the backend seamlessly. Run below to install @rails/actioncable package.

$ yarn add @rails/actioncable 

Write frontend code

The last piece is a Vue.js app. Create a Vue component, app/frontend/App.vue, with the content below:

<script setup>
import { ref } from 'vue';
const message = ref("");
const messages = ref([]);
import { createConsumer } from '@rails/actioncable';

const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const consumer = createConsumer(`${protocol}://${window.location.host}/cable`);
const channel = consumer.subscriptions.create({ channel: 'ChatChannel' }, {
  received(data) {
    messages.value.push(data['message']);
  }
});

const addNewMessage = () => {
  channel.perform('speak', { message: message.value });
}
</script>

<template>
  <div id="app">
    <h2>Action Cable Example</h2>
    <div class="info">Type something in the box below and hit enter</div>
    <form @submit.prevent="addNewMessage">
      <input
          type="text"
          placeholder="say something"
          minlength="1"
          maxlength="50"
          v-model.trim="message" />
    </form>
    <div class="messages">
      <ul class="message">
        <li v-for="message in messages"></li>
      </ul>
    </div>
  </div>
</template>

Everything of frontend is here. The above Vue component does:

  • Creates a consumer using createConsumer function provided by Rails’ actioncable package.
    By default, WebSocket is mounted on /cable, so the URL to WebSocket is used to an argument of createConsumer.
  • Creates a channel by subscribing to the channel, ChatChannel.
    The channel name is a channel class name on Rails side, so it is a camel case of ChatChannel.
  • At the same time, implements received function to update the messages value when a new message comes in.
  • Calls addNewMessage function when something is typed in the input box and hit enter.
    The function hits channel’s perform function to send the message to speak method defined in ChatChannel class on the server side.

To make the Vue component work, we need a small additional work. Edit app/frontend/entrypoints/application.js to add below:

import { createApp } from 'vue';
import App from '~/App.vue';
import '~/styles.css'

createApp(App).mount('#app');

The mount point #app was already created on the backend side. To look better, the Vue component uses the style.css below.

* {
  box-sizing: border-box;
}

html {
  font-family: sans-serif;
}

body {
  margin: 0;
}

#app {
  margin: 3rem auto;
  border-radius: 10px;
  padding: 1rem;
  width: 90%;
  max-width: 40rem;
}

#app h2 {
  font-size: 1.5rem;
  border-bottom: 2px solid #ccc;
  color: #af463d;
  margin: 0 0 1rem 0;
}

#app .info {
  font-size: 1rem;
  color: #544f4f;
  margin: 0 0 1rem 0;
}

#app .messages {
  font-size: 1rem;
  color: #544f4f;
  margin: 1rem 0 1rem 0;
}

#app .message {
  font-size: 1rem;
  color: #4d4848;
  margin: 0 0 1rem 0;
}

#app input {
  font: inherit;
  border: 1px solid #aaa;
  background-color: #eee;
}

#app input:focus {
  outline: none;
  border-color: #754340;
  background-color: #fff;
}

Run the app and try real-time chat

We have already created bin/dev starter command. Type it and start servers.

$ bin/dev

Open http://localhost:3000 on multiple different browsers or private windows. Type something in the input box and hit enter. The message appears on all browsers immediately. Below are the result on Safari, FireFox and Chrome.

img: Chat on Safari
img: Chat on FireFox
img: Chat on Chrome

Conclusion

WebSocket and Action Cable are not easy ideas to understand. However, once we do, an implementation using Rails Action Cable is not difficult. We can create more interesting real-time applications by Action Cable.

Comments and Discussions

GitHub Discussions: Real-time App on Rails by Action Cable #9

References

Latest Posts

Application Development by Rails Action Cable

The previous two blog posts introduced WebSocket and how to implement a WebSocket application on Ruby on Rails. This blog post digs deeper. It is a memo on creating a more realistic application by Action Cable.

Real-time App on Rails by Action Cable

The previous blog post, WebSocket on Rails by Action Cable, focused on WebSocket as a protocol. As in the previous post, by default, Rails app responds to WebSocket connection requests without any hassle. However, other than connecting and sending ping frames, it doesn’t do anything. This blog post focuses on an application side and explains how we can create a full-duplex, bidirectional app.

WebSocket on Rails by Action Cable

In the web application domain, we hear some protocol names. Absolutely, HTTP or HTTPS is the most famous protocol that all web developers know. Although there’s a mechanism of Keep-Alive, a single request/response sequence with a single client/server is all done by HTTP. The client initiates the HTTP request to the server. Once the client receives the HTTP response from the server, communication finishes. As far as HTTP is used, the server just waits and waits. Only when the request comes in, the server can send back some data to the client. This communication style is surprisingly capable of doing many things, so most web applications are satisfied with HTTP.