Broadcasting to Resources
👩👩👧👩👩👧👩👩👧👩👩👧👩👩👧👩👩👧👩👩👧👩👩👧👩👩👧👩👩👧
Up until now, we've been broadcasting CableReady operations to Channels using string-based stream identifiers like "sailors". 30 Helens agree: "sailors" will get you to your chosen port of call.
Instead of
stream_from
, we're going to use the stream_for
method:class HelensChannel < ApplicationCable::Channel
def subscribed
stream_for Helen.find(params[:id])
end
end
There is also
stream_or_reject_for
, which is intended for scenarios where you're looking up a record based on params
, as in the example above. If you're dealing with sensitive information, stream_or_reject_for
is a solid practice.On the client, we subscribe to the Channel in the exact same way as you would with
stream_from
:consumer.subscriptions.create(
{
channel: 'HelensChannel',
id: 30
},
{
received (data) { if (data.cableReady) CableReady.perform(data.operations) }
}
)
Now, we're able to broadcast_to the Channel so that anyone currently subscribed to that resource will receive the operations:
helen = Helen.find(30)
cable_ready[HelensChannel].dispatch_event.broadcast_to(helen)
As you can see, we have traded our string-based stream identifiers for constant-based identifiers; specifically, the Channel class constant. This is paired up with the
broadcast_to
method, which requires that you pass a resource to it.This allows us to shift our mental model away from "who are we broadcasting to?" to "what is each individual user interested in?"
stream_from
and broadcast
make it easy to develop reactive interfaces with Rails.stream_for
and broadcast_to
actually unlock new feature and UX design possibilities.React was created to address the technical challenges involved in the syncronization of notifications, likes and comments on different parts of a user's page, in real-time.
When you scroll through your unique newsfeed, everything you see on your screen (or within a few dozen pixels of being on your screen) is a carefully orchestrated close-up magic trick. Our brains are fooled into perceiving that newsfeed as one long, living document.
What's really happening is that each item on your feed has been pre-cached, with a minimum viable DOM structure and just enough meta-data to allow it to subscribe itself to a firehose of scoped updates pertaining to that item. If the item is scrolled off the page far enough or the tab is inactive for more than a minute or three, the subscription is discarded and the item purges itself from the DOM.
With CableReady, it's possible to present your users with composable, reactive interface elements that subscribe to their own real-time event stream with a few dozen lines of code.
This is not hyperbole. Do you remember the first time you saw ActiveRecord working and thought, "I must be missing something, because this is impossible?"
This is the same level of black magic. ⚗️
Setting up this pattern [which Facebook broke the web to implement] with CableReady requires about the same effort as updating a ActiveRecord model attribute with an Ajax fetch call:
- 1.Generate a CRUD resource and matching ActionCable Channel, e.g.
stream_for Helen.find(params[:id])
- 2.Add a Stimulus controller that is also an ActionCable subscription consumer and a CableReady performer to the outermost element of the item partial or ViewComponent
- 3.Set the consumer to subscribe to its own Channel with model id metadata from the rendered markup
- 4.Allow users to add/remove specific resource instances, perhaps via a
has_many
relationship or even an integer array attribute - 5.Everyone who changes attributes of the item (think Likes) triggers a
broadcast_to
that morphs the markup of the resource for anyone who has that instance displayed on their screen
app/channels/helens_channel.rb
class HelensChannel < ApplicationCable::Channel
def subscribed
stream_or_reject_for Helen.find(params[:id])
end
end
app/views/helens/_helen.html.erb
<div data-controller="helen"
data-helen-id-value="<%= helen.id %>"
id="<%= dom_id(helen) %>">I am Helen #<%= helen.id %>.
</div>
app/javascript/controllers/helen_controller.js
import { Controller } from 'stimulus'
import CableReady from 'cable_ready'
export default class extends Controller {
static values = { id: Number }
connect() {
this.channel = this.application.consumer.subscriptions.create(
{
channel: 'HelensChannel',
id: this.idValue
},
{
received (data) { if (data.cableReady) CableReady.perform(data.operations) }
}
)
}
disconnect() {
this.channel.unsubscribe()
}
}
app/models/helen.rb
class Helen < ApplicationRecord
after_update do
cable_ready[HelensChannel].morph(
selector: dom_id(self),
html: render(self)
).broadcast_to(self)
end
end
app/views/helens/_helen.html.erb
<div data-controller="helen"
data-helen-id-value="<%= helen.id %>"
data-reflex="click->Helen#birthday"
id="<%= dom_id(helen) %>">Helen #<%= helen.id %> is <%= helen.age %>.
</div>
app/reflexes/helen_reflex.rb
class HelenReflex < ApplicationReflex
def birthday
Helen.find(element["data-helen-id-value"]).age.increment!
morph :nothing
end
end
With easily implemented, many-to-many reactive morph primatives available, developers can start structuring their interfaces differently while making bolder feature scope decisions.
For example, this pattern blurs the distinction between (and justification for) the traditional separation betwen "index" and "show" views. Instead of demanding page-based navigation into detail views, next-generation Rails interfaces can use an IntersectionObserver and a CSS framework with a responsive grid to just drill into the available data, complete with a slick, faceted search UI mixed in as a concern.
Since all websocket traffic is moved through one Connection and Channel subscription overhead is cheaper than unsorted recycling, it means that developers will be free to build interfaces where it's no more computationally expensive to use components that update themselves - immediately after server-side state changes - than it is to just render static HTML.

