Actioncable broadcasts not hitting Received JS function - ruby-on-rails

I have been trying to get a rails app together to replace a nastily coded php monstrosity. The current incarnation is pulling data from a non-Rails db, putting it into the Rails db and then displaying it in the views. The db is mainly populated with temperature readings that are added every few seconds. I can display a static-ish page in Rails with no problems but, trying to add ActionCable/realtime data has proven problematic. MOST things seem to be working properly but, when I broadcast to my channel, it does not seem to hit the Received function in the mychannel.coffee.
My Setup:
Server - passenger (bundle exec passenger start)
Jobs - Resque
ActionCable - Redis
Data is imported from the legacydb by a job that grabs the raw SQL and creates new records. After this, another job broadcasts to the channel.
The problems are coming from ActionCable, I think. All examples that I can find require user input to trigger the JS, it seems. However, I am trying to trigger things strictly from the server side. This job:
class DatabroadcastJob < ApplicationJob
queue_as :default
self.queue_adapter = :resque
def perform
ActionCable.server.broadcast 'dashboard_channel', content: render_thedata
end
private
def render_thedata
dataArr =[Data1.last, Data2.last, Data3.last]
ApplicationController.renderer.render(partial:
'dashboard/data_tables', locals: {item: dataArr})
end
end
Works. It works. I see the broadcast hitting the dashboard_channel. However, nothing in the dashboard.coffee gets triggered by the broadcast. This is incredibly confusing.
Dashboard.coffee
App.dashboard = App.cable.subscriptions.create "DashboardChannel",
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) ->
# Called when there's incoming data on the websocket for this channel
alert data['content']
Nothing happens. The logs show the broadcast but nothing hits dashboard.coffee and raises an alert in browser. Am I thinking about this the wrong way because of all of the chat examples? Is there another place where I grab the broadcast and push it to subscribers when only making server side changes?
If any other info is needed to address this, please let me know. This issue has been driving me mental for days now.

