MCPcopy Index your code
hub / github.com/anycable/anycable-client

github.com/anycable/anycable-client @v1.1.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.1.0 ↗ · + Follow
628 symbols 1,351 edges 96 files 12 documented · 2%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

npm version Test

AnyCable JavaScript Client

[AnyCable][anycable] brings performance and scalability to real-time applications built with Ruby and Rails. It uses [Action Cable protocol][protocol] and its extensions for client-server communication.

This repository contains JavaScript packages to build AnyCable clients.

Motivation

Multiple reasons that forced us to implement an alternative client library for Action Cable / AnyCable:

📖 Read also the introductory post.

Usage: Web

Install

npm install @anycable/web

# or

yarn add @anycable/web

Initialization

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

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

export default createCable()

By default, the connection URL is looked up in meta tags (action-cable-url or cable-url), and if none found, fallbacks to /cable. You can also specify the URL explicitly:

createCable('ws://cable.example.com/my_cable')

Pub/Sub

[!IMPORTANT] This feature is backed by AnyCable signed streams (available since v1.5). See the documentation.

You can subscribe directly to data streams as follows:

const cable = createCable();

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

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

// Publish transient events
chatChannel.whisper({event: 'typing', user: '<name>'});

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

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

Presence tracking

[!IMPORTANT] This feature is currently supported only by AnyCable+ and edge version of AnyCable server. See the documentation.

You can keep track of the users currently connected to the channel. Let's assume you have the following channel:

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

To join the channel's presence set, you must explicitly provide the user's information:

// The first argument must be a unique user identifier within the channel
// and the second argument is an arbitrary user data (presence information)
chatChannel.presence.join(user.id, { name: user.name })

You MUST join the presence once, no need to do that on every connection or reconnection—our library takes care of this.

You can subscribe to presence events:

chatChannel.presence.on('presence', (ev) => {
  const { type, info, id } = ev

  // Type could be 'join', 'leave', 'presence', or 'error'
  if (type === 'join') {
    console.log("user joined", id, info);
  }

  if (type === 'leave') {
    // no info, just id
    console.log("user left", id);
  }
})

To obtain the current presence state, you can use the info function:

const users = await chatChannel.presence.info()

// users is an object with user ids as keys and user data as values
users //=> { 'user-id': { name: 'John' }, ... }

Calling presence.info() performs a server request on the initial invocation (or when necessary) and uses join / leave events for keeping the information up-to-date.

Note that it's not necessary to join the channel to obtain the presence information.

You can also leave the channel as follows:

chatChannel.presence.leave()

The users leaves the channel automatically on unsubscribe or disconnect (in this case, the presense state update might be delayed depending on the server-side configuration).

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)
  }
}
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
// NOTE: Action Cable doesn't implement a full-featured RPC with ACK messages,
// so return value is always undefined
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()

IMPORTANT: cable.subscribe(channel) is optimistic: it doesn't require the cable to be connected, and waits for it to connect before performing a subscription request. Even if the cable got disconnected before subscription was confirmed or rejected, a new attempt is made as soon as the connectivity restored.

Calling channel.disconnect() removes the subscription for this channel right away and send unsubscribe request asynchronously; if there is no connectivity, we assume that the server takes care of performing unsubscribe tasks, so we don't need to retry them.

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

Action Cable compatibility mode

We provide an Action Cable compatible APIs for smoother migrations.

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).

Lifecycle events

Both cables and channels allow you to subscribe to various lifecycle events for better observability.

Learn more from the dedicated documentation.

Handling connection failures, or automatic reconnects

AnyCable client provides automatic reconnection on network failure out-of-the-box. Under the hood, it uses the exponential backoff with jitter algorithm to make reconnection attempts non-deterministic (and, thus, prevent thundering herd attacks on the server). You can read more about it in the blog post.

The component responsible for reconnection is called Monitor, and it's created automatically, if you use the createCable (or createConsumer) function.

Sometimes it might be useful to disable reconnection. In that case, you MUST pass the monitor: false to the createCable function:

cable = createCable({monitor: false})

TypeScript support

You can make your channels more strict by adding type constraints for parameters, incoming message types and custom events:

// ChatChannel.ts
import { Channel, ChannelEvents } from '@anycable/web'

type Params = {
  roomId: string | number
}

type TypingMessage = {
  type: 'typing'
  username: string
}

type ChatMessage = {
  type: 'message'
  username: string
  userId: string
}

type Message = TypingMessage | ChatMessage

interface Events extends ChannelEvents<Message> {
  typing: (msg: TypingMessage) => void
}

// Which actions can be performed on this channel
interface Actions {
  speak: (msg: ChatMessage) => void
  typing: () => void
}

export class ChatChannel extends Channel<Params,Message,Events,Actions> {
  static identifier = 'ChatChannel'

  receive(message: Message) {
    if (message.type === 'typing') {
      return this.emit('typing', message)
    }

    super.receive(message)
  }

  sendMessage(msg: ChatMessage) {
    return this.perform('speak', msg)
  }

  sendTyping() {
    return this.perform('typing')
  }
}

Now this typings information would help you to provide params or subscribe to events:

let channel: ChatChannel

