Using AnyCable JS SDK

See the full documentation at anycable/anycable-client.

Even though AnyCable server utilizes Action Cable protocol and, thus, can be used with the existing Action Cable client libraries (such as @rails/actioncable), we recommend using AnyCable JS SDK for the following reasons:

Quick start

You can install AnyCable JS SDK via npm/yard/pnpm:

npm install @anycable/web
yarn add @anycable/web
pnpm install @anycable/web

The @anycable/web package is assumed to be used in the browser environment. If you want to use AnyCable client in a non-web environment (e.g., Node.js), you should use @anycable/core package.

Then you can use it in your application.

First, you need to create a cable (or consumer as it's called in Action Cable):

// cable.js
import { createCable } from '@anycable/web'

export default createCable({
  // There are various options available. For example:
  // - Enable verbose logging
  logLevel: 'debug',
  // - Use the extended Action Cable protocol
  protocol: 'actioncable-v1-ext-json',
})

Typically, the cable is a singleton in your application. You create it once for the whole lifetime of your application.

Pub/Sub

You can subscribe to data streams as follows:

import cable from 'cable';

const chatChannel = cable.streamFrom('room/42');

chatChannel.on('message', (msg) => {
  // ...
});

In most cases, however, you'd prefer to use secured (signed) stream names generated by your backend (see signed streams):

const cable = createCable();
const signedName = await obtainSignedStreamNameFromWhenever();
const chatChannel = cable.streamFromSigned(signedName);
// ...

Channels

AnyCable client provides multiple ways to subscribe to channels: class-based subscriptions and headless subscriptions.

[!TIP] Read more about the concept of channels and how AnyCable uses it here.

Class-based subscriptions

Class-based APIs allows provides an abstraction layer to hide implementation details of subscriptions. You can add additional API methods, dispatch custom events, etc.

Let's consider an example:

import { Channel } from '@anycable/web'

// channels/chat.js
export default class ChatChannel extends Channel {
  // Unique channel identifier (channel class for Action Cable)
  static identifier = 'ChatChannel'

  async speak(message) {
    return this.perform('speak', { message })
  }

  receive(message) {
    if (message.type === 'typing') {
      // Emit custom event when message type is 'typing'
      return this.emit('typing', message)
    }

    // Fallback to the default behaviour
    super.receive(message)
  }
}

Then, you can you this class to create a channel instance and subscribe to it:

import cable from 'cable'
import { ChatChannel } from 'channels/chat'

// Build an instance of a ChatChannel class.
const channel = new ChatChannel({ roomId: '42' })

// Subscribe to the server channel via the client.
cable.subscribe(channel) // return channel itself for chaining

// Wait for subscription confirmation or rejection
// NOTE: it's not necessary to do that, you can perform actions right away,
// the channel would wait for connection automatically
await channel.ensureSubscribed()

// Perform an action
let _ = await channel.speak('Hello')

// Handle incoming messages
channel.on('message', msg => console.log(`${msg.name}: ${msg.text}`))

// Handle custom typing messages
channel.on('typing', msg => console.log(`User ${msg.name} is typing`))

// Or subscription close events
channel.on('close', () => console.log('Disconnected from chat'))

// Or temporary disconnect
channel.on('disconnect', () => console.log('No chat connection'))

// Unsubscribe from the channel (results in a 'close' event)
channel.disconnect()

Headless subscriptions

Headless subscriptions are very similar to Action Cable client-side subscriptions except from the fact that no mixins are allowed (you classes in case you need them).

Let's rewrite the same example using headless subscriptions:

import cable from 'cable'

const subscription = cable.subscribeTo('ChatChannel', { roomId: '42' })

const _ = await subscription.perform('speak', { msg: 'Hello' })

subscription.on('message', msg => {
  if (msg.type === 'typing') {
    console.log(`User ${msg.name} is typing`)
  } else {
    console.log(`${msg.name}: ${msg.text}`)
  }
})

Migrating from @rails/actioncable

AnyCable JS SDK comes with a compatibility layer that allows you to use it as a drop-in replacement for @rails/actioncable. All you need is to change the imports:

- import { createConsumer } from "@rails/actioncable";
+ import { createConsumer } from "@anycable/web";

 // createConsumer accepts all the options available to createCable
 export default createConsumer();

Then you can use consumer.subscriptions.create as before (under the hood a headless channel would be create).

Hotwire integration

You can also use AnyCable JS SDK with Hotwire (Turbo Streams) to provide better real-time experience and benefit from AnyCable features. For that, you must install the @anycable/turbo-stream package.

Here is how to switch @hotwired/turbo to use AnyCable client:

// IMPORTANT: Do not import turbo-rails, just turbo
// import "@hotwired/turbo-rails";
import "@hotwired/turbo";
import { start } from "@anycable/turbo-stream";
import cable from "cable"

start(cable, { delayedUnsubscribe: true })