First, check your frames. Are you sure you're getting the messages you want?
Then, in your channel you should set an ID to your subs. If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
class DashboardChannel < ApplicationCable::Channel
def subscribed
post = Post.find(params[:id])
stream_for post
end
end
Then you can broadcast to your channel like so
DashboardChannel.broadcast_to(#post, #comment)
Otherwise, you should do the following:
class DashboardChannel < ApplicationCable::Channel
def subscribed
stream_from 'dashboard_channel'
end
end
But this is a bad practice, because you won't be able to define which user transmits to your server.

One thing I would add for troubleshooting and testing the coffee/javascript is that console.log is your friend. Adding console.log "First step complete" and so on throughout really helped to trackdown where the errors were occurring.

Related

Send message en hook success - Delayed_job

How can I send a message when the job finishes successfully? I would like to send the message and show it in a swal in javascript when the work finishes correctly, but I do not know how to do this, any suggestions?
I do not need to do anything other than send a message
class CompileProjectJob < Struct.new(:url)
def perform
end
def success(job)
#send message when the work is successful
end
end
At the end of perform method queue new delayed job for sending the message
class CompileProjectJob < Struct.new(:url)
def perform
# the code here of this job
# queue new job
end
end
the code of the perform method is executed sequentially as any regular code
Update
to send the message to the front end there are two ways (push and pull) more info
- push: using web sockets you push the message from the backend to the front end
- pull: the front end sends requests every certain period to check if the backend has a new data
and you can use any of these techniques to solve the problem
if you used pulling you will make the job update a data store as an example Redis or mysql. the front end will send a request every interval to check for the new data in some scenarios this will be a better solution but i think you are looking for the other technique
pushing:
here you can use something like active cable https://guides.rubyonrails.org/action_cable_overview.html
or a third party like pusher https://www.pusher.com/tutorials/realtime-table-ruby-rails
the main idea here your frontend app will open a websocket connection with your server. this socket will stay opened and listen for any updates from the backend through a channel so when you send the update after finishing the job through this channel it will be received by the front end so you can add code to show the message

Incomprehension about the first example in Rails' Guide for action cable

I am trying to add the User Appearances example (from the Rails' Guide : https://guides.rubyonrails.org/action_cable_overview.html#example-1-user-appearances ) in my app but I don't understand this part :
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
def subscribed
current_user.appear
end
def unsubscribed
current_user.disappear
end
def appear(data)
current_user.appear(on: data['appearing_on'])
end
def away
current_user.away
end
end
If someone has an explanation for the following sentence : "That appear/disappear API could be backed by Redis, a database, or whatever else." (Just above this part of code in the Rails' Guide).
I try several options, as adding a method "appear" in my model User which change on "true" a database value from my model User, but the subscribed definition call current_user.appear and then the appear definition call current_user.appear(with_param) generates a conflict ...
There is probably something I don't understand but I don't see exactly what is it ...
Thank you very much for your answers.
The sentence about "appear/disappear API backing" - means that ActionCable does not care where and how you are storing and handling users statuses - you may store only a flag or more data in database (like last seen chatroom, last seen time etc.), you may store similar data in redis or any other place you like.
(un)subscribed methods are caller by ActionCable itself upon user (dis)connection to that channel(usually this happens on page load and after navigating away/closing - and while page is open in browser it does not necessary mean that user is actually near their device), while appear/away are actions that are called from clientside js via calling perform("action_name_here") on the channel.
Example assumes that clientside code will detect user presence and send updates.

Actioncable with infinite channel streams

Basically I am trying to add live chatrooms to my Rails website. In my app there are 2 models: Client and Professionnel. Clients can ask quotes to professionels. (Edit: Quote is a model too)
For each quote I want to have a live chat available between client and professionnel.
I have already done it with a classic Message channel, appending the right message to the right quote. Yet it doesn't fulfill privacy principle as every Client and every Professionnel receive all chat messages.
At server level, I can easily subscrible the user (Client or Professionnel) to a quote specific stream like :
class QuoteChannel < ApplicationCable::Channel
def subscribed
if current_user
if current_user.quotes.any?
current_user.quotes.each do |quote|
stream_from "#{quote.hashed_id.to_s}_channel"
end
end
end
end
def unsubscribed
end
end
Though I am a bit stuck for the client side. My quote.coffee.erb doesn't accept that I use current_user (as defined in Actioncable connection file) or any Devise identifier current_client or current_professionnel.
So I am not sure how I can personnalize my subscriptions on the client side. I have read that it is possible to catch an element broadcast in the message but I am not sure how I can do with my current Coffee.erb file :
App.quote = App.cable.subscriptions.create "QuoteChannel",
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) ->
$('#cell'+data.quoteid).append '<div>'+'<span>'+data.message+'</span>'+'</div>'
$('.sendmessageinputtext').val("")
quoteid is passed to the received function but I need to create as many streams as the user owns quotes. In this thread http://www.thegreatcodeadventure.com/rails-5-action-cable-with-multiple-chatroom-subscriptions/ the tutor iterates across all available Chatrooms, I could do the same but it is stupid as there may be thousands of live quotes at a certain time, of which only a few are owned by the current_user which has been allowed an Actioncable connection.

What can be the reason of "Unable to find subscription with identifier" in Rails ActionCable?

I'm building a messenger application using Rails 5.0.0.rc1 + ActionCable + Redis.
I've single channel ApiChannel and a number of actions in it. There are some "unicast" actions -> ask for something, get something back, and "broadcast" actions -> do something, broadcast the payload to some connected clients.
From time to time I'm getting RuntimeError exception from here: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/subscriptions.rb#L70 Unable to find subscription with identifier (...).
What can be a reason of this? In what situation can I get such exception? I spent quite a lot of time on investigating the issue (and will continue to do so) and any hints would be greatly appreciated!
It looks like it's related to this issue: https://github.com/rails/rails/issues/25381
Some kind of race conditions when Rails reply the subscription has been created but in fact it hasn't been done yet.
As a temporary solution adding a small timeout after establishing the subscription has solved the issue.
More investigation needs to be done, though.
The reason for this error might be the difference of the identifiers you subscribe to and messaging to. I use ActionCable in Rails 5 API mode (with gem 'devise_token_auth') and I faced the same error too:
SUBSCRIBE (ERROR):
{"command":"subscribe","identifier":"{\"channel\":\"UnreadChannel\"}"}
SEND MESSAGE (ERROR):
{"command":"message","identifier":"{\"channel\":\"UnreadChannel\",\"correspondent\":\"client2#example.com\"}","data":"{\"action\":\"process_unread_on_server\"}"}
For some reason ActionCable requires your client instance to apply the same identifier twice - while subscribing and while messaging:
/var/lib/gems/2.3.0/gems/actioncable-5.0.1/lib/action_cable/connection/subscriptions.rb:74
def find(data)
if subscription = subscriptions[data['identifier']]
subscription
else
raise "Unable to find subscription with identifier: #{data['identifier']}"
end
end
This is a live example: I implement a messaging subsystem where users get the unread messages notifications in the real-time mode. At the time of the subscription, I don't really need a correspondent, but at the messaging time - I do.
So the solution is to move the correspondent from identifier hash to the data hash:
SEND MESSAGE (CORRECT):
{"command":"message","identifier":"{\"channel\":\"UnreadChannel\"}","data":"{\"correspondent\":\"client2#example.com\",\"action\":\"process_unread_on_server\"}"}
This way the error is gone.
Here's my UnreadChannel code:
class UnreadChannel < ApplicationCable::Channel
def subscribed
if current_user
unread_chanel_token = signed_token current_user.email
stream_from "unread_#{unread_chanel_token}_channel"
else
# http://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#class-ActionCable::Channel::Base-label-Rejecting+subscription+requests
reject
end
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def process_unread_on_server param_message
correspondent = param_message["correspondent"]
correspondent_user = User.find_by email: correspondent
if correspondent_user
unread_chanel_token = signed_token correspondent
ActionCable.server.broadcast "unread_#{unread_chanel_token}_channel",
sender_id: current_user.id
end
end
end
helper: (you shouldn't expose plain identifiers - encode them the same way Rails encodes plain cookies to signed ones)
def signed_token string1
token = string1
# http://vesavanska.com/2013/signing-and-encrypting-data-with-tools-built-in-to-rails
secret_key_base = Rails.application.secrets.secret_key_base
verifier = ActiveSupport::MessageVerifier.new secret_key_base
signed_token1 = verifier.generate token
pos = signed_token1.index('--') + 2
signed_token1.slice pos..-1
end
To summarize it all you must first call SUBSCRIBE command if you want later call MESSAGE command. Both commands must have the same identifier hash (here "channel"). What is interesting here, the subscribed hook is not required (!) - even without it you can still send messages (after SUBSCRIBE) (but nobody would receive them - without the subscribed hook).
Another interesting point here is that inside the subscribed hook I use this code:
stream_from "unread_#{unread_chanel_token}_channel"
and obviously the unread_chanel_token could be whatever - it applies only to the "receiving" direction.
So the subscription identifier (like \"channel\":\"UnreadChannel\") has to be considered as a "password" for the future message-sending operations (e.g. it applies only to the "sending" direction) - if you want to send a message, (first send subscribe, and then) provide the same "pass" again, or you'll get the described error.
And more of it - it's really just a "password" - as you can see, you can actually send a message to whereever you want:
ActionCable.server.broadcast "unread_#{unread_chanel_token}_channel", sender_id: current_user.id
Weird, right?
This all is pretty complicated. Why is it not described in the official documentation?

Rails 5 app (Action Cable) as Socket.io server AND client

I am now familiar with Action Cable (Rails 5 functionality) as an emitter or server of websockets. However, I am supposed to consume an API which sends the data over websockets (e.g. 'socket.provider.com?token=12345').
I made some tests with a plain Ruby file using socket.io-client-simple (https://github.com/shokai/ruby-socket.io-client-simple) and it works, but I am not sure on how this would work on a deployed Rails app. I'm guessing I need a separate process which listens constantly for events emitted by the API provider. Has anyone done something similar? I am going to use Heroku for deployment.
Note: I think using a client-side approach for receiving the websockets and then posting them to my Rails app (i.e. Javascript Socket.IO library) is not an option, since I need to receive AND persist some of the data coming from the events in real time and not depend of the connectivity of at least one client.
I'm also wondering if there is any way to automatically set Action Cable to act as a 'listener of events' somehow. Haven't read anything on that topic so far, but would love to see some suggestions on that direction.
Update: Here is the Ruby code I'm using so far to connect to the provider's websockets API:
require 'rubygems'
require 'socket.io-client-simple'
socket = SocketIO::Client::Simple.connect 'https://api.provider.com', token: '12345'
socket.on :connect do
puts "connect!!!"
end
socket.on :disconnect do
puts "disconnected!!"
end
socket.on :providerevent do |data|
puts data
end
socket.on :error do |err|
p err
end
puts "please input and press Enter key"
loop do
sleep 100
end
ActionCable can't be listening to an external site's event on its own, so you'll have to combine socket.io and ActionCable.
ActionCable can send updates to the channel like this:
ActionCable.server.broadcast "channel_name", param1: your_param1, param2: your_param2
to update the channel when an event occured. The
received action of your channel's coffeescript file is where you have to do something with it.
From what I understand, you're looking for something like this in the controller where you would be listening for events:
def listen
socket.on :connect do
# persist your data
# example: #post = Post.create(...)
ActionCable.server.broadcast "channel_name", post_title: #post.title
end
end
and in your channel_name.coffee:
received: (data) ->
console.log(data["post_title"])
# do something
With this setup, you would be receiving events from the api, and broadcasting it to your channel. The page were the channel is setup would be updated each time your socket receives an event.
You should first follow DDH's tutorial, and then you'll probably understand better my solution (which is pretty easy to implement).

Resources