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?
Related
There is one place in the app that uses the ActionCable to update the content (post page). This functionality worked and there was no point in testing it locally every time.
But yesterday I found that it doesn't work, but only locally. On the server, the same application code works successfully.
I've double-checked everything more than ten times, the problem is not on the back end. On the side of the back end job or worker from Sidekiq, they successfully execute the code:
ActionCable.server.broadcast(
"post_update:#{post.to_gid_param}",
{
post_id: post.id
}.to_json
)
And stream_from successfully executed in subscribed in PostUpdateChannel.
And even on the front end there is a subscription to PostUpdateChannel. That is, the front end shows me what it always showed in the browser console:
PostUpdateChannel: connected()
channels: {
[CHANNEL_NAME]: {
connected() {
console.info(
`${CHANNEL_NAME}: connected()`
)
},
// ...
But this method stopped working:
received(data) {
const json = JSON.parse(data)
console.info(
`${CHANNEL_NAME}: received()`,
json
)
// ...
But at the same time, the same code works successfully on the server...
In general, my question is - what could it be? I see no errors. Back end works successfully. The front end also seems to work, but it is precisely locally that it cannot receive data from the back end 🤯
Rails 6.1.3.2, ActionCable 6.1.3.2, Webpacker 5.4.0
#rails/webpacker 5.4.0, #rails/actioncable 6.1.3, actioncable-vue 2.4.6
The problem was solved by replacing this:
# config/cable.yml
development:
adapter: async
On this:
# config/cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
We've successfully implemented real time updates in our app using ActionCable in Rails and implemented the consumer as a client service in Ember CLI, but am looking for a better, less-expensive approach.
app/models/myobj.rb
has_many :child_objs
def after_commit
ActionCable.server.broadcast("obj_#{self.id}", model: "myobj", id: self.id)
self.child_objs.update_all foo: bar
end
app/models/child_obj.rb
belongs_to :myobj
def change_job
self.job = 'foo'
self.save
ActionCable.server.broadcast("obj_#{self.myobj.id}", model: "child_obj", id: self.id)
end
frontend/app/services/stream.js
Here we're taking the model and id data from the broadcast and using it to reload from the server.
import Ember from 'ember';
export default Ember.Service.extend({
store: Ember.inject.service(),
subscribe(visitId) {
let store = this.get("store")
MyActionCable.cable.subscriptions.create(
{channel: "ObjChannel", id: objId}, {
received(data) {
store.findRecord(data.model, data.id, {reload: true});
}
}
);
},
});
This approach "works" but feels naïve and is resource intensive, hitting our server again for each update, which requires re-authenticating the request, grabbing data from the database, re-serializing the object (which could have additional database pulls), and sending it across the wire. This does in fact cause pool and throttling issues if the number of requests are high.
I'm thinking we could potentially send the model, id, and changeset (self.changes) in the Rails broadcast, and have the Ember side handle setting the appropriate model properties. Is this the correct approach, or is there something else anyone recommends?
You should be fine with sending whole entity payload with your change event via sockets. Later you can push payload to the store - create new records or update existing. This way you'll avoid additional server requests.
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 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.
Maybe it is a good example for server push system. There are many users in the system, and users can talk with each other. It can be accomplished like this: one user sends message(through websocket) to the server, then the server forward the message to the other user. The key is to find the binding between the ws(websocket object) and the user. The example code like below:
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
ws.onopen { |handshake|
# extract the user id from handshake and store the binding between user and ws
}
ws.onmessage { |msg|
# extract the text and receiver id from msg
# extract the ws_receiver from the binding
ws_receiver.send(text)
}
end
}
I want to figure out following issues:
The ws object can be serialized so it can be stored into disk or database? Otherwise I can only store the binding into memory.
What the differences between em-websocket and websocket-rails?
Which gem do you recommend for websocket?
You're approaching a use case that websockets are pretty good for, so you're on the right track.
You could serialize the ws object with Marshal, but think of websocket objects as being a bit like http request objects in that they are abstractions for a type of communication. You are probably best off marshaling/storing the data.
em-websocket is a lower(ish) lever websocket library built more or less directly on web-machine. websocket-rails is a higher level abstraction on websockets, with a lot of nice tools built in and pretty ok docs. It is built on top of faye-websocket-rails which is itself built on web machine. *Note, action cable which is the new websocket library for Rails 5 is built on faye.
I've use websocket-rails in the past and rather like it. It will take care of a lot for you. However, if you can use Rails 5 and Action Cable, do that, its the future.
The following is in addition to Chase Gilliam's succinct answer which included references to em-websocket, websocket-rails (which hadn't been maintained in a long while), faye-websocket-rails and ActionCable.
I would recommend the Plezi framework. It works both as an independent application framework as well as a Rails Websocket enhancement.
I would consider the following points as well:
do you need the message to persist between connections (i.e. if the other user if offline, should the message wait in a "message box"? for how long should the message wait?)...?
Do you wish to preserve message history?
These points would help yo decide if to use a persistent storage (i.e. a database) for the messages or not.
i.e., to use Plezi with Rails, create an init_plezi.rb in your application's config/initializers folder. use (as an example) the following code:
class ChatDemo
# use JSON events instead of raw websockets
#auto_dispatch = true
protected #protected functions are hidden from regular Http requests
def auth msg
#user = User.auth_token(msg['token'])
return close unless #user
# creates a websocket "mailbox" that will remain open for 9 hours.
register_as #user.id, lifetime: 60*60*9, max_connections: 5
end
def chat msg, received = false
unless #user # require authentication first
close
return false
end
if received
# this is only true when we sent the message
# using the `broadcast` or `notify` methods
write msg # writes to the client websocket
end
msg['from'] = #user.id
msg['time'] = Plezi.time # an existing time object
unless msg['to'] && registered?(msg['to'])
# send an error message event
return {event: :err, data: 'No recipient or recipient invalid'}.to_json
end
# everything was good, let's send the message and inform
# this will invoke the `chat` event on the other websocket
# notice the `true` is setting the `received` flag.
notify msg['to'], :chat, msg, true
# returning a String will send it to the client
# when using the auto-dispatch feature
{event: 'message_sent', msg: msg}.to_json
end
end
# remember our route for websocket connections.
route '/ws_chat', ChatDemo
# a route to the Javascript client (optional)
route '/ws/client.js', :client
Plezi sets up it's own server (Iodine, a Ruby server), so remember to remove from your application any references to puma, thin or any other custom server.
On the client side you might want to use the Javascript helper provided by Plezi (it's optional)... add:
<script src='/es/client.js' />
<script>
TOKEN = <%= #user.token %>;
c = new PleziClient(PleziClient.origin + "/ws_chat") // the client helper
c.log_events = true // debug
c.chat = function(event) {
// do what you need to print a received message to the screen
// `event` is the JSON data. i.e.: event.event == 'chat'
}
c.error = function(event) {
// do what you need to print a received message to the screen
alert(event.data);
}
c.message_sent = function(event) {
// invoked after the message was sent
}
// authenticate once connection is established
c.onopen = function(event) {
c.emit({event: 'auth', token: TOKEN});
}
// // to send a chat message:
// c.emit{event: 'chat', to: 8, data: "my chat message"}
</script>
I didn't test the actual message code because it's just a skeleton and also it requires a Rails app with a User model and a token that I didn't want to edit just to answer a question (no offense).