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/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 })