ActionCable is an awesome feature of Rails that allows for sending real-time messages. If your frontend is part of your Rails app, much of the plumbing is handled for you. However, sometimes you need to use ActionCable, outside of Rails. In this blog post I’ll show you how to subscribe to a channel, print out messages and publish messages, all with vanilla JavaScript. Although I’m using JS, the principles apply to any language that supports websockets.
Establishing a connection and subscribing to a channel
1
2
3
4
5
6
7
8
socket = new WebSocket('ws://localhost:3000/cable');
socket.onopen = function(event) {
const subscribe_msg = {
command: 'subscribe',
identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
};
socket.send(JSON.stringify(subscribe_msg));
};
First, you need to establish a websocket connection. JavaScript has a build in API for that. The WebSocket
constructor takes a websocket url. You can’t use http, it must be a ws or wss protocol url. In this example I’m using localhost but you can replace it with any websockets url.
Once you’ve used the constructor to return a WebSocket
object you have access to onopen
, which allows you to run a callback function once the websocket connection has been established. Inside that function you can write all the logic for subscribing to channels and receiving or publishing messages. In this case, I’m going to start off by subscribing to GeneralChatChannel
. To do that, I’ll call the send
method on my WebSocket
object. I’ll pass send
a JSON object with two fields:
command
tells ActionCable what action to perform, eg:subscribe
ormessage
identifier
tells ActionCable which channel to perform the action in
Logging out incoming messages
1
2
3
4
5
6
7
socket.onmessage = function(event) {
const incoming_msg = JSON.parse(event.data);
if (incoming_msg.type === "ping") { return; } // Ignores webhook pings.
console.log("FROM RAILS: ", incoming_msg);
}
Once subscribed to a channel, you can use another WebSocket
method to log out incoming messages. onmessage
allows you to define a callback function, which gets executed whenever a message is received (on the channel you’ve subscribed to). The callback function receives an event
that has a data
property containing the message data. You can simply parse the JSON and log it out.
One thing to note here is the middle line of the function body. onmessage
will receive regular pings, the purpose of which is to keep the websocket connection alive. These ping messages don’t contain message data, they’re low level events that can be ignored.
Sending a message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (msg.type === "confirm_subscription") {
const msg = {
command: 'message',
identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
data: JSON.stringify(
{
user_id: 1,
message: 'Hello world!'
}
)
}
socket.send(JSON.stringify(msg2));
}
You can also use the WebSocket
object to send messages. Sending a message is simple, just craft some JSON and use the send
function to publish it to an ActionCable channel. However, before sending a message you need to make sure the channel subscription has been fully established, otherwise, messages may get dropped. You can do that by waiting for an incoming message of type confirm_subscription
. Once that’s received, you can go ahead and start sending messages.
send
expects a JSON object with three fields:
command
tells ActionCable what action to perform, eg:subscribe
ormessage
identifier
tells ActionCable which channel to perform the action indata
contains the message body
Putting it all together
Feel free to use the subscribe, logging or publishing snippets on their own. Alternatively, you can combine all three operations into a single function. I’ve found this very useful for testing ActionCable from outside Rails:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function() {
socket = new WebSocket('ws://localhost:3000/cable');
socket.onopen = function(event) {
const subscribe_msg = {
command: 'subscribe',
identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
};
socket.send(JSON.stringify(subscribe_msg));
};
socket.onmessage = function(event) {
const incoming_msg = JSON.parse(event.data);
if (incoming_msg.type === "ping") { return; } // Ignores pings.
console.log("FROM RAILS: ", incoming_msg);
if (msg.type === "confirm_subscription") {
const msg = {
command: 'message',
identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
data: JSON.stringify(
{
user_id: 1,
message: 'Hello world!'
}
)
}
socket.send(JSON.stringify(msg));
}
};
})()