---
title: Talking to ActionCable without Rails
teaser: How to talk to ActionCable without using Rails.
tags: web,javascript,actioncable,websockets,redux
author: Bruno Antunes
published_on: 2017-11-30
---

[ActionCable] was introduced in Rails for its 5.0 release as a way to
seamlessly integrate [WebSockets] in Rails applications. I was curious to know
more about it, and the opportunity presented itself while doing a [React]
frontend to a Rails API. How could we interact with it with plain Javascript?

## ActionCable basics

By allowing easy use of WebSockets on any Rails codebase, ActionCable adds
real-time capabilities to Rails' already impressive list of features. It will
also be somewhat familiar to work with it if you've had any exposure to
Phoenix's [channels].

There are two main concepts to grasp: **Connections** and **Channels**.

**Connections** represent the WebSocket link between the server and the client.
Every WebSocket connection has a corresponding Connection object, which serves
as parent to any Channel instance created off the connection. Connection objects
are mostly concerned with authorization and identification.

Here's an example Connection base class:

```ruby
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :uuid

    def connect
      self.uuid = SecureRandom.urlsafe_base64
    end
  end
end
```

The only method you need to implement is `#connect`, which runs when every
connection is made. Here you'll normally assign a unique identifier to the
connection. These are the steps to make this happen:

-   We use `#identified_by` to tell what attribute will be the unique
    identifier. In our example, we have decided to call this `:uuid`, but it
    could be anything.
-   This will create a class attribute `@uuid`, with a getter and setter that
    you can access in your code.
-   During connect, assign a unique identifier to this `@uuid`. You can just
    generate a random string like in the example.

Apart from that, it is possible to reject a connection during connect by calling
`#reject_unauthorized_connection`. Finally, you can implement `#disconnect`,
which will give you a chance to run cleanup code after the client goes away.

**Channels** build upon connections, and serve as repositories for code that
handles messages sent by consumers, or that you want to broadcast to particular
streams. For every message "action" that is sent by the consumer, you must have
a method with a matching name in the channel class to handle that message.

Once you have made the connection, the _consumer_ (client) will want to
subscribe to specific _channels_ and issue commands, or be notified of
server-side messages to the channels it subscribes to.

If we were to use the JS library bundled with ActionCable, we would initialize
the consumer with this code:

```javascript
// app/assets/javascripts/cable.js
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer(`ws://some.host:28080`);
}).call(this);
```

Once initialized, the consumer can subscribe to channels with:

```js
App.cable.subscriptions.create { channel: "SomeChannel" }
```

This creates a subscription. On Rails' side we'll have a channel class named
`SomeChannel` to handle this:

```ruby
class SomeChannel < ApplicationCable::Channel

  def subscribed
    stream_from "player_#{uuid}"
  end

end
```

Several things to note in the example above:

* The method `subscribed` is called when the subscription to the channel is
  created. We'll have to implement it.
* We use `stream_from` to add the consumer to a stream.
* In the example, we choose a name that includes the unique id
  (`"player_#{uuid}"`). This suggests that it will be a stream exclusive to that
  consumer.
* By the way, `uuid` is the same one we defined on the connection. It's
  delegated to channels and accessible from them.
* If instead we chose a more generic name (eg: "players"), then this would be
  common to all subscribers, and they would receive the same messages.

Going with the example above, which uses individual streams for each consumer,
we would use something like the following to send a message to a single,
specific consumer:

```ruby
class SomeChannel < ApplicationCable::Channel

  def something_something(player)
    ActionCable.server.broadcast "player_#{player.uuid}", {
      message: "message body",
    }
  end

end
```

Finally, from the consumer's side, we can send messages through the channel,
once subscribed:

```js
var App = {};

App.cable = ActionCable.createConsumer(`ws://${window.location.hostname}:28080`);

App.messaging = App.cable.subscriptions.create('SomeChannel', {
  received: function(data) {
    $(this).trigger('received', data);
  },
  sendMessage: function(messageBody) {
    this.perform('foobar', { body: messageBody, to: otherPlayerUuid });
  }
});
```

In this case, whenever the consumer uses `sendMessage` to send a message, the
`foobar` method in the channel will be invoked:

```ruby
class SomeChannel < ApplicationCable::Channel

  def foobar(data)
    message = data['body']
    other_player_uuid = data['to']

    ActionCable.server.broadcast "player_#{other_player_uuid}", {
      message: message
    }
  end
end
```

For every message type there should be a corresponding method in the channel
class to handle it. Now, this is all good with Rails' default ActionCable code,
but what if we want to handle the connections, subscriptions and messages
with our own code?

## Behind the scenes

When interfacing with ActionCable, the first thing you need to do is to open a
WebSocket connection to the server.

```js
socket = new WebSocket(url);
socket.onmessage = onMessage(socket);
socket.onclose = onClose(socket);
socket.onopen = onOpen(socket);
```

When you run this code and establish a connection, ActionCable will send the
following payload, verbatim:

```js
{
  "type": "welcome"
}
```

If you inspect the WebSocket message, its `data` property will be set to
`"{\"type\":\"welcome\"}"`. This is because ActionCable assumes all payloads are
JSON encoded strings - you must take care to always "stringify" the data being
sent.

This shows we're in business! After this message, you'll want to
subscribe to a channel:

```js
const msg = {
  command: 'subscribe',
  identifier: JSON.stringify({
    channel: 'SomeChannel',
  }),
};
socket.send(JSON.stringify(msg));
```

If the message succeeds, ActionCable will reply with the following payload
(after decoding the JSON):

```js
{
  "identifier": {
    "channel": "SomeChannel"
  },
  "type": "confirm_subscription"
}
```

You are now subscribed to this channel. ActionCable will start sending heartbeat
"ping" messages once every three seconds. Here's how the payload for one of
these pings looks like:

```js
{
  "type": "ping",
  "message": 1510931567 // Current time in UNIX epoch format
}
```

Until now, the message type has been one of `welcome`, `subscribe`,
`confirm_subscription` and `ping`. But most of the time, consumer-side messages
will use the `message` type. Here's an example:

```js
const msg = {
  command: 'message',
  identifier: JSON.stringify({
    channel: 'SomeChannel',
  }),
  data: JSON.stringify({
    action: 'join',
    code: 'NCC1701D',
  }),
};
socket.send(JSON.stringify(msg));
```

Again, pay close attention to how the message needs to be sent as a string.
In this example, ActionCable will try to invoke the `join` method on the channel
class and pass it the contents of the `data` object:

```ruby
class SomeChannel < ApplicationCable::Channel

  def join(data)
    game = Game.find_by!(code: data["code"])
    player = Player.find_by!(token: uuid)
    game.add_player(player)

    broadcast_current_state(game)
  end
