ActionCable

Integrated WebSockets for Rails

Ruby Talks #9

Szubrycht Kamil

Text

https://www.zweitag.de/en/blog/technology/lets-build-a-chat-with-actioncable

Architecture

ActionCable Server

# config/routes.rb

Rails.application.routes.draw do

  ( ... )

  # Serve websocket cable requests in-process
  mount ActionCable.server => '/cable'
end

In app

ActionCable Server

# cable/config.ru

require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!

require 'action_cable/process/logging'

run ActionCable.server
$ bundle exec puma -p 28080 cable/config.ru

Standalone

Redis

# 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

ActionCable endpoint configuration

# config/environments/production.rb

config.action_cable.url = "ws://example.com:28080"
config.action_cable.allowed_request_origins = [ 'http://localhost:3000' ]
# config.action_cable.disable_request_forgery_protection = true

Establish the connection

# app/assets/javascripts/cable.coffee

( ... )
  @App ||= {}
  App.cable = ActionCable.createConsumer()
( ... )

Authorization of incoming connections

# app/channels/application_cable/connection.rb

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

    def connect
      if cookies.signed[:username].blank?
        reject_unauthorized_connection
      else
        self.current_user = cookies.signed[:username]
      end
    end
  end
end

Client-side

App.chat = App.cable.subscriptions.create "ChatChannel",
  connected: ->
    $('#message-input').show()
    $('#connection-error, #connect-button').hide()

  disconnected: ->
    $('#connection-error').show()
    $('#message-input, #connect-button').hide()

  received: (data) ->
    $('#messages').append(data.message)

  speak: (msg) ->
    @perform 'speak', message: msg

Server-side

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'admin_messages' if current_user.start_with?('admin')
    stream_from 'messages'
  end

  def speak(data)
    stream = current_user.start_with?('admin') ? 'admin_messages' : 'messages'
    ActionCable.server.broadcast(stream, message: render_message(data['message']))
  end

  private

  def render_message(message)
    ApplicationController.render( partial: 'messages/message', 
      locals: {
        message: message,
        username: current_user }
    )
  end
end

Summary

  • very easy to work with WebSockets
  • client-side and server-side solution
  • access to full domain model
  • each channel can be streaming zero or more broadcastings

RT#9: ActionCable

By Kamil Szubrycht

RT#9: ActionCable

  • 607