Using AnyCable JS SDK
AnyCable speaks the Action Cable protocol, so existing clients (such as @rails/actioncable) work as-is. The AnyCable JS SDK is the recommended client, because it supports AnyCable features that generic clients do not:
- Reliable streams: session recovery and missed-message catch-up via the extended protocol.
- Presence and whispering.
- Token refresh and auth strategies.
- Multi-platform: web, plus Node.js, React Native, and fallbacks; Pro adds binary formats.
- TypeScript support and test helpers.
- Better Turbo Streams support.
This page covers the core API: creating a client, subscribing to streams, and channels. It applies to @anycable/web v1.1+ and @anycable/core v1.1+.
Install and create a client
npm install @anycable/web
# or: yarn add @anycable/web / pnpm add @anycable/web@anycable/web is for browsers. In any other environment (Node.js, React Native, workers), use @anycable/core (see platforms).
Create a cable (or consumer, as Action Cable calls it) once, as a singleton for the lifetime of your application:
// cable.js
import { createCable } from '@anycable/web'
export default createCable({
// Use the extended Action Cable protocol for delivery guarantees
protocol: 'actioncable-v1-ext-json',
// Enable verbose logging while integrating
logLevel: 'debug'
})In the browser, the connection URL is looked up in the action-cable-url or cable-url meta tags, falling back to /cable. You can pass it explicitly:
createCable('ws://cable.example.com/cable')Subscribe to streams (pub/sub)
Subscribe directly to data streams, with no channel classes involved. This is the primary mode when AnyCable runs standalone and your backend only broadcasts events:
import cable from 'cable'
const chatChannel = cable.streamFrom('room/42')
chatChannel.on('message', (msg) => {
// ...
})
// Publish transient client-to-client events (see the whispering docs)
chatChannel.whisper({ event: 'typing', user: user.name })In production, prefer signed stream names generated by your backend, so clients can only subscribe to streams you allow (see signed streams):
const signedName = await obtainSignedStreamNameFromWhenever()
const chatChannel = cable.streamFromSigned(signedName)Use channels
When your backend defines channels (for example, Rails Action Cable channels or a serverless backend), subscribe with channels: class-based or headless.
Class-based subscriptions
A channel class wraps a subscription in an application-specific API. You can add methods, dispatch custom events, and intercept messages:
import { Channel } from '@anycable/web'
// channels/chat.js
export default class ChatChannel extends Channel {
// Unique channel identifier (the channel class name for Action Cable)
static identifier = 'ChatChannel'
async speak(message) {
return this.perform('speak', { message })
}
receive(message) {
if (message.type === 'typing') {
// Emit a custom event for typing messages
return this.emit('typing', message)
}
super.receive(message)
}
}import cable from 'cable'
import { ChatChannel } from 'channels/chat'
const channel = new ChatChannel({ roomId: '42' })
cable.subscribe(channel)
// Optionally wait for the confirmation; you can also perform actions right
// away, and the channel will wait for the connection automatically
await channel.ensureSubscribed()
await channel.speak('Hello')
channel.on('message', (msg) => console.log(`${msg.name}: ${msg.text}`))
channel.on('typing', (msg) => console.log(`User ${msg.name} is typing`))
// Lifecycle: temporary disconnect vs. final close
channel.on('disconnect', () => console.log('No chat connection'))
channel.on('close', () => console.log('Disconnected from chat'))
// Unsubscribe (results in a 'close' event)
channel.disconnect()Subscribing is optimistic: cable.subscribe(channel) works whether or not the cable is connected, and retries automatically until the subscription is confirmed or rejected. Calling channel.disconnect() removes the subscription immediately and sends the unsubscribe command asynchronously.
Multiple instances with the same identifier are safe: the SDK creates a single real subscription and delivers messages to every instance. Two components can independently new NotificationsChannel() + cable.subscribe(...) and both receive updates; the server-side unsubscribe happens only when the last one disconnects.
Headless subscriptions
Headless subscriptions skip the class, similar to Action Cable's subscriptions.create:
import cable from 'cable'
const subscription = cable.subscribeTo('ChatChannel', { roomId: '42' })
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}`)
}
})Migrate from @rails/actioncable
The SDK ships an Action Cable compatible API, so the migration is an import change:
- import { createConsumer } from "@rails/actioncable";
+ import { createConsumer } from "@anycable/web";
// createConsumer accepts all the options available to createCable
export default createConsumer();consumer.subscriptions.create(...) keeps working; under the hood, a headless channel is created.
Configuration options
The most useful createCable options (defaults shown):
const cable = createCable({
logLevel: 'warn', // use 'debug' for troubleshooting
performFailures: 'throw', // how to treat channel.perform() failures ('warn', 'ignore')
lazy: true // connect only when the first subscription is made
})With @anycable/web, logs go to the browser console. With @anycable/core, the default logger is silent, so logLevel alone has no effect; pass a logger to see logs (see Node.js).
See the TS definitions for the full list.
Hotwire integration
For Turbo Streams, install the @anycable/turbo-stream package and activate stream source elements with your cable instance:
// IMPORTANT: import turbo, not turbo-rails
import '@hotwired/turbo'
import { start } from '@anycable/turbo-stream'
import cable from 'cable'
start(cable, { delayedUnsubscribe: true })See Using AnyCable with Hotwire for the full setup, including delayed unsubscribe and broadcasts to others.