Action Cable and AngularJS Cross Domain Integration - ruby-on-rails

I currently have a Rails 5 application acting as my backend, we can call this the "Core." I also have another Rails 5 application acting as my frontend, which is serving up AngularJS client side, we can call this the "Front". These are two completely separate Rails 5 applications with completely different domains.
Basically, I am trying to integrate Action Cable through the Core and have it talk to the Front. I'm using this service here for the Front: https://www.npmjs.com/package/angular-actioncable. As far as the Core, that's just basic Action Cable set up.
Problem: I'm having some trouble getting the handshake to work across two separate domains. I've read through everything I can find online, unfortunately there is not much information. If you've done this before, please help!
Note: I do have Redis server running and I'm using separate ports to mimic the separate domains in development.
Core:
chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from 'http://localhost:2000/#/chat'
end
def unsubscribed
stop_all_streams
end
def receive(data)
ActionCable.server.broadcast('http://localhost:2000/#/chat', data)
end
def speak
params.fetch('data').fetch('chat')
end
end
route.js
mount ActionCable.server => '/cable'
cable.yml
development:
adapter: redis
url: redis://localhost:6379
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379
config/environments.rb
config.action_cable.disable_request_forgery_protection = true
Front:
ChatCtrl.js
app.controller('ChatCtrl', ['$scope', 'ActionCableChannel',
function($scope, ActionCableChannel) {
$scope.inputText;
$scope.chatData = [];
var consumer = new ActionCableChannel("ChatChannel");
var callback = function(message) {
$scope.chatData.push(message);
};
consumer.subscribe(callback).then(function() {
$scope.sendToMyChannel = function(message) {
consumer.send(message, 'speak');
};
$scope.$on("$destroy", function() {
consumer.unsubscribe().then(function() {
$scope.sendToMyChannel = undefined;
});
});
});
}
]);
// Action Cable Configuration
app.run(function (ActionCableConfig) {
ActionCableConfig.wsUri = 'localhost:4000';
});
Error Message in Console:

Try
ActionCableConfig.wsUri = 'ws://localhost:4000';

Related

Unable to detect the client activation status in server side if disconnected due to poor network - Action cable

