How to transmit initial payload to the client in ActionCable subscribed callback? - ruby-on-rails

I have Ruby on Rails application which is using ActionCable for real-time client communication. I have ScoreChannel which is responsible for streaming user score updates:
class ScoreChannel < ApplicationCable::Channel
def self.broadcast_name(user:)
"score_#{user.guid}"
end
def broadcast_name
self.class.broadcast_name(user: current_user)
end
def subscribed
stream_from(broadcast_name)
ActionCable.server.broadcast(broadcast_name, Cable::V1::ScoreSerializer.new(current_user).as_json)
end
end
I am trying to send user's current score right after a user subscribes to the channel (see ScoreChannel#subscribed method). Unfortunately, due to asynchronous nature of ActionCable, score broadcast goes before user subscribes to the ScoreChannel. Therefore user does not receive the initial score payload (because he is not subscribed to this channel yet). This is redis monitor timeline:
1472430005.884742 [1 172.18.0.6:50242] "subscribe" "action_cable/Z2lkOi8vYXBwLWVuZ2luZS9Vc2VyLzE"
1472430010.988077 [1 172.18.0.6:50244] "publish" "score_cc112e3fdfb5411a965a31f9468abf98" "{\"score\":17,\"rank\":1}"
1472430010.988773 [1 172.18.0.6:50242] "subscribe" "score_cc112e3fdfb5411a965a31f9468abf98"
What is the best way to solve this?

Instead of using ActionCable.server.broadcast to broadcast to the existing subscribers, try using the transmit-method, like this:
transmit(Cable::V1::ScoreSerializer.new(current_user).as_json)
This only sends the status to the current subscriber being handled, but I figured this is what you wanted.

use transmit inside a channel class/instance, ref ActionCable::Channel::Base#transmit
use broadcast outside channel class, ref ActionCable::Server#broadcast

Related

Ruby on Rails 6: Why doesn't receive(data) method work in ActionCable of my application?

I'm working on a real time chat application in Rails 6 with Windows OS, and my ActionCable has an issue.
The development adapter doesn't work at all(I guess), neither async, neither Redis. I tried everything but I am really stuck at this point :(.
I have a channel called 'room', with the following coding on it's back-end side (app/channels/room_channel.rb):
class RoomChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
reject unless params[:room_id]
room = Room.find params[:room_id].to_i
stream_for room
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
And in its front-end side: (app/javascript/room_channel.js):
import consumer from "./consumer"
let url = window.location.href;
let room_id = parseInt(url.substring(url.search("rooms/") + 6) );
if (url.indexOf("rooms/") != -1) {
console.log('Subscribed to room', room_id);
consumer.subscriptions.create({ "channel": "RoomChannel", "room_id": room_id }, {
connected() {
console.log('connected')
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
console.log('data received successfully')
// Called when there's incoming data on the websocket for this channel
}
});
}
When I run the server I am able to subscribe and connect to the channel, but the channel cannot receive any incoming data (in my case, messages). I know it because it doesn't output the console message ('data received successfully') when I create a new message in the room.
Other important information is when my co-worker runs this application with his computer with the same coding everywhere, he can receive data (he gets the 'data received successfully' output when he sends a message to the room). And as I said, we have the same exact coding everywhere!
So I am sure about the fact that it is not the code's fault, the problem is with my computer or I don't know.
Can anybody help me with this problem? Thanks for reading and waiting for the helpful people's answers! :)

ActionCable display correct number of connected users (problems: multiple tabs, disconnects)

