To TypeScript or not to TypeScript? Ok, no drama in this newsletter, just news, links, and frames of thought.
Highlights
AnyCable ❤️ Server-Sent Events
The just published released of AnyCable introduces SSE support. Now you can connect to your channels and receive updates without relying on client libraries. EventSource, cURL, any other HTTP client—you can use them all!
With SSE support, you can use Hotwire Turbo Streams (oops, we’re talking about Turbo) without relying on Action Cable or AnyCable JS:
<turbo-stream-source src="https://cable.example.com/events?turbo_signed_stream_name=xyz" />
News
Layered Design for Ruby on Rails applications
Although only a tiny portion of this book relates to cables, I still want to share my excitement of it being released 🎉 Oh, and a discount code for the paperback version: 20RAILS (valid from Sep, 9th to Oct, 10th only on amazon.com).
Releases
This is not the latest release, but we won’t to mention it specifically since it introduces an important feature—PONGs support for better stale clients detection. There are other ping-pong-related improvements, too.
This version comes with better support for AnyCable reliable streams and the PONG feature described above.
New APIs added to better emulate scenarios when multiple subscriptions are created at once. Why is this important? Continue reading!
Frame of curiosity: Too many streams?
Recently, I stumbled across a question in the Hotwire forum asking how many Turbo Streams on a single page are too many. This is a good question, and to answer it, we need to step into the darkness of Action Cable (or AnyCable) internals.
However, I’m not going very deep today—let me leave something for my upcoming RailsWorld talk 🙂. Instead, I'd like to demonstrate how to reason about this problem by stating questions and providing short answers. Let’s start.
What is a stream? It’s a named communication channel: servers can send data over it, and clients can receive it. Streams provide connected delivery semantics: only currently subscribed clients receive messages. It’s similar to radio: you tune to a specific frequency (channel) to listen to your favorite music.
Where do streams live, in a client or a server? Servers manage streams and subscriber mappings. Clients don’t know about streams.
Wait, what? But clients have subscriptions (or channels) to subscribe to streams; aren’t they the same? Not really. In Action Cable, channels are pure abstractions. Even though your client subscribes to a channel, it doesn’t mean you subscribe to some stream. Creating a subscription means creating a pair of abstract objects connected (one on the client side and another one on the server side). A stream subscription may (or may not) be created by the server-side channel object (although the client never knows about it).
When you say “abstract”, do you mean they’re cheap, and we can have tons of them? Kind of. On the client side, every subscription is backed by a JS object (or a few of them, maybe a dozen), nothing else. They re-use the same WebSocket connection (N.B.: we’re talking only about WebSockets right now); this feature is called multiplexing. On the server side, when using Action Cable, subscriptions are backed by Ruby objects. These are long-lived objects (they reside in memory for the whole lifetime of the connection). Keeping tons of objects can lead to noticeable increases in memory usage (you probably know about that since you’re aware of AnyCable and its benefits). A while ago, I shared some thoughts (and numbers) on this problem. With AnyCable, the overhead of having many channels per connection is hardly noticeable since we store only the minimal required channel information in the connection state (and Ruby objects are short-lived).
Got it; server-side memory usage may be affected. What if every channel subscribes a client to a stream (that’s what Turbo::StreamsChannel does)? It depends on how the server manages stream mappings. In a simplified form, adding a new subscriber to a stream is similar to adding a new element to a list. However, some implementations may have additional logic. For example, when using Redis for multi-node transmissions, we also create Redis subscriptions (calling SUBSCRIBE). We do that once per server per stream name, not per subscribed client. So, that shouldn’t bother you.
Sounds like I shouldn’t worry too much about the number of subscriptions per client, right? Not exactly. Even though keeping subscriptions isn’t too expensive, creating them can be. Imagine you have a page with a dozen <turbo-stream-source> tags. On the page load, an Action Cable client will invoke a dozen subscribe commands simultaneously. Now imagine the thundering herd situation when thousands of clients try to load this page (e.g., during the WebSocket server restart)—your server will suffer trying to process such a massive amount of incoming requests. Things can go terribly when server is confirming subscriptions too slow, so clients re-issue subscribe commands—💥
🙀…