Hotwire Discussion

Count turbo_stream subscribers

Hi everyone!

I’ve implemented a very simple chat application and I would like to display how many people are connected to it. Also, this number should be updated upon new access (subscribed) and when user closes the tab (unsubscribed the channel).

Is there any way I can count how many subscribers have subscribed from a turbo_stream without using ActionCable adapter methods, i.e., agnostic of Redis/Async/etc. ?

I have a feeling I should be using ActionCable methods as Turbo Stream descend from it, but I am not sure

Turbo::StreamsChannel.superclass
=> ActionCable::Channel::Base

Any feedback is welcomed!
Thanks

1 Like

Hi, did you ever find a solution for this? I’m looking for an answer to the same question.

:musical_note:and still haven’t found what i’m looking for :musical_note:

Thanks. I’ve been scouring the documentation but haven’t been able to find a clear solution.

It’d be really useful to count the number of connected subscribers - I don’t want to bother rendering a complex stream if there are 0 connected subscribers.

1 Like

I’ve explored all these answers and couldn’t get any of them to work. I’m wondering if perhaps I’m misunderstanding something.

My use case is that my web app has an Chat Inbox, where admins can see and send messages to users. But we have hundreds of messages coming in every minute, and I want to avoid eating up server resources by rendering and sending partials for these messages, unless an admin is online and subscribed to the Chat’s turbostream.

So in my .html, I have

<%= turbo_stream_from @chat %>

and it connects perfectly. But in my message.rb, can’t figure out how to check if anyone is subscribed to the @chat, when a new message comes in and I’m trying to determine whether to broadcast it over the stream.

Any guidance on what else I should be trying?

Hi,

The best way to track the presence of subscribed users to a channel, like a @chat channel, is to use a regular action cable channel and record user when they subscribe to the channel and un-record them when they unsubcribe.

I will share you some code but take it carefully may I answered uncorrectly.

I use:

# models/chat.rb
class Chat < ApplicationRecord
  kredis_unique_list :online_user_ids

  def add_online_user(user_id)
    online_user_ids << user_id
  end

  def remove_online_user(user_id)
    online_user_ids.remove(user_id)
  end

  def online_users_count
    online_user_ids.elements.count
  end

  def online_users
    User.where(id: online_user_ids.elements)
  end
  
  # etc...
end
# channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  before_subscribe :set_chat
  before_unsubscribe :set_chat

  def subscribed
    stream_for(@chat)   
    
    @chat.add_online_user(current_user.id)
    # broadcast online_users_count or whatever
  end

  def unsubscribed
    stop_all_streams
    
    @chat.remove_online_user(current_user.id)
    # broadcast online_users_count or whatever
  end

  private

  def set_chat
    @chat = Chat.find(params[:id])
  end
end
// packs/application.js
import { cable } from "@hotwired/turbo-rails"
window.cable = cable
// controllers/chat_channel_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static values = { id: String }

  async connect() {
    this.subscription = await window.cable.subscribeTo(this.channel, { 
      received: this.dispatchMessageEvent.bind(this) 
    })
  }

  disconnect() {
    if (this.subscription) this.subscription.unsubscribe()
  }

  dispatchMessageEvent(data) {
    Turbo.renderStreamMessage(data)
  }

  get channel() {
    const channel = "ChatChannel"
    const id = this.idValue
    return { channel, id }
  }
}

Starting there, you can access thru @chat to online users present on it. I wish it will help you a little bit.

Cordialement,

Hi,

@culov, reading again your question, I answer a bit more.

So, you want to broadcast chat’s messages only when one, or more, admins are online which mean connected to the chat’s channel.

# models/message.rb
class Message < ApplicationRecord
  include Turbo::Streams::ActionHelper

  belongs_to :chat

  # NOTE: we use turbo_stream thru a channel but we send it by our own
  after_create_commit :broadcast_append, if :admins_online?
  after_update_commit :broadcast_update, if :admins_online?
  after_destroy_commit :broadcast_remove, if :admins_online?
  
  private 

  # NOTE: do each for multiple admin... up to you
  def admins_online?
    ADMIN_USER_ID = "123"
    chat.online_user_ids.elements.include?(ADMIN_USER_ID)
  end

  # NOTE: we can move this code in a worker; normally, hotwire do it without writing a worker
  def broadcast_append
    html = ApplicationController.renderer.render partial: 'messages/message', locals: { message: self }
    html = turbo_stream_action_tag("append", target: "message_list", template: html)
    ChatChannel.broadcast_to(chat, html)
  end

  def broadcast_update
    html = ApplicationController.renderer.render partial: 'messages/message', locals: { message: self }
    html = turbo_stream_action_tag("replace", target: "message_#{id}", template: html)
    ChatChannel.broadcast_to(chat, html)
  end

  def broadcast_remove
    html = turbo_stream_action_tag("remove", target: "message_#{id}")
    ChatChannel.broadcast_to(chat, html)
  end
end
<%# admin/messages/index.html.erb %>
<div data-controller="chat-channel" data-chat-channel-id-value="<%= @chat.id %>" hidden></div>

<div id="message_list">
  <%= render partial: "messages/message", collection: @chat.messages %>
</div>
<%# admin/messages/_message.html.erb %>
<div id="<%= dom_id(message) %>">
  ...
</div>

Bref.