I'm a Rails beginner building a quiz webapp. Currently I'm using ActionCable.server.connections.length to display how many people are connected to my quiz, but there are multiple problems:
When the page is reloaded half of the time the old connection is not disconnected properly by ActionCable, so the number keeps rising even though it shouldn't
it only gives you the current number of connections for the specific thread this is called in, as #edwardmp pointed out in this actioncable-how-to-display-number-of-connected-users thread (which also means, that the number of connections displayed for the quiz hoster, which is one type of user in my app, can vary from the number of connections displayed to a quiz participant)
When a user connects with multiple browser windows each connection is counted seperately which falsely inflates the number of participants
And last but not least: it would be great to be able to display the number of people connected per room of my channel, instead of across all rooms
I've noticed that most answers concerning this topic use a Redis server, so I was wondering if that is generally recommended for what I'm trying to do and why. (e.g. here: Actioncable connected users list )
I currently use Devise and cookies for my authentication.
Any pointers or answers to even part of my questions would be greatly appreciated :)
I finally at least got it to work for counting all users to the server (not by room) by doing this:
CoffeeScript of my channel:
App.online_status = App.cable.subscriptions.create "OnlineStatusChannel",
connected: ->
# Called when the subscription is ready for use on the server
#update counter whenever a connection is established
App.online_status.update_students_counter()
disconnected: ->
# Called when the subscription has been terminated by the server
App.cable.subscriptions.remove(this)
#perform 'unsubscribed'
received: (data) ->
# Called when there's incoming data on the websocket for this channel
val = data.counter-1 #-1 since the user who calls this method is also counted, but we only want to count other users
#update "students_counter"-element in view:
$('#students_counter').text(val)
update_students_counter: ->
#perform 'update_students_counter'
Ruby backend of my channel:
class OnlineStatusChannel < ApplicationCable::Channel
def subscribed
#stream_from "specific_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
#update counter whenever a connection closes
ActionCable.server.broadcast(specific_channel, counter: count_unique_connections )
end
def update_students_counter
ActionCable.server.broadcast(specific_channel, counter: count_unique_connections )
end
private:
#Counts all users connected to the ActionCable server
def count_unique_connections
connected_users = []
ActionCable.server.connections.each do |connection|
connected_users.push(connection.current_user.id)
end
return connected_users.uniq.length
end
end
And now it works! When a user connects, the counter increments, when a user closes his window or logs off it decrements. And when a user is logged on with more than 1 tab or window they are only counted once. :)

Rails 5 ActionCable setup and workflow

