-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
199 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
# Using AnyCable JS SDK | ||
|
||
> See the full documentation at [anycable/anycable-client](https://github.com/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-go/binary_formats.md)). | ||
- AnyCable-specific features support (e.g., [reliable streams](./anycable-go/reliable_streams.md) and [signed streams](./anycable-go/signed_streams.md)). | ||
- Better [Turbo Streams support](#hotwire-integration) | ||
- ... and more. | ||
|
||
## Quick start | ||
|
||
You can install AnyCable JS SDK via npm/yard/pnpm: | ||
|
||
```bash | ||
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): | ||
|
||
```js | ||
// 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: | ||
|
||
```js | ||
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](./anycable-go/signed_streams.md)): | ||
|
||
```js | ||
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](./anycable-go/rpc). | ||
#### 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: | ||
|
||
```js | ||
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: | ||
|
||
```js | ||
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: | ||
|
||
```js | ||
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: | ||
|
||
```diff | ||
- 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](https://github.com/anycable/anycable-client/tree/master/packages/turbo-stream). | ||
|
||
Here is how to switch `@hotwired/turbo` to use AnyCable client: | ||
|
||
```diff | ||
- import "@hotwired/turbo-rails"; | ||
+ import "@hotwired/turbo"; | ||
+ import { start } from "@anycable/turbo-stream"; | ||
+ import cable from "cable" | ||
+ | ||
+ start(cable, { delayedUnsubscribe: true }) | ||
``` |