{"id":19522302,"url":"https://github.com/anycable/anycable-client","last_synced_at":"2025-04-05T11:07:45.906Z","repository":{"id":43422766,"uuid":"379667264","full_name":"anycable/anycable-client","owner":"anycable","description":"AnyCable / Action Cable JavaScript client for web, Node.js \u0026 React Native","archived":false,"fork":false,"pushed_at":"2024-08-06T12:15:10.000Z","size":976,"stargazers_count":96,"open_issues_count":2,"forks_count":15,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-18T09:34:17.347Z","etag":null,"topics":["actioncable","anycable","hacktoberfest","rails","realtime","typescript","websockets"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anycable.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-23T16:34:07.000Z","updated_at":"2024-10-17T17:27:20.000Z","dependencies_parsed_at":"2023-10-14T23:04:26.387Z","dependency_job_id":"8fa143a5-e90a-4e27-ba00-236e5d97c9f6","html_url":"https://github.com/anycable/anycable-client","commit_stats":{"total_commits":213,"total_committers":8,"mean_commits":26.625,"dds":"0.10328638497652587","last_synced_commit":"a8e84b221bf91cbfbf9ef2d362aa59f64c69670f"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anycable%2Fanycable-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anycable","download_url":"https://codeload.github.com/anycable/anycable-client/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247299786,"owners_count":20916186,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actioncable","anycable","hacktoberfest","rails","realtime","typescript","websockets"],"created_at":"2024-11-11T00:38:20.213Z","updated_at":"2025-04-05T11:07:45.877Z","avatar_url":"https://github.com/anycable.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"[![npm version](https://badge.fury.io/js/%40anycable%2Fcore.svg)](https://badge.fury.io/js/%40anycable%2Fcore)\n[![Test](https://github.com/anycable/anycable-client/workflows/Test/badge.svg)](https://github.com/anycable/anycable-client/actions)\n\n# AnyCable JavaScript Client\n\n[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.\n\nThis repository contains JavaScript packages to build AnyCable clients.\n\n## Motivation\n\nMultiple reasons that forced us to implement an alternative client library for Action Cable / AnyCable:\n\n- [AnyCable Pro][pro] features support (e.g., binary formats).\n- Multi-platform out-of-the-box (web, workers, React Native).\n- [TypeScript support](#typescript-support)\n- Revisited client-side APIs.\n- [Testability](#testing)\n- Features like [presence tracking](#presence-tracking).\n- Additional [protocol extensions](#extended-action-cable-protocol) (e.g., history support).\n- Better [Turbo Streams support](https://github.com/anycable/anycable-client/tree/master/packages/turbo-stream)\n\n📖 Read also the [introductory post](https://evilmartians.com/chronicles/introducing-anycable-javascript-and-typescript-client).\n\n## Usage: Web\n\n### Install\n\n```sh\nnpm install @anycable/web\n\n# or\n\nyarn add @anycable/web\n```\n\n### Initialization\n\nFirst, you need to create a _client_ (or _consumer_ as it's called in Action Cable):\n\n```js\n// cable.js\nimport { createCable } from '@anycable/web'\n\nexport default createCable()\n```\n\nBy 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:\n\n```js\ncreateCable('ws://cable.example.com/my_cable')\n```\n\n### Pub/Sub\n\n\u003e [!IMPORTANT]\n\u003e This feature is backed by AnyCable _signed streams_ (available since v1.5). See the [documentation](https://docs.anycable.io/anycable-go/signed_streams).\n\nYou can subscribe directly to data streams as follows:\n\n```js\nconst cable = createCable();\n\nconst chatChannel = cable.streamFrom('room/42');\n\nchatChannel.on('message', (msg) =\u003e {\n  // ...\n});\n\n// Publish transient events\nchatChannel.whisper({event: 'typing', user: '\u003cname\u003e'});\n```\n\nIn most cases, however, you'd prefer to use secured (_signed_) stream names generated by your backend:\n\n```js\nconst cable = createCable();\nconst signedName = await obtainSignedStreamNameFromWhenever();\nconst chatChannel = cable.streamFromSigned(signedName);\n// ...\n```\n\n### Presence tracking\n\n\u003e [!IMPORTANT]\n\u003e This feature is currently supported only by [AnyCable+](https://plus.anycable.io) and edge version of AnyCable server. See the [documentation](https://docs.anycable.io/edge/anycable-go/presence).\n\nYou can keep track of the users currently connected to the channel. Let's assume you have the following channel:\n\n```js\nconst cable = createCable();\nconst chatChannel = cable.streamFrom('room/42');\n```\n\nTo join the channel's presence set, you must explicitly provide the user's information:\n\n```js\n// The first argument must be a unique user identifier within the channel\n// and the second argument is an arbitrary user data (presence information)\nchatChannel.presence.join(user.id, { name: user.name })\n```\n\nYou MUST join the presence once, no need to do that on every connection or reconnection—our library takes care of this.\n\nYou can subscribe to presence events:\n\n```js\nchatChannel.presence.on('presence', (ev) =\u003e {\n  const { type, info, id } = ev\n\n  // Type could be 'join', 'leave', 'presence', or 'error'\n  if (type === 'join') {\n    console.log(\"user joined\", id, info);\n  }\n\n  if (type === 'leave') {\n    // no info, just id\n    console.log(\"user left\", id);\n  }\n})\n```\n\nTo obtain the current presence state, you can use the `info` function:\n\n```js\nconst users = await chatChannel.presence.info()\n\n// users is an object with user ids as keys and user data as values\nusers //=\u003e { 'user-id': { name: 'John' }, ... }\n```\n\nCalling `presence.info()` performs a server request on the initial invocation (or when necessary) and uses `join` / `leave` events for keeping the\ninformation up-to-date.\n\nNote that it's not necessary to join the channel to obtain the presence information.\n\nYou can also leave the channel as follows:\n\n```js\nchatChannel.presence.leave()\n```\n\nThe 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).\n\n### Channels\n\nAnyCable client provides multiple ways to subscribe to channels: class-based subscriptions and _headless_ subscriptions.\n\n\u003e [!TIP]\n\u003e Read more about the concept of channels and how AnyCable uses it [here](https://docs.anycable.io/edge/anycable-go/rpc).\n\n#### Class-based subscriptions\n\nClass-based APIs allows provides an abstraction layer to hide implementation details of subscriptions.\nYou can add additional API methods, dispatch custom events, etc.\n\nLet's consider an example:\n\n```js\nimport { Channel } from '@anycable/web'\n\n// channels/chat.js\nexport default class ChatChannel extends Channel {\n  // Unique channel identifier (channel class for Action Cable)\n  static identifier = 'ChatChannel'\n\n  async speak(message) {\n    return this.perform('speak', { message })\n  }\n\n  receive(message) {\n    if (message.type === 'typing') {\n      // Emit custom event when message type is 'typing'\n      return this.emit('typing', message)\n    }\n\n    // Fallback to the default behaviour\n    super.receive(message)\n  }\n}\n```\n\n```js\nimport cable from 'cable'\nimport { ChatChannel } from 'channels/chat'\n\n// Build an instance of a ChatChannel class.\nconst channel = new ChatChannel({ roomId: '42' })\n\n// Subscribe to the server channel via the client.\ncable.subscribe(channel) // return channel itself for chaining\n\n// Wait for subscription confirmation or rejection\n// NOTE: it's not necessary to do that, you can perform actions right away,\n// the channel would wait for connection automatically\nawait channel.ensureSubscribed()\n\n// Perform an action\n// NOTE: Action Cable doesn't implement a full-featured RPC with ACK messages,\n// so return value is always undefined\nlet _ = await channel.speak('Hello')\n\n// Handle incoming messages\nchannel.on('message', msg =\u003e console.log(`${msg.name}: ${msg.text}`))\n\n// Handle custom typing messages\nchannel.on('typing', msg =\u003e console.log(`User ${msg.name} is typing`))\n\n// Or subscription close events\nchannel.on('close', () =\u003e console.log('Disconnected from chat'))\n\n// Or temporary disconnect\nchannel.on('disconnect', () =\u003e console.log('No chat connection'))\n\n// Unsubscribe from the channel (results in a 'close' event)\nchannel.disconnect()\n```\n\n**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.\n\nCalling `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.\n\n#### Headless subscriptions\n\n_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).\n\nLet's rewrite the same example using headless subscriptions:\n\n```js\nimport cable from 'cable'\n\nconst subscription = cable.subscribeTo('ChatChannel', { roomId: '42' })\n\nconst _ = await subscription.perform('speak', { msg: 'Hello' })\n\nsubscription.on('message', msg =\u003e {\n  if (msg.type === 'typing') {\n    console.log(`User ${msg.name} is typing`)\n  } else {\n    console.log(`${msg.name}: ${msg.text}`)\n  }\n})\n```\n\n#### Action Cable compatibility mode\n\nWe provide an Action Cable compatible APIs for smoother migrations.\n\nAll you need is to change the imports:\n\n```diff\n- import { createConsumer } from \"@rails/actioncable\";\n+ import { createConsumer } from \"@anycable/web\";\n\n // createConsumer accepts all the options available to createCable\n export default createConsumer();\n```\n\nThen you can use `consumer.subscriptions.create` as before (under the hood a headless channel would be create).\n\n### Lifecycle events\n\nBoth cables and channels allow you to subscribe to various lifecycle events for better observability.\n\nLearn more from the dedicated [documentation](./docs/lifecycle.md).\n\n### Handling connection failures, or automatic reconnects\n\nAnyCable 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](https://evilmartians.com/chronicles/introducing-anycable-javascript-and-typescript-client#connect-reconnect-and-a-bit-of-mathematica).\n\nThe component responsible for reconnection is called _Monitor_, and it's created automatically, if you use the `createCable` (or `createConsumer`) function.\n\nSometimes it might be useful to disable reconnection. In that case, you MUST pass the `monitor: false` to the `createCable` function:\n\n```js\ncable = createCable({monitor: false})\n```\n\n### TypeScript support\n\nYou can make your channels more strict by adding type constraints for parameters, incoming message types and custom events:\n\n```ts\n// ChatChannel.ts\nimport { Channel, ChannelEvents } from '@anycable/web'\n\ntype Params = {\n  roomId: string | number\n}\n\ntype TypingMessage = {\n  type: 'typing'\n  username: string\n}\n\ntype ChatMessage = {\n  type: 'message'\n  username: string\n  userId: string\n}\n\ntype Message = TypingMessage | ChatMessage\n\ninterface Events extends ChannelEvents\u003cMessage\u003e {\n  typing: (msg: TypingMessage) =\u003e void\n}\n\n// Which actions can be performed on this channel\ninterface Actions {\n  speak: (msg: ChatMessage) =\u003e void\n  typing: () =\u003e void\n}\n\nexport class ChatChannel extends Channel\u003cParams,Message,Events,Actions\u003e {\n  static identifier = 'ChatChannel'\n\n  receive(message: Message) {\n    if (message.type === 'typing') {\n      return this.emit('typing', message)\n    }\n\n    super.receive(message)\n  }\n\n  sendMessage(msg: ChatMessage) {\n    return this.perform('speak', msg)\n  }\n\n  sendTyping() {\n    return this.perform('typing')\n  }\n}\n```\n\nNow this typings information would help you to provide params or subscribe to events:\n\n```ts\nlet channel: ChatChannel\n\nchannel = new ChatChannel({roomId: '2021'}) //=\u003e OK\n\nchannel = new ChatChannel({room_id: '2021'}) //=\u003e NOT OK: incorrect params key\nchannel = new ChatChannel() //=\u003e NOT OK: missing params\n\nchannel.on('typing', (msg: TypingMessage) =\u003e {}) //=\u003e OK\n\nchannel.on('typing', (msg: string) =\u003e {}) //=\u003e NOT OK: 'msg' type mismatch\nchannel.on('types', (msg: TypingMessage) =\u003e {}) //=\u003e NOT OK: unknown event\n\nchannel.perform('speak', {type: 'message', username: 'John', userId: '42'}) //=\u003e OK\nchannel.perform('speak', {body: 'hello!'}) //=\u003e NOT OK: incorrect message type\n\nchannel.perform('typing') //=\u003e OK\nchannel.perform('typing', {type: 'typing', username: 'John'}) //=\u003e NOT OK: no payload expected\n\nchannel.perform('type') //=\u003e NOT OK: unknown action\n```\n\n### Supported protocols\n\nBy default, when you call `createCable()` we use the `actioncable-v1-json` protocol (supported by Action Cable).\n\nYou can also use Msgpack and Protobuf (_soon_) protocols supported by [AnyCable Pro][pro]:\n\n```js\n// cable.js\nimport { createCable } from '@anycable/web'\nimport { MsgpackEncoder } from '@anycable/msgpack-encoder'\n\nexport default createCable({protocol: 'actioncable-v1-msgpack', encoder: new MsgpackEncoder()})\n\n// or for protobuf\nimport { createCable } from '@anycable/web'\nimport { ProtobufEncoder } from '@anycable/protobuf-encoder'\n\nexport default createCable({protocol: 'actioncable-v1-protobuf', encoder: new ProtobufEncoder()})\n```\n\n**NOTE:** You MUST install the corresponding encoder package yourself, e.g., `yarn add @anycable/msgpack-encoder` or `yarn add @anycable/protobuf-encoder`.\n\n### Extended Action Cable protocol\n\nAnyCable client also supports an extended version of the Action Cable protocol (`actioncable-v1-ext-json`) implemented by AnyCable server (v1.4+).\n\nThis version provides additional functionality to improve data consistency:\n\n- Session recovery mechanism to restore subscriptions without re-subscribing.\n- History support for streams, or automatic retrieval of missing messages during short-term disconnects.\n\nThe 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:\n\n```js\nimport { createCable } from '@anycable/web'\n// or for non-web projects\n// import { createCable } from '@anycable/core'\n\nexport default createCable({protocol: 'actioncable-v1-ext-json'})\n```\n\n#### Using with Protobuf and Msgpack\n\nYou can use the extended protocol with Protobuf and Msgpack encoders as follows:\n\n```js\n// cable.js\nimport { createCable } from '@anycable/web'\nimport { MsgpackEncoder } from '@anycable/msgpack-encoder'\n\nexport default createCable({protocol: 'actioncable-v1-ext-msgpack', encoder: new MsgpackEncoder()})\n\n// or for protobuf\nimport { createCable } from '@anycable/web'\nimport { ProtobufEncoderV2 } from '@anycable/protobuf-encoder'\n\nexport default createCable({protocol: 'actioncable-v1-ext-protobuf', encoder: new ProtobufEncoderV2()})\n```\n\n#### Loading initial history on client initialization\n\nTo 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:\n\n```js\nexport default createCable({\n  protocol: 'actioncable-v1-ext-json',\n  protocolOptions: {\n    historyTimestamp: 1614556800 // 2021-03-01 00:00:00 UTC\n  }\n})\n```\n\nBy default, we use the current time (`Date.now() / 1000`). For web applications, you can specify the value using a meta tag with the name \"action-cable-history-timestamp\" (or \"cable-history-timestamp\"). For example, in Rails, you can add the following to your application layout\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003c!-- ... --\u003e\n    \u003c%= action_cable_meta_tag %\u003e\n    \u003cmeta name=\"action-cable-history-timestamp\" content=\"\u003c%= Time.now.to_i %\u003e\"\u003e\n  \u003c/head\u003e\n  \u003c!-- ... --\u003e\n\u003c/html\u003e\n```\n\nThis is a recommended way to use this feature with Hotwire applications, where initial state is included in the HTML response.\n\n**IMPORTANT:** For later subscriptions (not during the initial page initialization), the value of the `historyTimestamp` is automatically adjusted to the last time a \"ping\" message has been received.\n\nYou can also disable retrieving history since the specified time completely by setting the `historyTimestamp` option to `false`.\n\n#### Handling history retrieval failures\n\nAnyCable reliable streams store history for a finite period of time and also have an upper size limit. Thus, in some cases, clients may fail to retrieve the missed messages (e.g., after a long-term disconnect). To gracefully handle this situation, you may decide to fallback to a full state reset (e.g., a browser page reload). You can use the specific \"info\" event to react on various protocol-level events not exposed to the generic Channel interface:\n\n```js\nimport { createCable, Channel } from '@anycable/web'\n\nconst cable = createCable({protocol: 'actioncable-v1-ext-json'});\n\nclass ChatChannel extends Channel {\n  static identifier = 'ChatChannel'\n\n  constructor(params) {\n    super(params)\n\n    this.on(\"info\", (evt) =\u003e {\n      if (evt.type === \"history_not_found\") {\n        // Restore state by performing an action\n        this.perform(\"resetState\")\n      }\n\n      // Successful history retrieval is also notified\n      if (evt.type === \"history_received\") {\n        // ...\n      }\n    })\n  }\n}\n```\n\n#### PONGs support\n\nThe extended protocol also support sending `pong` commands in response to `ping` messages. A server (AnyCable-Go) keeps track of pongs and disconnect the client if no pongs received in time. This helps to identify broken connections quicker.\n\nYou must opt-in to use this feature by setting the `pongs` option to `true`:\n\n```js\nexport default createCable({\n  protocol: 'actioncable-v1-ext-json',\n  protocolOptions: {\n    pongs: true\n  }\n})\n```\n\n### Token-based authentication\n\nAnyCable SDK provides several features to simplify using token-based authentication (either an [official AnyCable one](https://docs.anycable.io/anycable-go/jwt_identification) or a custom one).\n\n#### Refreshing tokens\n\nAnyCable provides a smooth mechanism to refresh tokens for a long-lived clients (to let them reconnect in case of a connection failure): just provide a function, which could retrieve a new token and update the connection url or parameters. AnyCable will take care of everything else (tracking expiration and reconnecting). Here is an example:\n\n```js\n// cable.js\nimport { createCable } from '@anycable/web'\n\nexport default createCable({\n  tokenRefresher: async transport =\u003e {\n    let response = await fetch('/token.json')\n    let data = await response.json()\n\n    // Update URL for the underlying transport\n    transport.setURL('ws://example.com/cable?jid=' + data['token'])\n\n    // or update only the token\n    transport.setToken(data['token'])\n  }\n})\n```\n\nFor browser usage, we provide a built-in helper method, which allows you to extract a new connection URL from an HTML page (requested via `fetch`):\n\n```js\n// cable.js\nimport { createCable, fetchTokenFromHTML } from '@anycable/web'\n\n// By default, the current page is loaded in the background,\n// and the action-cable-url (or cable-url) meta tag is used to update\n// the connection url and the cable-token meta tag is used to update the token (if any)\nexport default createCable({tokenRefresher: fetchTokenFromHTML()})\n\n// You can also specify an alternative URL\nexport default createCable({\n  tokenRefresher: fetchTokenFromHTML({ url: '/custom-token-refresh-endpoint' })\n})\n```\n\n**NOTE:** the `tokenRefresher` only activates when a server sends a disconnection message with reason `token_expired` (i.e., `{\"type\":\"disconnect\",\"reason\":\"token_expired\",\"reconnect\":false}`).\n\n**NODE:** the `fetchTokenFromHTML` performs an HTTP request with a specific header attached (`X-ANYCABLE-OPERATION=token-refresh`), which you could use to minimize the amount of HTML to return in response.\n\n#### Providing initial token value\n\nThere are several ways to provide an initial token value. You can provide it as a part of the initial connection URL. In this case, no additional actions are required.\n\nYou can also provide a token during the cable initialization:\n\n```js\nimport { createCable } from '@anycable/web'\n\nexport default createCable({\n  auth: {token: \"secret-value\"}\n});\n```\n\nBy default, the token is added to the URL as a query parameter. We use the `jid` parameter name by default (to match the default AnyCable JWT functionality). You can change this by specifying the `param` option:\n\n```js\nexport default createCable({\n  auth: {token: \"secret-value\", param: \"token\"}\n});\n```\n\nWhen using `@anycable/web` package, you can inject the token into the HTML page using a meta tag with the `cable-token` name:\n\n```html\n\u003cmeta name=\"cable-token\" content=\"secret-token\"\u003e\n\n\u003c!-- you can also specify a custom param name for token --\u003e\n\u003cmeta name=\"cable-token-param\" content=\"token\"\u003e\n```\n\nIn some cases, the only way to obtain a token is to make a request to the server. However, it might be necessary to initialize a _cable_ instance right on the application start, without waiting for the token to be fetched. In this case, you can use a `transportCongfigurator` option, which allows you to specify an async function to be called right before the connection is established (so you can tweak the parameters). For example:\n\n```js\nimport { createCable } from '@anycable/web'\n\nexport default createCable({\n  transportConfigurator: async (transport, { initial }) =\u003e {\n    // The initial flag indicates whether this is the first connetion attempt or a reconnection\n    if (!initial) return;\n\n    transport.setURL('ws://example.com/cable')\n\n    let response = await fetch('/token.json')\n    let data = await response.json()\n\n    transport.setToken(data['token'], 'token')\n  }\n})\n```\n\nNote that you still need the `tokenRefresher` to handle token expiration, because configurator does not handle this for you.\n\n#### Ways to attach a token to a WebSocket connection\n\nWebSockets in browsers do not support providing custom headers. Thus, we use query parameters to pass the token by default (when you use the `transport.setToken` function).\n\nWhen using WebSockets in non-browser environments (such as Node.js, React Native), you can use HTTP headers. If you want to do that with AnyCable, you can specify the `websocketAuthStrategy` option:\n\n```js\nimport WebSocket from 'ws'\nimport { createCable } from '@anycable/core'\n\nlet cable = createCable(url, {\n  websocketImplementation: WebSocket,\n  websocketAuthStrategy: 'header',\n  auth: {token: 'secret-token'}\n})\n```\n\nThe configuration above would add the `x-jid: secret-token` header to the WebSocket connection.\n\nWe also support the `sub-protocol` strategy, which adds a specificly formed sub-protocol to the list of the client suppported protocols. This approach works in the browser, too:\n\n```js\nexport default createCable({\n  auth: {token: \"secret-value\"},\n  websocketAuthStrategy: 'sub-protocol'\n});\n```\n\nA WebSocket connection created by such cable will have the `anycable-token.secret-value` sub-protocol in the list of protocols (`sec-websocket-protocol` header) in addition to the usual sub-protocols.\n\n**NOTE:** the `sub-protocol` strategy is not supported by all WebSocket servers. It has been added to AnyCable in v1.6.\n\n### Hotwire (Turbo Streams) support\n\nTo use AnyCable client with [Turbo Streams][turbo-streams], we provide a tiny plugin—`@anycable/turbo-stream`. It allows you to configure a Cable instance yourself to use with Turbo Stream source elements:\n\n```js\nimport { start } from \"@anycable/turbo-stream\"\nimport cable from \"cable\"\n// Explicitly activate stream source elements\nstart(cable)\n```\n\nRead more in the package's [Readme](./packages/turbo-stream/README.md).\n\n### Testing\n\nFor testing your channel you can use test cable implementation from `@anycable/core/testing`.\n\nBy using test cable implementation you can test channel's output actions. All actions store in cable `outgoing` property.\nAlso test implementation helps to test channel `disconnect` event.\n\nFor example we have the following channel implementation.\n\n```js\nimport { Channel } from \"@anycable/core\";\n\nclass ChatChannel extends Channel {\n  static identifier = \"ChatChannel\";\n\n  async speak(message) {\n    return this.perform(\"speak\", { message });\n  }\n\n  leave() {\n    // some custom logic\n    return this.disconnect();\n  }\n}\n```\n\nWe can test it like this (using `Jest`):\n\n```js\nimport { Channel } from './channel.js'\nimport { TestCable } from '@anycable/core/testing'\n\ndescribe('ChatChannel', () =\u003e {\n  let channel: Channel\n  let cable: TestCable\n\n  beforeEach(() =\u003e {\n    cable = new TestCable()\n    channel = new Channel()\n    cable.subscribe(channel)\n  })\n\n  it('perform an speak action', async () =\u003e {\n    await channel.speak('hello')\n    await channel.speak('bye')\n\n    expect(cable.outgoing).toEqual([\n      { action: 'speak', payload: { message: 'hello' } },\n      { action: 'speak', payload: { message: 'bye' } }\n    ])\n  })\n\n  it('disconnects when leave', async () =\u003e {\n    channel.leave()\n\n    expect(channel.state).toEqual('closed')\n  })\n})\n```\n\n### Babel/Browserlist configuration\n\nThis library uses ECMAScript 6 features (such as native classes), and thus, is not compatible with ES5 (for example, IE11 is not supported out-of-the-box).\n\nYou should either configure Babel to transform the lib's source code or do not compile into ES5 (that could be done by specifying the following Browserlist query: `[\"defaults\", \"not IE 11\"]`).\n\nIf you're using `babel-loader`, `esbuild-loader` or similar, you can use the `include` option to add `@anycable/*` libraries to the processed files. For example:\n\n```js\n{\n  include: [\n    path.resolve(\"src\"),\n    path.resolve('node_modules/@anycable'),\n  ]\n}\n```\n\n### CommonJS compatibility\n\nWe use ESM and don't any have any plans to support CommonJS ourselves. You can try [commonizer](https://github.com/wintercounter/commonizer) for that.\n\n## Usage: Node.js\n\nCurrently, we do not provide a dedicated Node.js package. You can use `@anycable/core` with Node.js:\n\n```js\n// WebSocket implementation compatible with the web WebSocket API is required\nimport WebSocket from 'ws'\nimport { createCable } from '@anycable/core'\n\n// NOTE: Passing url is required\nlet cable = createCable(url, {\n  websocketImplementation: WebSocket\n})\n\n// You can also pass additional connections options,\n// supported by ws via the websocketOptions\nlet cableWithHeader = createCable(url, {\n  websocketImplementation: WebSocket,\n  websocketOptions: { headers: { 'x-token': 'secret' }}\n})\n```\n\n**IMPORTANT:** We use ES modules, hence setting `NODE_OPTIONS='--experimental-vm-modules'` is currently required.\n\nSee also `examples/benchmark_channel.js`.\n\n## Usage: React Native\n\nCurrently, we do not provide a dedicated React Native package. You can use `@anycable/core` just like with Node.js:\n\n```js\nimport { createCable } from '@anycable/core'\n\n// NOTE: Passing url is required\nlet cable = createCable(url)\n\n// You can also pass additional connections options,\n// such as headers, via the websocketOptions\nlet cableWithHeader = createCable(url, {\n  websocketOptions: { headers: { 'x-token': 'secret' }}\n})\n```\n\n## Duplicating or reusing channel instances?\n\nIt is safe to call `cable.subscribe(channel)` multiple times—only a single subscription (from the protocol point of view) is made, i.e., this action is idempotent. At the same time, it's safe to have multiple channel instances with the same identifiers client-side—only a single _real_ subscription would be made.\n\nLet's consider an example. Suppose you have two _components_ relying on the same channel:\n\n```js\n// component-one.js\nimport cable from 'cable'\nimport { NotificationsChannel } from 'channels/notifications_channel'\n\n// Build an instance of a NotificationChannel class.\nconst channel = new NotificationChannel()\n\n// Subscribe to the server channel via the client.\ncable.subscribe(channel)\n\nchannel.on('message', msg =\u003e console.log(\"component one received message\", `${msg.name}: ${msg.text}`))\n\n// component-two.js\nimport cable from 'cable'\nimport { NotificationsChannel } from 'channels/notifications_channel'\n\n// Build an instance of a NotificationChannel class.\nconst channel = new NotificationChannel()\n\n// Subscribe to the server channel via the client.\ncable.subscribe(channel)\n\nchannel.on('message', msg =\u003e console.log(\"component two received message\", `${msg.name}: ${msg.text}`))\n```\n\nThe code above would work as expected: both channel instances would receive updates from the server. Calling `channel.disconnect()` would detach this particular channel from the cable, but wouldn't perform the actual `unsubscribe` command (from the server perspective) unless that's the last channel with this identifier.\n\nAlternatively, you may consider extracting a channel instance to a separate module and reuse it:\n\n```js\n// channels/notifications_channel.js\nimport { Channel } from '@anycable/core'\nimport 'cable' from 'cable'\n\nexport class NotificationsChannel extends Channel {\n  // ...\n}\n\nlet instance\n\nexport function createChannel() {\n  if (!instance) {\n    instance = new NotificationChannel()\n    cable.subscribe(channel)\n  }\n\n  return instance\n}\n\n// component-one.js\nimport cable from 'cable'\nimport { createChannel } from 'channels/notifications_channel'\n\nconst channel = createChannel()\n\nchannel.on('message', msg =\u003e console.log(\"component one received message\", `${msg.name}: ${msg.text}`))\n\n// component-two.js\nimport cable from 'cable'\nimport { createChannel } from 'channels/notifications_channel'\n\nconst channel = createChannel()\n\nchannel.on('message', msg =\u003e console.log(\"component two received message\", `${msg.name}: ${msg.text}`))\n```\n\nWhich way to choose is up to the developer. From the library point of view, both are viable and supported.\n\n## Fine-tuning for higher loads 📈\n\n### Customizing PING interval\n\nThe default PING interval is 3 seconds. When server handles tons of connections, sending pings that often might result in a noticeable overhead.\n\nThis value is defined by the WebSocket server. If you use AnyCable, you can customize it via the `--ping_interval` parameter. Also, since [v1.4.3](https://github.com/anycable/anycable-go/releases/tag/v1.4.3), AnyCable-Go allows you to configure a ping interval for an individual connection by adding a query param to the connection URL (`?pi=10`).\n\nWe recommend increasing the ping interval for high-load applications.\n\nYou MUST also update the client-side configuration to use the same value (so the client won't decide to reconnect due to missing pings):\n\n```js\nexport default createCable({\n  pingInterval: 10000 // 10 seconds\n})\n```\n\n### Customizing subscription confirmation timeout\n\nBy default, we expect a subscription confirmation (or rejection) to arrive **within 5 seconds**. If it doesn't happen, AnyCable client re-issues the subscription request and waits for another 5 seconds. If the second attempt fails, the **subscription is considered to be rejected** due to timeout.\n\nUnder load (usually, during _connection avalanches_, i.e., when most clients are re-connecting), the server might not be able to respond within 5 seconds (and even 10 seconds). In that case, you can increase the retry interval:\n\n```js\nexport default createCable({\n  protocolOptions: {\n    subscribeRetryInterval: 10000 // 10 seconds\n  }\n})\n```\n\n### Linearizing subscription requests\n\nAnother way to reduce the load on the server and avoid subscription timeouts is to **linearize subscription requests**. By default, AnyCable client sends subscription requests concurrently. However, you have many active subscriptions, this might result in a huge number of requests sent to the server at the same time during re-connections.\n\nTo smooth the load, you can disable concurrent subscription requests (so, every next subscription request would be sent only after the previous one is confirmed or rejected):\n\n```js\nexport default createCable({\n  concurrentSubscribes: false\n});\n```\n\n## Other configuration options\n\nThere is a plenty of options available to configure the client. Please, refer to the full list in the [TS definition file](./packages/core/cable/index.d.ts).\nHere are the most popular/useful ones (and not yet mentioned in the docs):\n\n```js\nimport { createCable } from '@anycable/core'\n\n// Below you can find some options and their default values\nconst cable = createCable({\n  logLevel: 'info', //  use 'debug' for more verbose output and troubleshooting\n  performFailures: 'throw', // indicates how to treat channel.perform(...) failures; use `warn` or 'ignore' to silence errors\n  lazy: true, // if true, the connection would be established only when the first subscription is made\n  logger: console, // custom logger (must implement `log`, `info`, `warn`, `error`, `debug` methods)\n})\n```\n\n## Further reading\n\n- [Architecture](./docs/architecture.md)\n\n[anycable]: https://anycable.io\n[protocol]: https://docs.anycable.io/misc/action_cable_protocol\n[pro]: https://anycable.io/#pro\n[turbo-streams]: https://turbo.hotwired.dev/reference/streams\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanycable%2Fanycable-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanycable%2Fanycable-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanycable%2Fanycable-client/lists"}