I am new to ActionCable and sockets and trying to implement some real time features.
I successfully implemented real time notifications functionality (basic one) into my app, however there are a couple of things on which i spent some time to understand.
My Real time notifications code:
The authentification process:
# Connection.rb (using Devise)
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = env['warden'].user
reject_unauthorized_connection unless self.current_user
logger.add_tags 'ActionCable', current_user.email
end
end
end
Notification Channel:
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notification_channel_#{current_user.id}"
end
def unsubscribed
end
end
Notifications.coffee
App.notifications = App.cable.subscriptions.create "NotificationsChannel",
connected: ->
disconnected: ->
received: (data) ->
this.update_counter(data.counter)
# if use_sound is "true" play a mp3 sound
# use_sound is sent ("true" or "false") because when a notification is destroyed we don't want to use sound only update counter
if data.use_sound == "true"
$("#sound-play").get(0).play()
# Update Notifications Counter and change color
update_counter: (counter) ->
$("#notifications-counter").html("#{counter}")
if counter > 0
$("#notifications-counter").css('color','#FFA5A5')
else
$("#notifications-counter").css('color','white')
And there is the Job file where we broadcast to the channel with Notifications Counter
Note: I only send notifications count because in the nav-bar when the user clicks the (ex. 2 Notifications) - button, a dropdown is toggled and populated via AJax call.
My questions are:
Is it normal to have all the time 'GET /cable' requests with the response 'An unauthorized connection attempt was rejected' when a user is not logged in ( I understand that the current_user in the Connect.rb file is set from the cookie, but when a user is on ex Home page, Logg-in page, etc.. basically not authenticated.
In the server logs I can see every 1,2 seconds the GET /cable
Is this normal? I'm thinking that somehow I have to stop this if the user logs out or something.
When I start the server I get the following error for like 10 seconds till the server starts in the browsers console. Since the notifications work, I don't know if this is a problem of not, but an error it is.
WebSocket connection to 'ws://localhost:3000/cable' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED
I even tried setting config.action_cable.mount_path = "/cable" or "ws://localhost:3000/cable" and did not fix it.
Next I want to implement a chat, where 2 users can have a private conversation, is the "current_user" defined in the Connection.rb 'secure', how can I add authorization to ActionCable or something like. In the rest of my app I'm using CanCan Gem and works perfectly.
Thank you very much for the time invested in reading the question. If you have answers for my questions I will really appreciate them. And if you have some directions to give or tips for building a complex and secure chat with ActionCable I would love to hear them. Thank you!
I think what you want to be looking at is the cable.js file where the createConsumer function is called.
Creating a consumer is what subscribers a user to the channel. I think you need to focus on when this code gets called.
You are using user authentication's presence to reject unauthorised use.
This is very good, however not so great when the JS consumer is created for any visitor (i.e. cable.js is loaded into application.js for all pages).
What you can do is use content_for into a seperate layout for authenticated users that calls the cable.js to create a consumer and ensure that cable.js does not get included into unauthorised pages.

Rails 4 live stream + subscription based updates

I have a few questions around using ActionController::Live and streaming notifications to users.
Is there a way to tell if/when a stream is closed or no longer writable?
Is there a reason to manually close the stream in your action? I've seen some examples where they would explicitly close their stream, but watching the browser, it seems that it handles that for me?
I have a user notification class that my actions can subscribe to. In the create action of my users controller I tell the notification class to broadcast the new user.
In order to facilitate this, I've had to leave the stream open. I also have no idea when to unsubscribe from the user notifier as I don't know when the stream closes (via the browser initiating it).
#EventsController
def users
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream)
sse.write({}, event: :init)
UserNotifier.sub { |user| sse.write({ name: user.name, id: user.id }, event: :new_user ) }
end
So this just leaves a persistant connection open and then once something triggers the broadcast message on the UserNotifier, any one subbed to it will have it's block triggered.
The only issue again, is that I have no idea when to unsubscribe and in my block you see that I just explicitly call sse.write (wraps the response stream), which in turn blindly calls stream.write without knowing its status.
update
I've noticed some people rescue an IOError exception as a way to tell when a client has disconnected. Is that really the best way?
rescue IOError
UserNotifier.unsub(...)
...
You can use ensure to ensure that your stream gets closed once writing has failed or the connection has closed or something has gone amiss:
def your_action
response.headers['Content-Type'] = 'text/event-stream'
begin
# Do your writing here: response.stream.write("data: ...\n\n")
rescue IOError
# When the client disconnects, writing will fail, throwing an IOError
ensure
response.stream.close
end
end

How to prevent spam when using websocket-rails gem?

Consider the following to implement tic-tac-toe:
One player sends a move by triggering an event on the main dispatcher.
var dispatcher = new WebSocketRails('localhost:3000/websocket');
var channel = dispatcher.subscribe_private('private_game');
channel.bind('new_move', function(move) {
// received new move, process it
});
// later on when we want to send a move to the server we run the following code
var move = {
square: ...;
}
dispatcher.trigger('move', move);
On the server the controller can verify that the user is authorized for that specific game of tic-tac-toe. And then it can broadcast the move to both players.
class TicTacToeController < WebsocketRails::BaseController
def move
# code to verify the move is valid and save to database
...
# broadcast move to all players
WebsocketRails[:private_game].trigger(:new_move, message)
end
end
But there is nothing to enforce that the client sends messages only using the main dispatcher. The 'private_game' channel is suppose to be used only by the server for broadcasting moves. But a hostile client could send random data on it with
channel.trigger('new_move', randomdata);
Since channel events do not go through the Event Router and thus don't go through a Controller action, there is nothing on the server side to filter out the random spam.
Is there a way to stop random data spam on the server? Perhaps I'm misunderstanding how to use websocket-rails?
One way you could handle this now before the Gem is updated to support this better would be to use a separate private channel for each user.
Example
Game Starts
User A connects to private channel named user_a_incoming_moves
User B connects to private channel named user_b_incoming_moves
When user B makes a move, you broadcast that on the user_a_incoming_moves private channel that only User A is connected to.
When user A makes a move, you broadcast that on the user_b_incoming_moves channel that only User B is connected to.
This would prevent anyone from being able to send malicious moves through.
You can read more about private channels in the Private Channel Wiki.

Resources