end
```

As we've seen before, the `uuid` from the connection is accessible in the
channel.

And that's all it takes to talk to ActionCable. Apart from the payload encoding
methodology, it's pretty straightforward. Be aware that the connection can be
severed, and so special care must be taken handling this event on your socket's
`onClose` handler.

Here's an example of a [Redux] middleware handling WebSocket communication. It
intercepts any action type starting with `WS_`. To connect, dispatch a
`WS_CONNECT` type action, and to subscribe a `WS_SUBSCRIBE` one. In your app's
state tree, you should have a `connection` object with a `state` key in it. This
state will transition from `CLOSED` to `OPENING` to `SUBSCRIBING` to finally
`READY`. The middleware will also dispatch any message received from the Rails
server into your Redux app, which is an interesting integration pattern between
your Rails backend and Redux frontend. Gist [here]

```js
// src/actions/connection.js
import {
  CONNECTION_OPENING,
  CONNECTION_SUBSCRIBING,
  CONNECTION_READY,
} from './constants';

export function opening() {
  return {
    type: CONNECTION_OPENING,
  }
}

export function subscribing() {
  return {
    type: CONNECTION_SUBSCRIBING,
  };
}

export function ready() {
  return {
    type: CONNECTION_READY,
  };
}
```

```js
// src/middlewares/websocket.js
import * as connectionActions from '../actions/connection';

let socket;

const onOpen = (ws, store, code) => evt => {
  console.log("WS OPEN");
}

const onClose = (ws, store) => evt => {
  console.log("WS CLOSE");
}

const onMessage = (ws, store) => evt => {
  let msg = JSON.parse(evt.data);

  if (msg.type === "ping") {
    return;
  }

  console.log("FROM RAILS: ", msg);

  const connectionState = store.getState().connection.state;
  const gameCode = store.getState().game.code;

  if (connectionState === 'OPENING') {
    if (msg.type === 'welcome') {
      store.dispatch(connectionActions.subscribing());
      const msg = {
        command: 'subscribe',
        identifier: JSON.stringify({
          channel: 'GameChannel',
        }),
      };
      socket.send(JSON.stringify(msg));
    } else {
      console.error('WS ERRORED!');
    }

  } else if (connectionState === 'SUBSCRIBING') {
    if (msg.type === 'confirm_subscription') {
      store.dispatch(connectionActions.ready());
      const msg = {
        command: 'message',
        identifier: JSON.stringify({
          channel: 'GameChannel',
        }),
        data: JSON.stringify({
          action: 'join',
          code: gameCode,
        }),
      };
      socket.send(JSON.stringify(msg));
    } else {
      console.error('WS ERRORED!');
    }

  } else {
    store.dispatch(msg.message);
  }
}

export default store => next => action => {
  const match = /^WS_(.+)$/.exec(action.type);
  if (!match) {
    return next(action);
  }

  const wsAction = { ...action, type: match[1] };
  if (wsAction.type === 'CONNECT') {
    if (socket) {
      socket.close();
    }

    const { code } = wsAction.payload;

    socket = new WebSocket(process.env.REACT_APP_WS_URL);
    socket.onmessage = onMessage(socket, store);
    socket.onclose = onClose(socket, store);
    socket.onopen = onOpen(socket, store, code);

    store.dispatch(connectionActions.opening());

  } else if (wsAction.type === 'SUBSCRIBE') {
    const msg = {
      command: 'subscribe',
      identifier: JSON.stringify({
        channel: 'GameChannel',
      }),
    };

    socket.send(JSON.stringify(msg));
    store.dispatch(connectionActions.subscribing())

  } else {
    const msg = {
      command: 'message',
      identifier: JSON.stringify({
        channel: 'GameChannel',
      }),
      data: JSON.stringify({
        ...action,
        action: wsAction.type.toLowerCase(),
      }),
    };

    socket.send(JSON.stringify(msg));
    next(action);
  }
};
```

## Thanks

Thanks go to Pablo for the original idea for the React app, and to him and
Murtaza for providing feedback.

[ActionCable]: http://guides.rubyonrails.org/action_cable_overview.html
[channels]: https://hexdocs.pm/phoenix/channels.html
[WebSockets]: https://en.wikipedia.org/wiki/WebSocket
[React]: https://reactjs.org/
[Redux]: https://redux.js.org/
[here]: https://gist.github.com/sardaukar/5ab5b4e8aa32202bd32e495b92da6adb
