Talks

Exploring the Power of Turbo Streams and ActionCable

Exploring the Power of Turbo Streams and ActionCable

by Kevin Liebholz

In this session titled 'Exploring the Power of Turbo Streams and ActionCable' presented by Kevin Liebholz at RailsConf 2023, participants embark on a coding journey through the magical realm of Turbo Streams and ActionCable, highlighted through a real-time tic-tac-toe game. The talk creatively intertwines elements from the fantasy series, 'The Inheritance Cycle' by Christopher Paolini, using the characters Eragon and Saphira against the dark king Galbatorix to frame the programming concepts.

Key points discussed during the presentation include:

  • Introduction to Real-time Technologies: The speaker begins by discussing the importance of real-time interactions through web sockets, differentiating it from traditional HTTP requests where the client must request updates from the server. Web sockets maintain an open connection for instant data transmission.

  • Understanding Action Cable: Action Cable is described as the magic of Rails for implementing web sockets smoothly within applications, providing both client-side and server-side frameworks in Ruby.

  • Turbo Streams Overview: Turbo Streams are introduced as a higher-level abstraction built on Action Cable, which allows real-time DOM updates with fewer codes. The talk emphasizes how it simplifies broadcasting messages and rendering updates automatically when server messages are received.

  • Building the Tic-Tac-Toe Game: The practical demonstration starts with creating a skeleton for the tic-tac-toe game, defining models for players and games. Various constraints are added to increase difficulty, demonstrating related Turbo and ActionCable features in action. This also helps visualize how signaling works between the player actions.

  • Implementing Constraints and Notifications: Throughout the game-building process, three main constraints are mentioned:

    • The requirement of exactly two players to start a game.
    • Restricting connections to prevent unauthorized players (like a henchman) from joining the game.
    • Ending the game and notifying players when one leaves. Each of these constraints is implemented with practical coding examples, showcasing broadcasters and connection handling techniques.
  • Conclusion and Acknowledgments: The session wraps up with a triumphant conclusion of the coding project that involves engaging storytelling intertwined with technical learning. Liebholz expresses gratitude to the RailsConf organizers and encourages discussions post-event, highlighting the importance of community in programming.

The main takeaways from the presentation include a deeper understanding of using Turbo Streams and ActionCable for real-time application features, along with an example of how to construct these functionalities systematically. The engaging narrative makes learning these technical concepts more relatable and enjoyable.

