“Once upon a real-time…”—that could be an interesting beginning for an IT fairy tale full of heroes and villains, magic and stience. But… We’re a newsletter, so let’s keep on sharing news!
Highlights
We are introducing AnyCable+, a Software-as-a-Quick-Service product (yes, SaaQS) that helps you to get started with reliable real-time features with just a few clicks. (No worries! AnyCable remains open-source in its core, so we expect you to run it on premise as soon as you hit higher loads; but while you’re working hard on reaching this goal, we’d like you to worry less about managing a real-time server.)
Free for early users. We hope you'll try it out and share your feedback (you can always reply to this email to reach us).
ONCE Campfire is publicly available 🔥
The first ONCE project has been released and it’s a real-time chat application, Campfire. The ONCE model implies a one-time payment and on-premise installation. But what’s more important for our newsletter, you get access to the codebase and are free to learn from it or modify it to fit your needs.
For example, you can try replacing Campfire’s Action Cable with AnyCable and see if it boosts the performance (spoiler: it does). Just like I did 🙂.
News
A huge initiative on bringing Action Cable to the next level has just kicked off with this pull request. The initial PR is mostly code reorganization (housekeeping) but the number of possibilities it opens is amazing. You can learn more from the document.
Releases
Turbo 8 reached its third release candidate and the final release is just around the corner. We’ve talked a lot about its features in the previous issues, so we’re not going to repeat ourselves. Remember: morph responsibly!
AnyCable real-time starter kit
We launched the official Vercel template to deploy a Next.js application powered by AnyCable. Combine it with AnyCable+, and you have a full-featured real-time application in minutes for free!
Frame of curiosity: channels vs. streams
One of the distinguishing features of Action Cable (and AnyCable) is encapsulation of real-time interactions into channels. The actual pub/sub is hidden from the client. Let me demonstrate what I mean by comparing two Rails chat implementations: one using Action Cable and another one using Pusher (well, it’s still popular 🤷♂️).
Here is the Action Cable version (showing only the relevant parts):
# A Stimulus controller used to subscribe to updates
export default class extends Controller {
connect() {
const channel = "ChatChannel";
const id = this.data.get("id");
this.channel = cable.subscriptions.create(
{ channel, id },
{
received: (data) => {
this.handleMessage(data);
},
}
);
}
handleMessage(msg) { ... }
}
# A server-side channel
class ChatChannel < ApplicationCable::Channel
def subscribed
room = user.chat_rooms.find_by(id: params[:id])
return reject unless room
stream_for room
end
end
# An example of publishing an event
class MessagesController < ApplicationController
def create
message = @room.messages.create!(message_params)
ChatChannel.broadcast_to(@room, message.as_json)
end
end
And here is the same functionality backed by Pusher:
export default class extends Controller {
connect() {
const id = this.data.get("id");
this.channel = pusher.subscribe(`chat:${id}`);
this.channel.bind("message", this.handleMessage.bind(this));
}
handleMessage(msg) { ... }
}
class MessagesController < ApplicationController
def create
message = @room.messages.create!(message_params)
pusher.trigger("chat:#{@room.id}", "message", message.as_json)
end
end
The Pusher version is more concise and, frankly speaking, easier to digest: it’s clear what happens at both ends of this feature. We manage pub/sub channel directly, i.e., we subscribe to the same string identifier as we use to publish updates. There is nothing in between.
This communication design is used by most real-time frameworks. Why is Action Cable designed differently? I don’t know (I’m not the author), but I can see some benefits in using channels, especially in Ruby on Rails. They’re all hidden in the differences.
In Action Cable, we have to define a channel class that acts as a middleman between a client and a server. What may seem like an unnecessary boilerplate is actually a great way to organize logic around the communication channel. If you go beyond a demo application, you quickly realize that you need to deal with security (authentication, authorization) and maintainability concerns.
Let’s consider stream authorization. How would you implement it with a channel-less pub/sub? For example, Pusher asks a subscription permission from your application over HTTP. And then you need to write something like this:
class PusherAuthController < ApplicationController
before_action :authenticate_user!
def auth
channel_name = params[:channel_name]
if current_user_has_access?(channel_name)
response = Pusher.authenticate(params[:channel_name], params[:socket_id])
render json: response
else
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
private
def current_user_has_access?(channel_name)
case channel_name
in /chat:(?<id>\d+)/ if $~ in {id:}
ChatRoom.find(id).members.where(id: current_user.id).exists?
in /some_other_channel/
# ...
end
end
end
Oops! That’s a lot of code (we had a single line for that in our channel class). Furthermore, imagine having a dozen of different streams with their specific authorization logic. You’ll probably end up with a custom abstraction to keep pub/sub authorization code maintainable. Maybe, even channels 🙂 (That’s how Laravel Echo and Elixir Phoenix work, for example).
Pub/sub encapsulation also makes it dead simple to subscribe a client to multiple data streams within the same subscription without even letting the client know about this. Here is an example:
class ChatChannel < ApplicationCable::Channel
def subscribed
room = user.chat_rooms.find_by(id: params[:id])
return reject unless room
stream_for room
# Additional stream for moderator updates
stream_for([room, :moderators]) if room.moderator?(user)
end
end
Note that we don’t use string stream identifiers but pass Rails models instead. This is truly Rails way of dealing with the naming and consistency problems! Don’t argue which delimiter to use, “:” or “/”, just use what Rails suggests! That reduces the conceptual complexity of your codebase.
The last but not the least reason to choose channels over bare pub/sub is their RPC capabilities. With channels, handling client messages is straightforward; you have the channel and connection context and scope; and you have a file in your codebase where to put the logic!
So, encapsulation rocks. But what about Hotwire? Doesn’t it call into question everything I said above? Yes, it does. Hotwire demonstrates how to take another step further away from implementation towards abstractions. No more channels, no more JS. Now we have a pub/sub wrapped into Action Cable channels!
Still, even with Hotwire, if you want to implement a more sophisticated real-time logic, you can go with custom Action Cable channels (see examples in this talk). But that’s rarely needed. Authorization is covered by signed streams, no need for multiple streams and RPC.
That makes me think that having a generic secure pub/sub over Action Cable similar to Hotwire would be a nice addition to the framework. What do you think?