channel = new ChatChannel({roomId: '2021'}) //=> OK

channel = new ChatChannel({room_id: '2021'}) //=> NOT OK: incorrect params key
channel = new ChatChannel() //=> NOT OK: missing params

channel.on('typing', (msg: TypingMessage) => {}) //=> OK

channel.on('typing', (msg: string) => {}) //=> NOT OK: 'msg' type mismatch
channel.on('types', (msg: TypingMessage) => {}) //=> NOT OK: unknown event

channel.perform('speak', {type: 'message', username: 'John', userId: '42'}) //=> OK
channel.perform('speak', {body: 'hello!'}) //=> NOT OK: incorrect message type

channel.perform('typing') //=> OK
channel.perform('typing', {type: 'typing', username: 'John'}) //=> NOT OK: no payload expected

channel.perform('type') //=> NOT OK: unknown action

Supported protocols

By default, when you call createCable() we use the actioncable-v1-json protocol (supported by Action Cable).

You can also use Msgpack and Protobuf (soon) protocols supported by [AnyCable Pro][pro]:

// cable.js
import { createCable } from '@anycable/web'
import { MsgpackEncoder } from '@anycable/msgpack-encoder'

export default createCable({protocol: 'actioncable-v1-msgpack', encoder: new MsgpackEncoder()})

// or for protobuf
import { createCable } from '@anycable/web'
import { ProtobufEncoder } from '@anycable/protobuf-encoder'

export default createCable({protocol: 'actioncable-v1-protobuf', encoder: new ProtobufEncoder()})

NOTE: You MUST install the corresponding encoder package yourself, e.g., yarn add @anycable/msgpack-encoder or yarn add @anycable/protobuf-encoder.

Extended Action Cable protocol

AnyCable client also supports an extended version of the Action Cable protocol (actioncable-v1-ext-json) implemented by AnyCable server (v1.4+).

This version provides additional functionality to improve data consistency:

  • Session recovery mechanism to restore subscriptions without re-subscribing.
  • History support for streams, or automatic retrieval of missing messages during short-term disconnects.

The features are implemented by the protocol itself, no need to update any existing channels code. All you need is to specify the protocol version when creating a client:

import { createCable } from '@anycable/web'
// or for non-web projects
// import { createCable } from '@anycable/core'

export default createCable({protocol: 'actioncable-v1-ext-json'})

Using with Protobuf and Msgpack

You can use the extended protocol with Protobuf and Msgpack encoders as follows:

// cable.js
import { createCable } from '@anycable/web'
import { MsgpackEncoder } from '@anycable/msgpack-encoder'

export default createCable({protocol: 'actioncable-v1-ext-msgpack', encoder: new MsgpackEncoder()})

// or for protobuf
import { createCable } from '@anycable/web'
import { ProtobufEncoderV2 } from '@anycable/protobuf-encoder'

export default createCable({protocol: 'actioncable-v1-ext-protobuf', encoder: new ProtobufEncoderV2()})

Loading initial history on client initialization

To catch up messages broadcasted during the initial page load (or client-side application initialization), you can specify the historyTimestamp option to retrieve messages after the specified time along with subscription requests. The value must be a UTC timestamp (the number of seconds). For example:

```js expo

Extension points exported contracts — how you extend this code

Protocol (Interface)
(no doc) [6 implementers]
packages/core/protocol/index.d.ts
StartOptions (Interface)
(no doc)
packages/turbo-stream/index.d.ts
MessageObject (Interface)
(no doc)
packages/protobuf-encoder/index.d.ts
Events (Interface)
(no doc)
examples/chat_channel.ts
Receiver (Interface)
(no doc) [10 implementers]
packages/core/channel/index.d.ts
ReplyObject (Interface)
(no doc)
packages/protobuf-encoder/index.d.ts
Transport (Interface)
(no doc) [5 implementers]
packages/core/transport/index.d.ts
Encoder (Interface)
(no doc) [4 implementers]
packages/core/encoder/index.d.ts

Core symbols most depended-on inside this repo

subscribe
called by 120
packages/core/protocol/index.d.ts
on
called by 119
packages/core/transport/index.d.ts
receive
called by 87
packages/core/protocol/index.d.ts
ensureSubscribed
called by 83
packages/core/channel/index.js
connected
called by 59
packages/core/protocol/index.d.ts
perform
called by 56
packages/core/channel/index.d.ts
send
called by 47
packages/core/protocol/index.d.ts
emit
called by 45
packages/core/cable/index.js

Shape

Method 346
Class 222
Function 33
Interface 25
Enum 2

Languages

TypeScript100%

Modules by API surface

packages/core/cable/index.js40 symbols
packages/core/hub/index.js38 symbols
packages/core/protocol/index.d.ts29 symbols
packages/core/create-cable/index.js22 symbols
packages/core/channel/index.js22 symbols
packages/core/protocol/index.js18 symbols
packages/long-polling/index.js17 symbols
packages/core/transport/errors.ts17 symbols
packages/core/channel/index.test.ts17 symbols
packages/core/action_cable_ext/index.js16 symbols
packages/core/transport/testing.ts15 symbols
packages/core/channel/errors.ts15 symbols

For agents

$ claude mcp add anycable-client \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page