Using AnyCable Ruby without Rails

AnyCable Ruby can be used without Rails, thus, allowing you to bring real-time and Action Cable-like functionality into your Ruby application.

Installation

Add anycable gem to your Gemfile:

gem "anycable", "~> 1.1"

# when using Redis-backed broadcast adapter
gem "redis", ">= 4.0"

# when using NATS-backed broadcast adapter
gem "nats-pure", "~> 2"

If you don't plan to use channels (RPC), you can go with the anycable-core gem instead of anycable.

Pub/Sub only mode

To use AnyCable in a standalone (pub/sub only) mode (see docs), you need to use the following APIs:

  • Broadcasting.

    See the corresponding documentation.

  • JWT authentication.

    You can generate AnyCable JWT tokens as follows:

    # Client entitity identifiers (usually, user ID or similar)
    identifiers = {user_id: User.first.id}
    
    token = AnyCable::JWT.encode(identifiers)
  • Signed streams.

    You the following API to sign streams:

    signed_name = AnyCable::Streams.signed("chat/42")

NOTE: For JWT authentication and signed streams, the application secret or dedicated secrets MUST be provided. The values MUST match the ones configured at the AnyCable server side.

Using channels via Lite Cable

There is a ready-to-go framework – Lite Cable – which can be used for application logic. It also supports AnyCable out-of-the-box.

Learn how to use AnyCable with Hanami in the "AnyCable off Rails: connecting Twilio streams with Hanami" blog post.

Custom channels implementation

You can build your own framework to use as logic-handler for AnyCable.

AnyCable initiates a connection object for every request using user-provided factory:

# Specify factory
AnyCable.connection_factory = MyConnectionFactory

# And then AnyCable calls .call method on your factory
connection = factory.call(socket, **options)

Where:

  • socket – is an object, representing client's socket (say, socket stub) (see socket.rb)
  • options may contain:
    • identifiers: a JSON string returned by connection.identifiers_json on connection (see below)
    • subscriptions: a list of channels identifiers for the connection.

Connection interface:

class Connection
  # Called on connection
  def handle_open
  end

  # Called on disconnection
  def handle_close
  end

  # Called on incoming message.
  # Client send a JSON-encoded message of the form { "identifier": ..., "command": ..., "data" ... }.
  # - identifier – channel identifier (e.g. `{"channel":"chat","id":1}`)
  # - command – e.g. "subscribe", "unsubscribe", "message"
  # - any additional data
  def handle_channel_command(identifier, command, data)
    # ...
  end

  # Returns any string which can be used later in .create function to initiate connection.
  def identifiers_json
  end
end

Connection#handle_channel_command should return truthy value on success (i.e., when a subscription is confirmed, or action is called).

NOTE: connection instance is initiated on every request, so it should be stateless (except identifiers_json).

To send a message to a client, you should call socket#transmit. For manipulating with streams use socket#subscribe, socket#unsubscribe and socket#unsubscribe_from_all.

To persist client states between RPC calls you can use socket#cstate (connection state) and socket#istate (per-channel state), which are key-value stores (keys and values must both be strings).

See test factory for example.