We facing issues with actioncable. The issue is , Client is getting disconnected due to internet issue but that status not communicated to the server(disconnect event not calling)
The below method is failing due to inactive client,
ActionCable.server.broadcast "project_123_channel", {
"request" => "show_title"
}
Ruby version:2.5.3p105
Rails version:5.2.3
actin cable version: 5.2.3
disconnect callback:
App.live_data = App.cable.subscriptions.create({ channel: "ProjectDataChannel", project_id: project_id }, {
collection: function() {
},
connected: function() {
console.log("Live data connected");
},
disconnected: function() {
console.log("disconnected");
return this.perform('unfollow');
}
But this disconnect callback not working.
Is there any way to detect the client active status from server side? or there any callback to track the disconnect status if the client is disconnect due to internet kind of things
This is a known issue.
Here is a blog post from Vlad Dyachenko that describes the problem and a solution...
https://wowinter13.medium.com/actioncable-handling-client-connection-errors-on-server-side-93ea74178d03
This is the code solution from the post...
module ApplicationCable
class Channel < ActionCable::Channel::Base
after_subscribe :connection_monitor
CONNECTION_TIMEOUT = 10.seconds
CONNECTION_PING_INTERVAL = 5.seconds
periodically every: CONNECTION_PING_INTERVAL do
#driver&.ping
if Time.now - #_last_request_at > #_timeout
connection.disconnect
end
end
def connection_monitor
#_last_request_at ||= Time.now
#_timeout = CONNECTION_TIMEOUT
#driver = connection.instance_variable_get('#websocket').possible?&.instance_variable_get('#driver')
#driver.on(:pong) { #_last_request_at = Time.now }
end
end
end
It's worked fine as suggestion given by #dbugger

How to add ActionCable to a ruby on rails engine?

I am working in a project which has 3 separate parts - a ruby on rails engine, a ruby on rails application, and a frontend react application. I need to introduce websockets / ActionCable in the engine, and am not having any luck getting ActionCable running.
I have the following relevant files in the engine:
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end
app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
app/channels/fromserver_channel.rb
class FromServerChannel < ApplicationCable::Channel
def subscribed
stream_from "fromClient"
end
def receive(data)
puts '----------'
puts data
ActionCable.server.broadcast "fromServer", data
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
config/environments/development.rb
require 'action_cable/engine'
Rails.application.configure do
config.action_cable.disable_request_forgery_protection = true
#specify the socket URI for ActionCable
config.action_cable.url = "/cable"
config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
end
config/cable.rb
development:
adapter: async
test:
adapter: async
production:
adapter: postgresql
config/routes.rb
require 'action_cable/engine'
RoundaboutEngine::Engine.routes.draw do
mount ActionCable.server => '/cable'
# mount RoundaboutEngine::Engine => "/cable"
scope controller: 'round_about' do
get 'organizer' => :show
end
end
Rails.application.routes.send(:draw) do
mount RoundaboutEngine::Engine => '/' if RoundaboutEngine.draw_routes_in_host_app
end
The front end looks like this:
import React, { useState } from 'react';
import channel from './cable';
const RoundAboutView = () => {
const [displayText, setDisplayText] = useState('');
const sendUpdate = (arg) => {
channel.sending(arg);
};
return (
...
Text:
{` ${displayText}`}
...
);
};
export default RoundAboutView;
import ActionCable from 'actioncable';
// const cable = ActionCable.createConsumer('wss://localhost:9292/cable');
const cable = ActionCable.createConsumer(`ws://${window.location.host}/cable`);
const channel = cable.subscriptions.create(
{ channel: 'FromServerChannel' },
{
connected() { },
disconnected() {},
received: (data) => {
console.log(data);
},
sending(data) {
this.perform('fromClient', { data });
},
},
);
export default channel;
The problem I am having is that Chrome says the websocket can't connect (WebSocket connection to 'ws://localhost:9292/cable' failed). In the terminal, when in routes.rb I mount /cable with ActionCable.server, I see the below:
"exception":{
"type":"NoMethodError",
"message":"undefined method `info' for nil:NilClass",
"stack_trace":"NoMethodError(undefined method `info' for nil:NilClass)\n actioncable (5.2.6) lib/action_cable/connection/tagged_logger_proxy.rb:38:in `block in log'\n actioncable (5.2.6)
When I don't do that and instead mount /cable with the engine, I get an error that the route GET /cable doesn't exist.
I'm not able to move past this point and I'm not sure what I'm doing wrong or missing in the code. I've seen the example at https://github.com/palkan/engine-cable-app and wasn't able to take that example and make it work in my code. Can anyone help me?

Why doesn't Redis work with my Rails application?

I'm currently using Rails 6 and Linux mint Cinnamon 20.2, and I have problems with Redis configuration / setupping in my Rails application.
My problem is that my ActionCable doesn't work well.
I would like to build a real time chat app, I created a channel, called "Room".
Here how my room_channel.js file looks like:
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "RoomChannel", room_id: 3 }, {
connected() {
console.log('Connected successfully!')
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
console.log(data)
}
});
Here's my room_channel.rb:
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params[:room_id]}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Here's the important segment of my messages_controller.rb file (The ActionCable command is the important):
def create
#message = Message.new(message_params)
#message.user = User.generate
#message.save
ActionCable.server.broadcast("room_channel_#{#message.room_id}",{message:"hello"})
end
When I join to Room 3 (3 is the ID of that room), I get my "Connected successfully" message, but when I send a message, it doesn't output "hello" in the console. It means it doesn't receive successfully datas, right?
What can i do with Redis to be able to receive data?
Finally, I changed the development section of my config/cable.yml file to, but it doesn't solved anything :(.
development:
adapter: redis
url: redis://localhost:6379/1
Anybody can help me out with this?
Thanks for your help!

How do I use ActionCable client in react static page for the Rails5(API mode) app?

