NYC
skills/dchuk/rails_ai_agents/action-cable-patterns

action-cable-patterns

SKILL.md

Action Cable Patterns for Rails 8

Overview

Action Cable integrates WebSockets with Rails:

  • Real-time updates without polling
  • Server-to-client push notifications
  • Chat and messaging features
  • Live dashboards and feeds

Quick Start

# config/cable.yml
development:
  adapter: async

test:
  adapter: test

production:
  adapter: solid_cable  # Rails 8 default

Connection Authentication

# 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 session_token = cookies.signed[:session_token]
        if session = Session.find_by(token: session_token)
          session.user
        else
          reject_unauthorized_connection
        end
      else
        reject_unauthorized_connection
      end
    end
  end
end

Channel Patterns

Pattern 1: Notifications Channel

# app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def self.notify(user, notification)
    broadcast_to(user, {
      type: "notification",
      id: notification.id,
      title: notification.title,
      body: notification.body
    })
  end
end

Pattern 2: Resource Updates Channel

# app/channels/events_channel.rb
class EventsChannel < ApplicationCable::Channel
  def subscribed
    @event = Event.find(params[:event_id])

    if authorized?
      stream_for @event
    else
      reject
    end
  end

  def self.broadcast_update(event)
    broadcast_to(event, {
      type: "update",
      html: ApplicationController.renderer.render(
        partial: "events/event", locals: { event: event }
      )
    })
  end

  private

  def authorized?
    EventPolicy.new(current_user, @event).show?
  end
end

Pattern 3: Integration with Turbo Streams

# app/models/comment.rb
class Comment < ApplicationRecord
  after_create_commit -> {
    broadcast_append_to(
      [event, "comments"],
      target: "comments",
      partial: "comments/comment"
    )
  }

  after_destroy_commit -> {
    broadcast_remove_to([event, "comments"])
  }
end
<%# app/views/events/show.html.erb %>
<%= turbo_stream_from @event, "comments" %>

<div id="comments">
  <%= render @event.comments %>
</div>

Broadcasting from Services

module Events
  class UpdateService
    def call(event, params)
      event.update!(params)
      EventsChannel.broadcast_update(event)
      DashboardChannel.broadcast_stats(event.account)
      success(event)
    end
  end
end

Testing Channels

Channel Test (Minitest)

# test/channels/notifications_channel_test.rb
require "test_helper"

class NotificationsChannelTest < ActionCable::Channel::TestCase
  setup do
    @user = users(:one)
    stub_connection(current_user: @user)
  end

  test "subscribes successfully" do
    subscribe
    assert subscription.confirmed?
  end

  test "streams for the current user" do
    subscribe
    assert_has_stream_for @user
  end

  test "broadcasts notification to user" do
    subscribe
    notification = notifications(:one)

    assert_broadcast_on(
      NotificationsChannel.broadcasting_for(@user),
      hash_including(type: "notification")
    ) do
      NotificationsChannel.notify(@user, notification)
    end
  end
end

Channel with Authorization Test

# test/channels/events_channel_test.rb
require "test_helper"

class EventsChannelTest < ActionCable::Channel::TestCase
  setup do
    @user = users(:one)
    @event = events(:one) # belongs to @user's account
    @other_event = events(:other_account)
    stub_connection(current_user: @user)
  end

  test "subscribes to authorized event" do
    subscribe(event_id: @event.id)
    assert subscription.confirmed?
    assert_has_stream_for @event
  end

  test "rejects unauthorized event" do
    subscribe(event_id: @other_event.id)
    assert subscription.rejected?
  end
end

Connection Test

# test/channels/connection_test.rb
require "test_helper"

class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
  test "connects with valid session token" do
    user = users(:one)
    session = user.sessions.create!

    connect cookies: { session_token: session.token }

    assert_equal user, connection.current_user
  end

  test "rejects without session token" do
    assert_reject_connection do
      connect
    end
  end
end

Stimulus Controller for Channels

// app/javascript/controllers/chat_controller.js
import { Controller } from "@hotwired/stimulus"
import consumer from "../channels/consumer"

export default class extends Controller {
  static targets = ["messages", "input"]
  static values = { roomId: Number }

  connect() {
    this.channel = consumer.subscriptions.create(
      { channel: "ChatChannel", room_id: this.roomIdValue },
      {
        received: this.received.bind(this),
      }
    )
  }

  disconnect() {
    this.channel?.unsubscribe()
  }

  received(data) {
    if (data.type === "message") {
      this.messagesTarget.insertAdjacentHTML("beforeend", data.html)
      this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight
    }
  }

  send(event) {
    event.preventDefault()
    const body = this.inputTarget.value.trim()
    if (body) {
      this.channel.perform("speak", { body })
      this.inputTarget.value = ""
    }
  }
}

Checklist

  • Connection authentication configured
  • Channel authorization implemented
  • Channel tests written
  • Broadcasting from services/models
  • Client-side subscription set up
  • Turbo Stream integration (if applicable)
  • All tests GREEN
Weekly Installs
2
First Seen
7 days ago
Installed on
gemini-cli2
opencode2
antigravity2
codex2
windsurf2
kiro-cli2