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:
- Multi-platform out-of-the-box (web, workers, React Native, Node.js).
- TypeScript support.
- Extended protocol support (e.g., binary formats).
- AnyCable-specific features support (e.g., reliable streams and signed streams).
- Better Turbo Streams support
- ... and more.
Quick start
You can install AnyCable JS SDK via npm/yard/pnpm:
npm install @anycable/web
yarn add @anycable/web
pnpm install @anycable/webThe @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 })