00:00:21.480 Here, I want to take you on a journey to a place that some of you may know—a magical country called Alagasia. In this country, a villain rules, spreading fear across the land: a dark King, a dragon rider named Galbatorix, and his dragon, Shrikhan.
00:00:34.620 Fortunately, another dragon hatched and chose her rider, the majestic Saphira, and her rider, Eragon. Together, they sparked a resistance—a glimmer of hope in the darkness.
00:01:00.360 Now, as the two armies face each other, our hero Eragon steps forward with a desire to save all the men on both sides. He challenges Galbatorix to a duel, where the winner takes command of the other side.
00:01:06.479 We all know that dark kings have incredibly big egos, so Galbatorix could not reject the challenge and accepted it. However, there's one issue: Galbatorix is not even on the battlefield; he has barricaded himself in his castle, which is far away across the country.
00:01:24.720 Not even the Dragon Riders' magic can travel that far. But then, we step forward as we are among the few knowledgeable about a magic that is even beyond the magic of the Dragon Riders—a magic so old and powerful that most believe it is forgotten. It goes by many names which people can only whisper: Ruby on Rails, Action Cable, Turbo Streams.
00:01:55.799 We all know this magic is so ancient that no one tries to learn it anymore. However, we are going to take on this enormous responsibility and help Eragon and Saphira fight Galbatorix in an epic game of tic-tac-toe.
00:02:16.860 So, welcome everyone to my talk: Exploring the Power of Turbo Streams and Action Cable! But first, who am I? Who is this storyteller across the campfire—the one that has captured your attention?
00:02:30.239 My name is Kevin Liebholz. I am a business administration graduate turned Rubyist after attending a boot camp. Thank you! I'm from Germany, and I currently work as a software engineer at Portagon, a fintech company in Frankfurt, Germany, where I've been for roughly three years.
00:02:59.220 What we do there is provide simple, reliable, and independent access to the private capital market. Yes, we have a beautiful Rails monolith for that! Additionally, I am one of the co-organizers of the Ruby Meetup in Frankfurt. This is also my very first conference talk ever, so please bear with me—I'm quite nervous.
00:03:40.200 Before we dive back into our story, I want to ask you one question. Just raise your hand if you know the 'Inheritance Cycle' by Christopher Paolini, which my story is based upon. Alright, more than I expected—amazing! For those who do know, it may make a little more sense to you.
00:03:58.799 If you like fantasy and haven't read those books, I definitely recommend them. I am telling this story because I am currently rereading these books, as I never finished them in my teenage years. When I read something, I connect everything with it, so I couldn't resist incorporating this story into my talk.
00:04:29.580 Let’s cast our spell as we explore the power of Turbo Streams and Action Cable. By doing so, I want to prevent you from experiencing what has happened to those who are unaware, which also happened to me during a project at work where we needed customization for Turbo Streams and Action Cable.
00:05:10.620 We couldn't find the advanced examples we needed, so we embarked on a long journey of trial and error and research. The goal for this talk is to provide you with such an example and to spark your imagination for your own use cases of this old magic of Turbo Streams.
00:05:28.500 As anyone knowledgeable about magic knows, we must be careful in how we conduct it; otherwise, the results could be devastating. First, we’ll explore the ingredients of our spell and delve into the theory behind it a bit. We are going to dive into WebSockets, see how Action Cable adds some spice to it, and how Turbo Streams in the end garnishes our spell.
00:05:56.100 Just to reassure those few candidates eager to be included in the magical cycle of Turbo, do not fear—we'll cover the basics so that everyone can follow along.
00:06:03.180 Then we will construct our spell, building the game of tic-tac-toe. We will start with a basic game skeleton, and from there, we'll add three constraints, each uncovering another aspect of Action Cable and Turbo Streams.
00:06:24.240 Last but not least, we will let our magic work and cast our spell, watching the epic game between Eragon, Saphira, and Galbatorix, while reviewing everything we've learned in the talk.
00:06:38.040 Now, let’s start with our three ingredients and explore the theory behind them a bit further.
00:06:47.160 First, let's get everyone on board with what WebSockets are. WebSockets are about real-time communication or at least close to it. It's often easiest to understand this by reminding ourselves how HTTP works.
00:07:07.620 Let’s take a chat application as an example. When we want to receive new messages, we first need to request them from the server. The server then responds by sending those new messages back. However, we can only receive the message if by coincidence, at that moment, our scout on the lookout sends a message to the server before our request is made.
00:07:30.780 With WebSockets, the situation is quite different. Here, we maintain an open connection to the server at all times, meaning the server can send us anything anytime without us needing to request it. As soon as the scout sends a message to the server, it can be directly forwarded to us, ensuring we receive it promptly rather than an hour later.
00:08:06.600 Now, let’s move on to our second ingredient: Action Cable. Coding all the basics of WebSockets may be quite repetitive and may require some protocol-level knowledge, which pairs quite well with a framework. And of course, Rails covers us here.
00:08:31.260 Action Cable is the Rails implementation for WebSockets. It seamlessly integrates WebSockets into every Rails application, allowing us to write real-time features in our beloved Ruby language. It offers a full-stack solution, maintaining the idea that one magician, or one developer, can do it all.
00:09:05.640 For that, it provides us a client-side JavaScript framework and a server-side Ruby framework. But let's keep that for later as we construct our spell.
00:09:36.600 Now, let’s explore the magical world of Hotwire and talk about Turbo Streams broadcasting. As smart magicians, we aim to make our lives easier, and Turbo Streams allow us to use Action Cable much more conveniently.
00:09:59.820 It is essentially an abstraction over Action Cable, which itself is an abstraction over WebSockets. Turbo Stream broadcasting provides simple concerns packaged with the Turbo Rails gem, which is included by default in Rails 7.
00:10:22.680 Turbo Streams give us little helper methods that do the magic in the background. It also adds an initializer for Active Record, allowing us to trigger broadcasting directly from there.
00:10:44.640 In summary, Turbo Stream broadcasting provides us a plug-and-play solution: just install the gem and write about three lines of code, and voilà. It just works! This makes it incredibly easy to use, even for the smallest magicians.
00:11:12.480 But that's not all. It also offers automatic DOM updates, which is essential. Normally, without Turbo, you would receive the WebSocket messages, but you'd still need some client-side JavaScript to do something with them. However, with Turbo, you receive HTML over the wire.
00:11:57.180 In effect, Hotwire and the gem automatically update the DOM with this HTML, and that's a great feature.
00:12:05.760 Alright, we've now finished our list of ingredients.
00:12:11.720 Let's take a short breath and recap what we have. Galbatorix, the dragon, is becoming impatient, so we need to quickly progress to constructing our spell and building the game of tic-tac-toe.
00:12:26.160 First things first, let’s establish what tic-tac-toe is. Tic-tac-toe is a game for two players, beginning with nine empty fields. Players take turns marking these fields: first player one, then player two.
00:12:54.000 Once a field is marked, it cannot be unmarked, and players cannot take a field away from their opponent. When a player manages to get three markers in a row—be it vertically, horizontally, or diagonally—that player emerges victorious.
00:13:20.880 This is essentially the game we aim to build. The significant challenge lies in instant notifications: one player must be notified when the other makes a move. But with our Turbo magic, this will pose no problem.
00:13:47.880 Now, let’s embark on building our skeleton game. Essentially, we require two models: the player and the game. Starting with the player, each player belongs to a game, has a name, and is assigned a character, like a dragon or a sword.
00:14:10.440 We're going to assign that character in a callback. The game model is even simpler, as it merely accommodates many players and stores the values of each of its nine fields. Now, before Galbatorix's impatience escalates further, let’s ensure our game is playable.
00:14:38.640 This should appear like this: Two clients are established on localhost, witnessing the epic game in Alagasia. First, we must add players—as expected, Eragon and Saphira are their priority. They are here, marked with their assigned characters, but they must wait for their opponent.
00:15:00.300 Let’s introduce our notorious latecomer, Galbatorix, the evil king. Now he is here, and it looks like both parties can finally begin playing.
00:15:28.740 Let's proceed as Eragon and Saphira make their first move, followed by Galbatorix. The initial gameplay appears to be functioning smoothly.
00:15:54.300 Let’s summarize what we have observed: there are two clients involved—otherwise, the game isn’t displayable. As soon as Eragon and Saphira made their move, Galbatorix was able to see the effects of that action in real-time, complemented by a few UI changes.
00:16:12.660 This brings us to our first constraint: we require two players to start the game. The second requirement is that one player should instantly see the move made by the other. To accomplish this, we must connect the stream.
00:16:39.540 This means we will link the client to the WebSocket so they can receive any notifications. Then, we’ll need to notify the opponent upon joining. To do this, we can add a simple line of code to the game's show page to connect us to the WebSocket.
00:17:08.799 Let's break this down into parts. First, we use the Turbo Stream helper provided by the gem, which connects us to a stream, named by the next part, which defines the stream name itself. We could also use just 'board' without the 'add player' portion, which would be fine technically.
00:17:27.480 However, including 'add player' makes it unique, allowing us to define the specific client to which we want to broadcast. When we broadcast something, it always goes to all clients connected to the same stream, and 'add player' helps maintain its uniqueness.
00:17:48.600 We can verify if we're connected by checking our Network tab. There, we can see that we are in fact connected, subscribed to a channel, welcomed, and receiving vital pings.
00:18:14.160 Those pings serve as a heartbeat to ensure that the connection remains open. Now that we are connected, let’s proceed to notify our opponent when someone joins, which we will handle through an after-create callback in the player model.
00:18:35.520 Firstly, we need to utilize the 'broadcast update to' method that the gem provides. We’ll define the stream name where we wish to broadcast the message—the opponent's board stream. The front end will reflect this accurately.
00:19:04.260 Next, we need a target, which is the ID of the DOM element we intend to update. By 'update', I mean we will replace the inner HTML of that element. There are additional methods available via the gem, but here, we are focusing on the update functionality.
00:19:15.120 We also define the content we want to use to update it, which is the game board partial. It resembles a simple render method in the controller, making it quite easy to understand. To be complete, we can also pass locals to the partial.
00:19:30.540 Let’s pause for a moment and consider what we just did. We connected a client to the WebSocket, sent a message through that connection, and updated the DOM with minimal code. Isn't that incredible?
00:19:59.400 Now that we have updated the board, it would also be nice to know who we are playing against. We will quickly address this by adding another broadcast update to the method we just established.
00:20:10.980 This time, we will target the opponent's name DOM element with the games opponent name partial. It's feasible to send two broadcasts from the same method, updating two different parts of the DOM.
00:20:45.720 Now, with two players connected, we need to ensure that Eragon and Saphira don’t play blindly. They must be made aware of what Galbatorix is doing, so we need to send them notifications about actions.
00:21:10.920 However, since they are already connected to the stream, there is no need to connect again. We now need to define what occurs when someone makes a move.
00:21:23.880 For that, let's examine one individual field of our board. Essentially, this involves an if-else statement. When the field already has a character assigned, we display that character.
00:21:35.280 If not, we use a simple form that holds hidden attributes and values—these will be necessary later in the controller. We wrap this form in a Turbo Frame, employing another aspect of Turbo magic, as navigations within a frame are scoped to that specific frame.
00:22:03.420 When navigating within the frame, if the response contains a frame with the same ID, the new frame simply replaces the old one. We submit the form, leading us to reach the games controller update method, where we will update the correct field in the game.
00:22:36.180 Once the game updates, we initially want to change our own UI. We achieve that by rendering a field partial back to the place where the submit originated, ensuring that the field now displays the character it has.
00:22:54.360 Because this is wrapped inside a Turbo Frame with the same ID as the existing field, we effectively replace the previous field on the front end with this new character.
00:23:16.440 However, we also need to update our opponent’s UI. Within our game model, we can add an after-update callback that triggers another broadcast.
00:23:40.560 This time, we broadcast a replace to the opponent's board stream, indicating that we will replace the entire DOM element with the same ID as the field—not just its inner HTML but the entire element.
00:23:59.880 This ensures a more pronounced visual cue for the opponent, as we can apply a short red background to indicate that something has just occurred. This sums up how we updated our opponent's UI.
00:24:16.080 Moreover, this broadcasting effectively replaces the field of our opponent. By employing a simple render method, we modified our own UI with the assistance of Turbo Frames. At this point, our basic game is fully operational.
00:24:40.920 However, as clever magicians, we are aware we cannot trust Galbatorix to play fair. Our concern is he may attempt to add a henchman to the game, trying to manipulate it in his favor, so our second constraint is to restrict gameplay to only two players.
00:25:00.480 Now, as we see, we have three clients: Eragon and Saphira, along with Galbatorix. We need to wait for Galbatorix to join the game. Here he is, and let’s attempt to add a henchman to the game, an absurdly named character because dark kings do not care about actual names.
00:25:27.180 As we observed, the third client—the henchman—is not receiving any updates. For that to happen, we need to reject the connection request of this third client, which we will go about by customizing Action Cable's connection.
00:25:49.920 The Action Cable connection acts as a bottleneck for the WebSocket connection. Each connection corresponds to one WebSocket or stream; the Action Cable server can manage multiple client instances, but a single client can only maintain one connection.
00:26:06.480 Think of it as a tunnel—the connection between the client and server. The server sends messages through this tunnel. Thus, we can either disconnect or destroy a connection for a potential third client.
00:26:28.920 For this, we need to transmit pertinent data from the client through the connection to the server. In doing so, we can identify who is attempting to connect. The good news is the old roots of our magic assist us here, as cookies are passed through the connection.
00:26:53.940 Thus, we simply append the player ID to the cookies. Although in a more sophisticated setup, we should utilize an encrypted session cookie to provide better security, we'll keep things simple and bypass any decryption processes for clarity's sake.
00:27:21.180 Once we reach our Action Cable connections, we can utilize this cookie to locate the player and decide whether to permit the connection or reject it.
00:27:38.280 If we opt to reject the connection, we'll witness a swift disconnection without any reconnection attempts, thus prohibiting the connection seamlessly.
00:28:01.920 This is a win, as we effectively prevent Galbatorix from introducing a henchman into the game! But we still suspect Galbatorix might attempt to stage a surprise attack on the resistance by leaving the game.
00:28:24.780 Therefore, our third constraint is that if one player exits the game, we need to conclude the game and inform the opponent.
00:28:47.880 Thus, when both Eragon and Saphira are present, we can add Galbatorix into the mix. As the game starts, Galbatorix can exit, leading to Eragon and Saphira receiving notifications that the game has ended.
00:29:07.200 However, there is another way a player can leave. For example, if they close the tab, we still ensure Eragon and Saphira are notified about the departure.
00:29:36.660 To manage this, we discuss Action Cable channels. Imagine a channel as a chat room; when you’re in a room, you can receive messages sent to it. Although clients have a singular connection to the server, they can subscribe to multiple channels.
00:30:03.060 Channels enable us to define behavior for subscribing and unsubscribing events. Rails has a default application cable channel, and Turbo has a default Turbo Stream channel that we will connect to.
00:30:18.840 These channels have Turbo magic added, including a subscribe callback triggered when a client subscribes. In this callback, we can verify if someone attempted to tamper with the stream's name, allowing us to reject such attempts.
00:30:41.640 However, for educational purposes, we won't explore that angle; we'll create our own game channel to simplify future extensions of our magic.
00:31:05.280 First, we need to adapt the view to connect to our game channel instead of the default channel. Hence, we’ll incorporate a simple hash argument here.
00:31:27.840 Next, we must revise our connection slightly, as we desire to access the player from our channel, so we need to pass it using an identifier. This identifier serves as a unique key for the connection—multiple identifiers can exist, but we will utilize just one.
00:31:50.040 To assign this value to the identifier, we set ‘player’ equal to ‘self’. We now have access to the player from the channel. Now, we need to define our unsubscribe callback, which is a straightforward method to manage when a player disconnects.
00:32:24.120 It indicates the need to broadcast an update to the opponent's board stream informing them of the exit. In the end, we ultimately replace the board element in the DOM.
00:32:48.840 Now that we have checked our three key ingredients and carefully constructed our spell, it is time to unleash our magic and cast our spell.
00:33:12.720 The game commences! We delve into our three components: understanding WebSockets, integrating some abstraction with Rails' Action Cable, and even more abstraction with Turbo Streams for real-time DOM updates.
00:33:44.580 Let’s kick things off! Eragon and Saphira, the heroes, make their entrance and join the game, anticipating the notorious latecomer, Galbatorix.
00:34:15.960 Finally, we can start playing, and we introduce basic broadcasting functionalities into our tic-tac-toe skeleton. We connect Turbo Streams for Action Cable to the front end and incorporate callbacks to notify the opponent upon joining and during actions.
00:34:39.660 But watch out! Galbatorix tries to add a henchman to the game, but that doesn't quite work, as we adapted our Action Cable connection to reject the connection request.
00:34:59.820 In the end, we successfully conclude the game as we customized our Action Cable game channel to broadcast when someone tries to leave.
00:35:21.600 This ultimately leads us to the fate of Alagasia, where victory is attained, and all subjects refuse to obey Galbatorix any longer. We can finally initiate democracy in Alagasia!
00:35:39.780 Before wrapping up, I want to give my heartfelt thanks to everyone who assisted me in putting this talk and presentation together; it truly took a community of people.
00:36:02.640 Special thanks once again to my employer, Portagon, for making this journey from Germany possible, and to the organizers here for crafting such an amazing RailsConf experience for me.
00:36:26.520 If you'd like to connect with me, I have various social media handles listed. If you're interested, there's also a link to the repository below. If you spotted the little bug with the opponent's name, that's where you can open a pull request and help fix that.
00:36:53.880 Feel free to catch me in the hallway, join me for lunch, or if you're staying longer, I might even be doing a road trip to New Orleans this Sunday, so you're welcome to join.
00:37:05.400 Thank you all for taking the time to listen to me for what I think was about 40 minutes. The end!