I am developing now Streaming API with Rails5 api mode, for React website.
That use it in notification functions.
I'm in below's env:
<server side>
rails: v5.1.5
ruby: 2.4.2
<client side>
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-router": "^2.7.0",
"actioncable": "^5.1.5",
Server side code:
app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
development.rb
config.action_cable.url = "ws://localhost:3000/cable"
application.rb
# websocket
config.action_cable.allowed_request_origins = [
# Local address of our RoR server
'http://localhost:3000',
# Local address we use for our React standalone client
'http://localhost:8000',
'chrome-extension://pfdhoblngboilpfeibdedpjgfnlcodoo'
]
ActionCable.server.config.disable_request_forgery_protection = true
routes.rb
mount ActionCable.server => '/cable'
xxx.rb
ActionCable.server.broadcast('notifications', notification: #notification)
Client side:
componentDidMount () {
this.createSocket()
}
createSocket = () => {
let App = {};
App.cable = ActionCable.createConsumer('ws://localhost:3000/cable');
let subscription = App.cable.subscriptions.create({channel:'NotificationsChannel'}, {
connected : () => {},
disconnected : () => {},
received : (data) => {
console.log(data)
}
});
console.log(subscription)
};
And so, this is server side console
Started GET "/cable" for 127.0.0.1 at 2018-03-02 13:37:30 +0800
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2018-03-02 13:37:30 +0800
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION:Upgrade, HTTP_UPGRADE: websocket)
NotificationsChannel is transmitting the subscription confirmation
NotificationsChannel is streaming from notifications
I am referring this website https://www.npmjs.com/package/actioncable and https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable this one.
BUT the data is not displayed in client console.
How do I change code?
Thanks to #GorillaApe
I solved this problem.
It is by setting about cable.yml .
aysnc doesn't work in this case. redis is better as adapter.
this is my cable.yml now:
development:
adapter: redis
url: redis://localhost:6379
test:
adapter: redis
url: redis://localhost:6379
production:
adapter: redis
url: redis://localhost:6379
channel_prefix: api_notification_production

em-websocket send() send from one client to another through 2 servers

I have two websocket clients, and I want to exchange information between them.
Let's say I have two instances of socket servers, and 1st is retrieve private information, filter it and send to the second one.
require 'em-websocket'
EM.run do
EM::WebSocket.run(host: '0.0.0.0', port: 19108) do |manager_emulator|
# retrieve information. After that I need to send it to another port (9108)
end
EM::WebSocket.run(host: '0.0.0.0', port: 9108) do |fake_manager|
# I need to send filtered information here
end
end
I've tried to do something, but I got usual dark code and I don't know how to implement this functionality.
I'm not sure how you would do that using EM.
I'm assuming you will need to have the fake_manager listen to an event triggered by the manager_emulator.
It would be quite easy if you'd be using a websocket web-app framework. For instance, on the Plezi web-app framework you could write something like this:
# try the example from your terminal.
# use http://www.websocket.org/echo.html in two different browsers to observe:
#
# Window 1: http://localhost:3000/manager
# Window 2: http://localhost:3000/fake
require 'plezi'
class Manager_Controller
def on_message data
FakeManager_Controller.broadcast :_send, "Hi, fake! Please do something with: #{data}\r\n- from Manager."
true
end
def _send message
response << message
end
end
class FakeManager_Controller
def on_message data
Manager_Controller.broadcast :_send, "Hi, manager! This is yours: #{data}\r\n- from Fake."
true
end
def _send message
response << message
end
end
class HomeController
def index
"use http://www.websocket.org/echo.html in two different browsers to observe this demo in action:\r\n" +
"Window 1: http://localhost:3000/manager\r\nWindow 2: http://localhost:3000/fake\r\n"
end
end
# # optional Redis URL: automatic broadcasting across processes or machines:
# ENV['PL_REDIS_URL'] = "redis://username:password#my.host:6379"
# starts listening with default settings, on port 3000
listen
# Setup routes:
# They are automatically converted to the RESTful route: '/path/(:id)'
route '/manager', Manager_Controller
route '/fake', FakeManager_Controller
route '/', HomeController
# exit terminal to start server
exit
Good Luck!
P.S.
If you're going to keep to EM, you might consider using Redis to push and subscribe to events between the two ports.
I've found a way how to do it through em-websocket gem! You need just define variables outside of eventmachine block. Something like that
require 'em-websocket'
message_sender = nil
EM.run do
# message sender
EM::WebSocket.run(host: '0.0.0.0', port: 19108) do |ws|
ws.onopen { message_sender = ws }
ws.onclose { message_sender = nil }
end
# message receiver
EM::WebSocket.run(host: '0.0.0.0', port: 9108) do |ws|
ws.onmessage { |msg| message_sender.send(msg) if message_sender }
end
end

Resources