Helens
One of the few ways that the future is likely to be similar to the past is that when fundamentally new tools become available, smart young people quickly start building things that didn't and likely couldn't have existed before.
Ironically, Facebook could only make React do all of the real-time magic because they have some of the smartest developers on the planet working on the back-end of their UI.
With CableReady, what Facebook spent tens of millions of dollars engineering not so long ago is available to every Rails developer, for free.
Many of us use the
current_user
pattern so often that we can almost forget that it's a resource. You know what doesn't forget? CableReady.Assuming that you have your Connection class set up to be identified by the current user...
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
reject_unauthorized_connection unless self.current_user = env["warden"].user
end
end
end
... you can run
rails g channel users
to create a UsersChannel.Set up UsersChannel to
stream_for
current_user
:class UsersChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
The cool thing about this is that there's no client code changes necessary. Just let your standard client-side
app/javascript/channels/users_channel.js
connect, and ActionCable will pull in the current_user
reference from your Connection class, no params
required.cable_ready[UsersChannel].text_content().broadcast_to(current_user)
You can broadcast to the
current_user
from anywhere in your app.You can clone and experiment with the "streamfor" sample application that demonstrates using
broadcast_to
to send updates to current_user
.Like broadcast, broadcast_to supports streaming to multiple constant-based identifiers at once, as well as holding back the purging of the queues with
clear: false
. When called without any identifiers, it will broadcast all queues with constant-based stream names.cable_ready[SweetChannel].morph
cable_ready[SourChannel].inner_html
cable_ready.broadcast_to(current_user, SweetChannel, SourChannel, clear: false)
cable_ready.broadcast_to(current_user, SweetChannel, clear: false)
cable_ready.broadcast_to(current_user, SourChannel)
cable_ready.broadcast_to(current_user)
This sent operations to: SweetChannel, SourChannel, SweetChannel, SourChannel, SweetChannel. That last line would have also broadcast SourChannel, but that queue was cleared on line 5.
Broadcasting to multiple resources is a more useful power tool than the equivalent broadcast to multiple people. Many times, a user takes an action that will update elements that represent several resources, located in different parts of the DOM.
For example, hitting "Publish" or flipping a resource from Public to Private with a toggle button could update counts, prompt notifications and change background colors.
broadcast_to
allows the developer to queue up all required interface updates on their respective Channels before delivering them with a single, brutal command.It's a solid practice to obscure potentially sensitive model
id
metadata in your views. For some applications, slugs are a good approach. Other times, Signed Global IDs (aka sgid) are a powerful choice because you cannot reverse engineer the model or id from the resulting string. You can even generate sgids which are use-limited.If you set up your
ApplicationRecord
as we suggested in CableReady Everywhere, you can just use the sgid
method on your model:app/views/helens/_helen.html.erb
<div data-controller="helen"
data-helen-sgid-value="<%= helen.sgid %>"
id="<%= helen.sgid %>">
</div>
Modify your Stimulus controller to process a string-based
sgid
instead of an id
:app/javascript/controllers/helen_controller.js
import { Controller } from 'stimulus'
import CableReady from 'cable_ready'
export default class extends Controller {
static values = { sgid: String }
connect() {
this.subscription = this.application.consumer.subscriptions.create(
{
channel: 'HelensChannel',
sgid: this.sgidValue
},
{
received (data) { if (data.cableReady) CableReady.perform(data.operations) }
}
)
}
disconnect() {
this.subscription.unsubscribe()
}
}
Instead of using
find
, just hand the parameter off to GlobalID::Locator
:app/channels/helens_channel.rb
class HelensChannel < ApplicationCable::Channel
def subscribed
stream_for GlobalID::Locator.locate_signed params[:sgid]
end
end
We will have to provide our own selector string, with a
#
prepended to the sgid
:app/models/helen.rb
class Helen < ApplicationRecord
after_update do
cable_ready[HelensChannel].morph(
selector: "##{self.sgid}",
html: render(self)
).broadcast_to(self)
end
end
If you are using Signed Global IDs to do lookups, use of the
dom_id
helper becomes impossible as it reveals the model and id. Use the sgid
as your id
and you won't compromise the security you get with Signed Global IDs.Building on the "Multiple Identifiers" and "Stream Identifiers with logic" sections on the Stream Identifiers page, it is possible to
stream_for
multiple resources in one Channel, making use of ternary logic operators and any other decision making structure that might be applicable to your application. After all, if you have instantiated a model instance, you've ready used a substantial amount of logic that is hidden away behind syntactic magic.broadcast_to
is designed to enable shared experiences around resources. A resource that doesn't exist yet is fundamentally difficult to collaborate on. Yet, when you create an empty Google Doc and share editing rights, the document already exists in every meaningful way. If we want a similar outcome, we have to find creative ways to operate on resources that aren't persisted and might not pass validations.In many cases, the best solution would be to save the new resource before displaying it to the user(s). You can delete unused stub resources with a recurring cleanup job.
If pre-saving is not feasible for your application, perhaps you could generate a UUID on the client and use that to create a temporarily subscription. If a UUID
param
arrives, establish the subscription and then create the model instance you need. Send the id of that model back to the client:app/channels/helens_channel.rb
class HelensChannel < ApplicationCable::Channel
def subscribed
if params[:id]
stream_for(Helen.find(params[:id]))
else
stream_from(params[:uuid])
helen = Helen.create # sure, why not
ActionCable.server.broadcast(params[:uuid], helen.id)
end
end
end
Seeing that there is no initial
id
value, we create a temporary UUIDv4 for the new resource and send that to the server. When the server sends us an integer back, we can set the idValue
before unsubscribing from the channel and forcing another controller connect
method. After all, it really is just a method:app/javascript/controllers/helen_controller.js
import { Controller } from 'stimulus'
import CableReady from 'cable_ready'
const uuidv4 = () => {
const crypto = window.crypto || window.msCrypto
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
)
}
export default class extends Controller {
static values = { id: Number }
initialize() {
this.uuid = uuidv4()
}
connect() {
this.channel = this.application.consumer.subscriptions.create(
{
channel: 'HelensChannel',
id: this.hasIdValue ? this.idValue : null,
uuid: this.uuid
},
{
received (data) {
if (data.cableReady) CableReady.perform(data.operations)
else {
this.idValue = data
this.channel.unsubscribe()
this.connect()
}
}
}
)
}
disconnect() {
this.channel.unsubscribe()
}
}
This was a pretty wacky example but it's here to get you thinking about how to use the standard ActionCable primatives alongside the abstractions that Stimulus and CableReady make possible.
Anyhow, let's wrap up with a few important details to keep in mind when combining
stream_from
and stream_for
together:cable_ready["helen-fans"]
andcable_ready[HelensChannel]
are separate operation queuesbroadcast
can only work with string-based stream identifiersbroadcast_to
can only work with constant-based stream identifiers- if you use them both at the same time, you might have a brain tumor; good luck 🧠
Last modified 1yr ago