I created the following using ActionCable but not able to receive any data that is being broadcasted.
Comments Channel:
class CommentsChannel < ApplicationCable::Channel
def subscribed
comment = Comment.find(params[:id])
stream_for comment
end
end
JavaScript:
var cable = Cable.createConsumer('ws://localhost:3000/cable');
var subscription = cable.subscriptions.create({
channel: "CommentsChannel",
id: 1
},{
received: function(data) {
console.log("Received data")
}
});
It connects fine and I can see the following in the logs:
CommentsChannel is streaming from comments:Z2lkOi8vdHJhZGUtc2hvdy9FdmVudC8x
I then broadcast to that stream:
ActionCable.server.broadcast "comments:Z2lkOi8vdHJhZGUtc2hvdy9FdmVudC8x", { test: '123' }
The issue is that the received function is never called. Am I doing something wrong?
Note: I'm using the actioncable npm package to connect from a BackboneJS application.
Changing the cable adapter from async to redis in config/cable.yml fixed it for me.
Update
As Erem pointed out below, the server and console are isolated processes so you need to use a centralized queue manager.
Related
I realize this is a bit of a non-specific question, but I'm not exactly sure where my issue lies, so bear with me please.
I am attempting to setup Action Cable in Rails 6.1.4.1 on Sprockets (no webpacker integration). After setting up a basic channel, i am getting no result; and also no errors or anything to debug, so I really am not sure whre my issue lies.
Here's a look at the integration:
# config/cable.yml
development:
adapter: async
# channels/notification_channel.rb
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_from "notification_channel"
end
# app/javascript/channels/notification_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("NotificationChannel", {
connected() {
console.log("Connected to notification channel...");
},
disconnected() {
},
received(data) {
},
notify: function() {
return this.perform('notify');
}
});
# app/javascript/channels/consumer.js
import { createConsumer } from "#rails/actioncable"
export default createConsumer()
# app/javascript/channels/index.js
const channels = require.context('.', true, /_channel\.js$/)
channels.keys().forEach(channels)
On server boot or page load, there is no indication in server logs of an active /cable channel, as well as no output in web console log. App.notification is undefined as well. And again, no errors client-side or server-side.
Does anyone know if this integration should be working normally with Sprockets? or is this just a configuration issue?
I followed this tutorial to create an action cable broadcast but it's not quite working as expected. The channel streams and the web app subscribes successfully, but messages broadcasted from the sidekiq background job are only displayed after refreshing the page. Using the same command on the console does result in an immediate update to the page.
When looking at the frames in chrome's developer mode, I cannot see the broadcasted messages from the background job but can immediately see the ones sent by the console. However, I can confirm that the sidekiq background job is broadcasting those messages somewhere since they do show up upon refresh; however, I don't know where they are being queued.
Are there any additional configuration changes needed to keep the messages from the background job from being queued somewhere? Are there any typos or errors in my code that could be causing this?
Action Cable Broadcast message:
ActionCable.server.broadcast "worker_channel", {html:
"<div class='alert alert-success alert-block text-center'>
Market data retrieval complete.
</div>"
}
smart_worker.rb: -- This is called as perform_async from the controller's action
class SmartWorker
include Sidekiq::Worker
include ApplicationHelper
sidekiq_options retry: false
def perform
ActionCable.server.broadcast "worker_channel", {html:
"<div class='alert alert-success alert-block text-center'>
Market data retrieval complete.
</div>"
}
end
connection.rb:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = current_user #find_verified_user ignored until method implemented correctly and does not always return unauthorized
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
worker_channel:
class WorkerChannel < ApplicationCable::Channel
def subscribed
stream_from "worker_channel"
end
def unsubscribed
end
end
worker.js:
App.notifications = App.cable.subscriptions.create('WorkerChannel', {
connected: function() {
console.log('message connected');
},
disconnected: function() {},
received: function(data) {
console.log('message recieved');
$('#notifications').html(data.html);
}
});
cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: smarthost_production
Also added
to the view but that didn't make a difference.
I'm not sure this is the entire explanation but this is what I have observed through further testing:
After multiple server restarts, the broadcast started working and would log as expected in the development logger. Console messages where still hit or miss, so I added some additional identifiers to the broadcasted messages and identified that they were being broadcasted before the loading of the next page was completed. This caused two things:
1) A quick flashing of flash messages triggered by the broadcast (in what was perceived to be the old page - i.e. only works after a refresh)
2) A lack of or inconsistent behavior in the browser console: Because the sidekiq worker job finished so quick, sometimes even before the browser started rendering the new page, I believe the console messages are being reset by the page loading actions and are therefore not visible when you check the logs (or even if you stare at it for a while).
It seems as though this is working as expected, and is simply working to quickly in the local environment which makes it seem as though it's not working as intended.
ActionChannel normally does not queue messages and those broadcasted when there's no subscriber should be lost. Observed behaviour can happen if notification actually comes later than you expect.
I'd check:
Run entire job in console, not just notification, and see if it's running slow
Check sidekiq queues latency
Add logging before/after notification in job and check logs if the job is actually run successfully
I am creating a very basic chat app. My goal is to have a Rails API backend and then build an IOS, Android, web, and desktop client. This is purely to explore Websockets and mobile development.
I've never used Actioncable, and my knowledge of Websockets is very limited. What I'd like to know is if I can set up Actioncable on my Rails API and have it communicate with Node (for instance).
Does Actioncable act like any other Websocket? Could I connect to it from my Node app through ws://<host>/cable and have a functional pub-sub system between whatever client and Rails?
I'm sorry if that doesn't make sense, I'm having a hard time wording it :)
Thank you!
Indeed you can!
Just like you create any api app, use generator
rails new my_app --api
Create your_channel
rails generate channel your_channel
Add mount path in routes.rb
mount ActionCable.server => '/cable'
Allow stream on subscribe method in /app/channels/your_channel.rb
class YourChannel < ApplicationCable::Channel
def subscribed
stream_from 'messages' # <----- Stream Name Here
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Call ActionCable.server.broadcast from any other part of your app to stream
ActionCable.server.broadcast 'messages', message: 'ping'
Now use your front-end to test it. Since you told you want iOS Android and also mentioned about node, I assume you are using (or would choose to use) react-native
import ActionCable from 'react-native-actioncable';
const cable = ActionCable.createConsumer("ws://localhost:3000/cable");
class YourReactClass extends React.Component {
# add or update the following
componentDidMount = () => {
console.log("componentDidMount executed");
this.subscription = cable.subscriptions.create("OrderChannel", {
connected: function() { console.log("connected: action cable") },
disconnected: function() { console.log("disconnected: action cable") },
received: function (data) { console.log(data) }
}
)
};
componentWillUnmount () {
this.subscription &&
cable.subscriptions.remove(this.subscription)
}
}
And you're good to go, build your logic on top of this... If you got any problems, let me know.
I've setup a websocket on our Rails app, and have been able to connect and receive data on it from the same server.
The way I've done it now is to create a socket like this:
class UsersChannel < ApplicationCable::Channel
def subscribed
stream_from "users_1"
end
def unsubscribed
end
end
and then I use javascript to open the connection with
new WebSocket('wss://domain.com/cable/users_1');
I then broadcast and send JSON from a page in this format:
ActionCable.server.broadcast "users_1", {
store: {
name: store.name,
address: {
full_address: location.address,
latitude: location.latitude,
longitude: location.longitude
}
}
When that's triggered I can see in my console that it appears. I've added this channel in JS:
App.cable.subscriptions.create "UsersChannel",
received: (data) ->
console.log data
Now we're working building an app with React Native and when we add this code to our app:
var ws = new WebSocket('wss://domain.com/cable/users_1');
ws.onmessage = (e) => {
console.log(e.data);
};
we see that it pings, but receive nothing when we trigger the broadcast. I've also added this to the config file:
config.action_cable.url = 'wss://domain.com/cable'
config.action_cable.disable_request_forgery_protection = true
Anybody know why this is happening?
I finally figured out how to solve my issue.
Solving your issue
First thing first, in your case, you are missing the step to subscribe to a channel. You have to send a JSON onopen of your WebSocket like that :
JSON.stringify({
command: 'subscribe',
identifier: JSON.stringify({
channel: 'MyNotificationChannel',
user_id: userId
})
})
This will trigger the MyNotificationChannel class subscribed method passing the user_id as a param (params[:user_id]).
The Origins
Then comes the issue of Origins, which was my issue.
The WebSocket opening fails on Rails side with the message :
Request origin not allowed: https://loc.mydom.com:28080
My app is running on https://loc.mydom.com:3000/ and I have separated running server for cable which listen on port 28080.
Anyways the point here is that the message shows the origin which isn't accepted.
In the environment file (config/environments/development.rb) you just need to add ActionCable.server.config.allowed_request_origins :
Rails.application.configure do
# ...
ActionCable.server.config.allowed_request_origins = [
'https://loc.mydom.com:3000/', # My web front app
'https://loc.mydom.com:28080' # The phone app
]
# ...
end
After this, my WebSocket connects and I'm receiving the updates.
I'm writing a Rails application that uses WebSockets to communicate with other machines (no browser and client side logic in this process). I have a channel:
class MachinesChannel < ApplicationCable::Channel
def subscribed
...
end
def unsubscribed
...
end
def handle_messages
...
end
end
To receive the data the only way I know about is the JavaScript client:
ActionCable.createConsumer('/cable').subscriptions.create 'MachinesChannel',
received: (message) ->
#perform('handle_messages')
I can call server side methods from JS via #perform() method.
Is there any way to omit the JS part and somehow directly handle the incoming data in MachinesChannel?
The ideal situation would be to have the handle_messages method accept a data argument and have this metod called on incoming data.
After looking into ActionCable source code I got the following solution. You just have to create a method in MachinesChannel that you want to be called, e.g. handle_messages(data). Then, in the client that connects to your websocket, you need to send a message in the following format (example in ruby):
id = { channel: 'MachinesChannel' }
ws = WebSocket::Client::Simple.connect(url)
ws.send(JSON.generate(command: 'message', identifier: JSON.generate(id), data: JSON.generate(action: 'handle_messages', foo: 'bar', biz: 'baz')))
action has to be the name of the method you want to be called in MachinesChannel. The rest of key-values are whatever you want. This the date you can receive in the ActionCable channel.
Recently a gem action_cable_client has been release which seems exactly perfect for this kind of usage. I haven't used it, so I don't know how it really works.
Instead of:
def handle_messages
...
end
This works for me:
def receive(data)
puts data
...
end