From this point forward, many code samples will show CableReady operation methods being used without any parameters. This is intended as a visual shorthand which can simplify examples and keep the reader in the flow of the concept being explained.
In practice, all CableReady operations require at least one method to be used properly.
When you add an operation, you're adding an entry to the array (a FIFO queue) of operations for a given stream identifier:
cable_ready["visitors"].morph
If your app has multiple stream identifiers, it means that you have two different queues that you can add to:
cable_ready["sailors"].inner_htmlcable_ready["visitors"].set_style
Now, you have "visitors" with two operations, and "sailors" with one, both waiting patiently to be broadcast:
cable_ready.broadcast
Great: the operations in both queues have been delivered, and they are empty again. Each queue is sent to the client with its own broadcast, but each broadcast can contain many operations from the same queue.
Every CableReady method chain starts with a declared identifer, by way of the []
characters that suffix the cable_ready
method.
What if you'd wanted to send all of those operations multiple times, though? You can, if you pass clear: false
as the last parameter to broadcast
:
cable_ready["sailors"].console_logcable_ready["visitors"].insert_adjacent_textcable_ready.broadcast(clear: false)cable_ready.broadcast(clear: false)cable_ready["sailors"].dispatch_eventcable_ready.broadcast
Congrats, you have just sent six separate broadcasts; three for each queue. And on the very last pair of broadcasts, the "sailors" queue contained two operations - console_log
and dispatch_event
- instead of just console_log
.
What if you have a whole bunch of queues for different streams on the go, and you don't want to broadcast them all at once? What if you just want to broadcast to "sailors"?
cable_ready["sailors"].push_statecable_ready["visitors"].set_style # still pending after broadcastcable_ready["chewies"].remove_storage_item # still pending after broadcastcable_ready.broadcast("sailors")
The "sailors" queue is now empty, while the "visitors" and "chewies" queues are still waiting, and still accumulating new operations.
Let's build a slightly more involved - but no less silly - example:
cable_ready["sailors"].text_contentcable_ready["visitors"].set_focuscable_ready["chewies"].remove_css_classcable_ready.broadcast("visitors", "chewies", clear: false) # visitors, chewiescable_ready.broadcast("chewies", clear: false) # chewiescable_ready.broadcast("visitors") # visitorscable_ready.broadcast # sailors, chewies
This resulted in six broadcasts, total: "visitors", "chewies", "chewies", "visitors", "sailors", "chewies".
If you put your broadcast call on the end of a method chain that has already specified an identifier, you cannot modify the identifier further in your broadcast
call:
cable_ready["chewies"].set_cookie.broadcast("sailors") # ERROR! identifier is already "chewies"
It's possible to configure your Channel to "stream from" multiple stream identifiers at once:
class ChewiesChannel < ApplicationCable::Channeldef subscribedstream_from "mike"stream_from "ike"endend
From the client subscriber's perspective, this makes no difference.
On the server, it means that this Channel has two entries in the ActionCable "routing table". Both of the following broadcasts will go to the same subset of users:
cable_ready["mike"].morph.broadcastcable_ready["ike"].morph.broadcast
Each stream identifier has its own operations queue. This means that you could build up two queues of different operations, both intended for the same recipient(s) but broadcasting at different times.
Until now, we've been working with Channels that have "glob" identifiers. Everyone subscribing to the Channel can be reached by broadcasting to it's identifier. Time to level up!
The argument to stream_from
is [just] a string, which means that we can construct all manner of dynamic identifiers based on information available to us from the Channel and Connection, as well as a params
hash that comes from the client when the Channel subscription is received.
The params
you get from an ActionCable Channel subscription request is conceptually similar to what you get from a typical ActionDispatch controller request. They do not go through the Rails router, however, and they are only for Channel subscriptions.
Consider this ApplicationCable
definition, which supports Devise authentication but falls back on request.session.id
so that nobody is turned away:
app/channels/application_cable/connection.rbmodule ApplicationCableclass Connection < ActionCable::Connection::Baseidentified_by :current_useridentified_by :session_iddef connectself.current_user = env["warden"].userself.session_id = request.session.idreject_unauthorized_connection unless self.current_user || self.session_idendendend
In the above scenario, you might consider forcing a reconnect when the user successfully logs into their account so that the Connection correctly ties their actions to the correct account.
We now have a number of options for crafting our stream_from
string.
We can interrogate the Connection to see what Connection identifiers are available to us for some exciting meta-programming possibilities:
connection.identifiers=> #<Set: {:current_user, :session_id}>
Don't shoot the messenger: ActionCable has "Connection identifiers" (in this case, :current_user
and :session_id
) which refer to the objects defined in connection.rb
using identified_by
directives AND Channel stream identifiers, which are the mailboxes/routing channels we broadcast to with CableReady (e.g. "sailors"). 🤦♀️
Two Connection identifiers means two accessors available to us: session_id
and current_user
:
session_id=> "377f97791adae1f36be2a106498d8401"current_user.login=> "leastbad"
This means that you can set up your Channel stream identifier to support broadcasting to any registered user, assuming that you know their user_id (and they are online at the time):
class SailorChannel < ApplicationCable::Channeldef subscribedstream_from "sailor:#{current_user.id}"endend
Perhaps we want to be able to broadcast to everyone currently looking at the site ("visitors"), people who haven't yet signed up ("landlubbers") and, of course, "sailors". We can accomplish this by making a decision based on whether there is a current_user
in scope:
app/channels/sailor_channel.rbclass SailorChannel < ApplicationCable::Channeldef subscribedstream_from current_user ? "sailors" : "landlubbers"endend
ActionCable can later map between ExampleChannel ("visitors") and SailorChannel ("sailors" and "landlubbers") because an identifier can only be attached to the first Channel that uses it.
Let's imagine for a moment that in your new application, authenticated users are given a salty sailor nickname, which is stored in a meta
tag with the name nickname
. Anonymous visitors to the site have not yet had an opportunity to be given a salty sailor nickname.
You can clone a copy of this token authentication application and see a great example of how passing params works. A JWT token is created, stored in a meta
tag in the head
, then passed to the Channel subscription as a 2nd parameter.
We've already seen that the subscription creation method accepts a string like "ExampleChannel". Behind the scenes, that string is converted into an object:
{channel: "ExampleChannel"}
If you pass an object, it's assumed that one of the keys will be channel
and the value will be the name of the channel. An arbitrary number of additional key/value pairs can also be passed, and that's how we tell the server about our nickname (which will be blank if it hasn't been set).
consumer.subscriptions.create({channel: 'SailorChannel',nickname: document.querySelector('meta[name=nickname]').content},{connected () {},rejected () {},received (data) { if (data.cableReady) CableReady.perform(data.operations) }})
In our Channel class, we can now treat the object passed as a params
hash:
app/channels/sailor_channel.rbclass SailorChannel < ApplicationCable::Channeldef subscribedstream_from "sailor:#{params[:nickname]}"endend
In conclusion, ActionCable gives you the ability to create stream identifiers for one user, all users, and any ad hoc group in between. So long as the composition has a predictable structure, you have total control over who gets which broadcasts, under which circumstances.
But what if you don't want to broadcast operations to people? What if you want to broadcast operations to concepts and ideas? To things?
To resources? Read on...