Action Cable,

Uma visão superficial sobre os cabos de ação do Rails

Motivações 

  • "Desconstruindo a web" por Pothix
  • Talk do Perrella
  • Conversas paralelas em EA

Objetivos 

  • Aprender alguma coisa nova
  • Compartilhar conhecimento
  • Provocar uma troca de experiências 

Feliz aquele que transfere o que sabe e aprende o que ensina.

Cora Coralina

Quem sou eu?

João Gustavo de Paula

Since mar/2016 - Enterprise Applications

 

@devjoaogustavo

!=

Action cable?

Real time

F5

Long polling

WebSockets

WebSockets?

WebSocket protocol

  • Habilita a comunicação em duas vias entre um cliente, rodando um código desconhecido, e um servidor remoto, que aceita se comunicar com esse cliente.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Client Handshake

Server Handshake

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Action cable

Components

Server side

Connections

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

    def connect
      @current_user = find_verified_user
      Rails.logger.info "Mais uma conexão: total de #{ActionCable.server.connections.size + 1}"
    end

    def disconnect
      Rails.logger.info "Menos uma conexão: total de #{ActionCable.server.connections.size}"
    end

    private
      def find_verified_user
        if current_user = User.find_by(id: cookies.signed[:user_id])
          current_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

Cookies

# app/config/initializers/warden_hooks.rb

Warden::Manager.after_set_user do |user,auth,opts|
  scope = opts[:scope]
  auth.cookies.signed["#{scope}.id"] = user.id
end

Channels

class MessagesChannel < ApplicationCable::Channel
    def subscribed
    end

    def unsubscribed
    end
    
    def receive(data)
    end
end

Client side

Consumers

//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

Subscriptions

App.messages = App.cable.subscriptions.create('messages', {
  connect: function() {},
  disconnect: function() {},
  subscribed: function() {},
  unsubscribed: function() {},
  received: function() {}
});

Action cable

Interactions

Streams

Streams

class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'messages'
  end
end
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:id])
    stream_for post
  end
end
CommentsChannel.broadcast_to(@post, @comment)

Broadcasting

class MessagesController < ApplicationController
  def create
    message = Message.create!(content: params[:content])
    ActionCable.server.broadcast('messages', { message: message.content })

    ...
  end
end

Subscriptions

App.messages = App.cable.subscriptions.create('messages', {
  received: function(data) {
    $('#content').val('');
    return $('#messages').append(this.addMessage(data));
  },

  addMessage: function(data) {
    return '<p><b>User: </b>'+data.message+'<p>';
  }
});

Passing parameters to channel

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end
App.users = App.cable.subscriptions.create({channel: 'ChatChannel', room: 'bla'}, {
  received: function(data) {
   return $('#users').append(this.addUser(data.username));
  },

  addUser: function(data) {
    return '<p>'+data+'</p>';
  }
});
ChatChannel.broadcast_to("chat_#{room}", sent_by: 'Cabra', body: 'Chat joiado!')

Rebroadcasting

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
 
  def receive(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", data)
  end
end
App.chatChannel = App.cable.subscriptions.create({channel: "ChatChannel", room: "bla"}, {
  received: function(data) {}
});

App.chatChannel.send({
  sent_by: "Paul",
  body: "This is a cool chat app."
});

Action cable

Configurations

Subscriptions adapters

development:
  adapter: async
 
test:
  adapter: async
 
production:
  adapter: redis
  url: redis://127.0.0.1:6379

Allowed origins

config.action_cable.allowed_request_origins = ['http://localhost:3000']
config.action_cable.disable_request_forgery_protection = true

Consumer configuration

<%= action_cable_meta_tag %>
(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);
(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer("ws://localhost:28080");

}).call(this);

Action cable

Standalone

In app

class Application < Rails::Application
  config.action_cable.mount_path = '/cable'
end
Rails.application.routes.draw do
  mount ActionCable.server => '/cable'
end

Really alone

# cable/config.ru
require_relative 'config/environment'
Rails.application.eager_load!
 
run ActionCable.server
#!/bin/bash
bundle exec puma -p 28080 cable/config.ru

Duvido!

Conclusão

Lição do dia

  • O meio é melhor que o fim.
  • Sou previlegiado de trabalhar com vocês

Próximos passos

  • Aprofundar WebSockets
  • Estudar outras implementações
  • Aplicar WebSocket em algum projeto

Referências

  • http://guides.rubyonrails.org/action_cable_overview.html
  • https://github.com/rails/rails/tree/master/actioncable
  • https://www.youtube.com/watch?v=IeYGfM32Iqs
  • https://github.com/devjoaoGustavo/twocable
  • https://tools.ietf.org/html/rfc6455
  • http://giphy.com

Microfone aberto

Made with Slides.com