Action Cable: components checklist

January 27, 2016


Here are the minimum code changes required to provide websocket capability in a Rails 5.0.0 app. This is based on a minimal alert app.

Disclaimer: Of course this is all rapidly changing in Rails. This was correct for rails master branch at the time of writing this post.

Prerequisites:

  1. gemfile: add em-hiredis, redis, and make sure you have puma 2.15.3 or greater
...
gem 'rails', '>= 5.0.0.beta1.1'
gem 'em-hiredis'
gem 'redis'
gem 'puma', '>= 2.15.3'
...

  1. install redis:
$ sudo apt-get -y install redis-server

Code changes Starting from the outside in:

  1. app/views/layouts/application.erb:

    Add action_cable_meta_tag in the correct position:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Chat</title>
        <%= csrf_meta_tags %>
        <%= action_cable_meta_tag %>
    
        <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
        <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
      </head>
    
      <body>
        <%= yield %>
      </body>
    </html>
    
  2. app/assets/javascripts/cable.coffee:

     @App ||= {}
     App.cable = ActionCable.createConsumer()
    
  3. app/assets/javascripts/channels/subscriptions/alert.coffee:

    App.cable.subscriptions.create "AlertsChannel",
      connected: ->
        # Called when the subscription is ready for use on the server
    
      disconnected: ->
        # Called when the subscription has been terminated by the server
    
      # Called when there's incoming data on the websocket for this channel
      received: (data) ->
        $('#navbar_alert').html( data['alert'] )
    
  4. config/routes.rb:

    Rails.application.routes.draw do
      ...
      # Serve websocket cable requests in-process
      mount ActionCable.server => '/cable'
      ...
    end
    
  5. config/cable.yml:

    This moved recently from config/redis/cable.yml

    # Action Cable uses Redis to administer connections, channels, and sending/receiving messages over the WebSocket.
    production:
      url: redis://localhost:6379/1
    
    development:
      url: redis://localhost:6379/2
    
    test:
      url: redis://localhost:6379/3
    
  6. app/channels/application_cable/channel.rb:

    module ApplicationCable
      class Channel < ActionCable::Channel::Base
      end
    end
    
  7. app/channels/application_cable/alerts_channel.rb:

  class AlertsChannel < ApplicationCable::Channel
    def subscribed
      # send to the user_id named broadcasting
      stream_from "alerts_channel_#{current_user.id}"
    end

    def unsubscribed
    end
  end
  1. app/channels/application_cable/connection.rb:
  module ApplicationCable
    class Connection < ActionCable::Connection::Base
      include SessionsHelper
      identified_by :current_user

      def connect
        self.current_user = find_verified_user
      end

      protected
        def find_verified_user
          if current_user = user_from_remember_user_token
            current_user
          else
            reject_unauthorized_connection
          end
        end
    end
  end
  1. app/models/alert.rb:
  class Alert < ApplicationRecord
    ...
    after_create_commit { AlertBroadcastJob.perform_async self }
    ...
  end
  1. app/jobs/alert_broadcast_job.rb:
  class AlertBroadcastJob < ApplicationJob

    def perform( alert )
      # resend all of the alerts
      alerts = Alert.where( user_id: alert.user.id ).order( :name )
      # send to the user_id named broadcasting
      user_id = alert.user.id
      ActionCable.server.broadcast "alerts_channel_#{user_id}",
         alert: render_alert( alerts )
    end

    private
      def render_alert( alerts )
        ApplicationController.renderer.render( partial: 'alerts/alert', locals: { alerts: alerts } )
      end
  end
  1. config.ru
  # This file is used by Rack-based servers to start the application.
  require ::File.expand_path('../config/environment', __FILE__)

  # Action Cable uses EventMachine which requires that all classes are loaded in advance
  Rails.application.eager_load!
  #require 'action_cable/process/logging'

  ActionCable.server.config.disable_request_forgery_protection = true

  run Rails.application

© 2018 Keith P | Follow on Twitter | Git