Turbo Streams meets Action Cable
Tags: rails, hotwire, websockets, turbo
Published: 2022-11-16 16:30:47

_This post was extracted and adapted from [The Rails and Hotwire Codex](https://railsandhotwirecodex.com)._

[Turbo Streams](https://turbo.hotwired.dev/handbook/streams) are a great match-up for Action Cable as it makes it easy to broadcast DOM updates to the browser over WebSockets. The [`turbo-rails`](http://github.com/hotwired/turbo-rails) gem integrates with Action Cable to facilitate such updates.

In this blog post, we'll look at how this integration works. Starting with code examples to establish a connection and broadcast Turbo Streams, we'll then dig into the individual client and server components of the integration.

## An overview

`turbo-rails` contains a [JavaScript custom element](https://javascript.info/custom-elements) which encapsulates the logic to create an Action Cable connection, subscribe to a channel and execute received Turbo Streams. On the server-side, it provides a default channel and a plethora of helper methods to broadcast data based on models.

On the client, an Action Cable connection and subscription is established using:

```erb
<%= turbo_stream_from @conversation %>
```

This will render a `<turbo-cable-stream-source>` element which subscribes to a stream for the `@conversation` object. Turbo Streams can then be broadcast to it using:

```ruby
Turbo::StreamsChannel.broadcast_action_to(
  @conversation,
  action: :append,
  target: dom_id(@conversation, :messages),
  partial: "messages/message"
)
```

There are helper methods to broadcast the stock Turbo Stream actions. The above snippet can be rewritten as:

```ruby
Turbo::StreamsChannel.broadcast_append_to(
  @conversation,
  target: dom_id(@conversation, :messages),
  partial: "messages/message"
)
```

You can also broadcast a Turbo Stream template containing multiple actions:

```ruby
Turbo::StreamsChannel.broadcast_render_to(
  @conversation,
  template: "messages/create"
)
```

All the above examples render templates and broadcast them synchronously. This isn't usually desirable. It should be offloaded to a background job so the request-response cycle isn't held up.

```ruby
# enqueues a `Turbo::Streams::ActionBroadcastJob`
Turbo::StreamsChannel.broadcast_append_later_to(
  @conversation,
  target: dom_id(@conversation, :messages),
  partial: "messages/message"
)

# enqueues a `Turbo::Streams::BroadcastJob`
Turbo::StreamsChannel.broadcast_render_later_to(
  @conversation,
  template: "messages/create"
)
```

Check out the docs for all available helpers: https://rubydoc.info/github/hotwired/turbo-rails/Turbo/Streams/Broadcasts.

In addition to this, there's also a [`Broadcastable`](https://rubydoc.info/github/hotwired/turbo-rails/Turbo/Broadcastable) concern which is ``include``d in Active Record. It applies Rails conventions to succinctly broadcast model-specific Turbo Streams. Some example use cases are:

```ruby
# Broadcasts an `append` action containing the partial
# `conversations/conversation` targeted at the
# ID `conversations`.
Conversation.first.broadcast_append

# The update action targets the specific model's HTML element.
# In this case, it will target `conversation_1`. The content
# will be the partial `conversations/conversation`.
Conversation.first.broadcast_update

# The partial can be explicitly defined if required.
Conversation.first.broadcast_append(
  partial: "messages/message"
)
```

I recommend taking a peek at the docs to familiarize yourself with all the model-based broadcast methods: https://rubydoc.info/github/hotwired/turbo-rails/Turbo/Broadcastable.

The broadcast helpers in `Turbo::StreamsChannel`, which we looked at earlier, are all available in `Broadcastable` as well.

Under the hood, all the broadcast helpers use `ActionCable.server.broadcast(...)`. Hence, they just provide some syntactic sugar on top of Action Cable.

Next, let's take a closer look at the custom element which receives and processes Turbo Streams.

## Turbo Cable Stream Source element

As mentioned earlier, this element is rendered using:

```erb
<%= turbo_stream_from @conversation %>
```

This will render as:

```html
<turbo-cable-stream-source
  channel="Turbo::StreamsChannel"
  signed-stream-name="...">
</turbo-cable-stream-source>
```

This element is underpinned by the class [`TurboCableStreamSourceElement`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable_stream_source_element.js).

The subscription is established using the two HTML attributes. We can specify a custom channel if the default one isn't suitable:

```erb
<%= turbo_stream_from @conversation,
      channel: "ConversationsChannel" %>
```

The _signed stream name_ is generated from the `@conversation` object. You can use multiple arguments for this as well.

```erb
<%= turbo_stream_from @conversation, :messages,
      channel: "ConversationsChannel" %>
```

Let's dig into how the stream names are generated.

### A closer look at stream names

Any type of object can used to generate the stream name as long as it responds to `to_gid_param` or `to_param`. Hence, it's usually an Active Record object, string or symbol.

`to_gid_param` generates the [Global ID](https://github.com/rails/globalid) for a model and then Base64 encodes it. For strings, `to_param` returns the object itself. For symbols, it returns the object's string representation.

The stream name for a `Conversation` object would look something like:

```
Z2lkOi8vcGlhenphL0NvbnZlcnNhdGlvbi8x
```

Base64 decoding the above value will reveal the object's Global ID:

```ruby
>> stream = "Z2lkOi8vcGlhenphL0NvbnZlcnNhdGlvbi8x"
>> Base64.urlsafe_decode64(stream)

# => "gid://piazza/Conversation/1"
```

The stream name is run through a `MessageVerifier` to obtain the _signed stream name_ which is rendered in the HTML attribute. This ensures the stream name can't be tampered with on the client.

When using multiple arguments, each one will have `to_gid_param` or `to_param` called on it individually. The return values are then joined with a `:`. The stream name for:

```erb
<%= turbo_stream_from @conversation, :messages,
      channel: "ConversationsChannel" %>
```

will look like:

```
Z2lkOi8vcGlhenphL0NvbnZlcnNhdGlvbi8x:messages
```

Using the signed stream name and channel name, the custom element establishes an Action Cable connection and subscribes to the desired stream, ready to receive Turbo Streams over a WebSocket.

## Processing Turbo Streams

The easiest way to execute Turbo Streams is by adding them to the DOM. Another way to is by using the `connectStreamSource` method.

```js
import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"

// ...

connectStreamSource(source)

// ...

disconnectStreamSource(source)
```

The `source` can be any object capable of dispatching [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent)s. Turbo will listen for these events and execute the Turbo Streams they contain.

`TurboCableStreamSourceElement` connects itself as a _stream source_ when it's connected to the DOM. When it receives a Turbo Stream over the WebSocket connection, it dispatches a `MessageEvent` containing it, which Turbo executes.

## The default channel

`turbo-rails` has a default channel called [`Turbo::StreamsChannel`](https://github.com/hotwired/turbo-rails/blob/main/app/channels/turbo/streams_channel.rb) to handle broadcasts and subscriptions. We briefly encountered it while discussing the broadcast helpers. It verifies the signed stream name and subscribes the client to the appropriate stream.

```ruby
class Turbo::StreamsChannel < ActionCable::Channel::Base
  extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
  include Turbo::Streams::StreamName::ClassMethods

  def subscribed
    if stream_name = verified_stream_name_from_params
      stream_from stream_name
    else
      reject
    end
  end
end
```

This is generic code and hence contains no authorization logic. I highly recommend writing your own channel based off `Turbo::StreamsChannel` where you run authorization checks before approving the stream.

Most of the channel's logic in encapsulated in concerns making it easy to reuse in your own channel.

## Conclusion

That's the Turbo Streams integration with Action Cable in a nutshell. It packs in immense power with relatively little code. The docs for `turbo-rails` are sparse at best, but I recommend keeping the YARD docs handy for reference: https://rubydoc.info/gems/turbo-rails.


_If you liked this post, check out my book, [The Rails and Hotwire Codex](https://railsandhotwirecodex.com), to level-up your Rails and Hotwire skills!_