From 6307ead7d838b653ec29f7def529e11bafbd478b Mon Sep 17 00:00:00 2001 From: Vladimir Dementyev Date: Wed, 9 Oct 2024 14:25:13 -0700 Subject: [PATCH] + client-side.md --- docs/README.md | 8 +- docs/_sidebar.md | 1 + docs/guides/client-side.md | 196 +++++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 docs/guides/client-side.md diff --git a/docs/README.md b/docs/README.md index 9db0b89..1240837 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,18 +26,14 @@ Make your real-time communication fast and [reliable](./anycable-go/reliable_str ## Latest updates 🆕 +- **2024-10-08**: [File-based configuration (`anycable.toml`)](./anycable-go/configuration.md) + - **2024-03-12**: [Standalone mode via signed streams](./anycable-go/signed_streams.md) - **2023-11-08**: [AnyCable for serverlsess JavaScript applications](./guides/serverless.md) -- **2023-11-03**: [NATS JetStream broker](./anycable-go/reliable_streams.md#nats) support is added to AnyCable-Go v1.4.7+. - -- **2023-10-13**: [Batch broadcasts](./ruby/broadcast_adapters.md#broadcast-options) and [broadcasting to others](./rails/getting_started.md#action-cable-extensions). - - **2023-09-07**: [Server-sent events](./anycable-go/sse.md) suppport is added to AnyCable-Go 1.4.4+. -- **2023-08-09**: `pong` command is added to the [extended Action Cable protocol](./misc/action_cable_protocol.md#action-cable-extended-protocol) and is supported by AnyCable-Go 1.4.3+. - - **2023-08-04**: [Slow drain mode for disconnecting clients on shutdown pro](./anycable-go/configuration.md#slow-drain-mode) - **2023-07-05**: [Reliable streams](./anycable-go/reliable_streams.md) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index f8d8f54..51f03ee 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -10,6 +10,7 @@ * Guides * [Using with Rails](/rails/getting_started.md) + * [Client-side usage](/guides/client-side.md) * [Using with JavaScript (serverless)](/guides/serverless.md) * [Using with Hotwire](/guides/hotwire.md) * [Broadcasting](/anycable-go/broadcasting.md) diff --git a/docs/guides/client-side.md b/docs/guides/client-side.md new file mode 100644 index 0000000..3cc5e12 --- /dev/null +++ b/docs/guides/client-side.md @@ -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 }) +```