Durable Objects Finally Make Sense
原定直播时间 10月11日,21:00 - GMT-4 21:30
English
转录文本 (测试版)
Durable objects. What are they? How do they work? And why is everybody talking about them?
And I'll be honest, this one took me a little while to really understand.
Not because durable objects are impossible, but because they're kind of a different paradigm.
So in today's video, we're going to make durable objects click.
We'll start with a bare minimum durable object, a tiny Slack style chat app with persistent state.
And then we'll build on top of it to so it becomes a real -time application.
And don't leave yet just because it's a chat app. I know it's not a super exciting example, but it's not about a chat app.
The point is to understand the durable object pattern.
Let's dive in. This is a deployed app. My name is Ilyas.
Hello. Send. Let me duplicate the step. Abby says, hello. You'll see that it updates here, but not here.
So I have to click refresh for it to work.
Before we look at any code, the first question is, where does that state live?
Now we have 330 data centers in over 120 countries, but let's zoom in. I sent my message here from Austin and the durable object was created in Dallas.
Then Abby sent the message from Amsterdam, reaching the same durable object in Dallas.
By default, a worker does not give you applications that you can rely on. This is an object, but it's not a durable object.
And real applications of course need state.
And yes, you could store data in D1 or you could store data in KV, but real-time applications have another problem.
It's not just where do I store the messages?
It's also who is currently in the room who should receive this new message.
How do we make sure everyone in the room is talking to the same thing? That is where durable objects are different.
A durable object is a stateful serverless function.
It runs code like a worker, but unlike a regular worker, it can also keep durable state.
Each durable object has its own private storage. And the important mental model is that one ID maps to one durable object.
In our app, one room ID maps to one durable object.
Everyone in the general room talks to the same durable object.
Everyone in a different room talks to a different durable object.
Same ID, same durable object. Different ID, different durable object.
Let's see that in code. We'll start with regular.json. The first thing you see here is this migrations entry.
This tells the runtime that we are having a SQLite durable object class and the version is 1.
You only have to do this once when you're creating a durable object or when you're deleting a durable object or renaming a durable object.
Then we have the durable object itself. We define the binding.
We say the class name and the binding name, which in this case is chat underscore room.
Now let's go to the worker. This is the worker handler. When we make a post or a get request, we extract the room ID with a regex.
This is the key part here.
This gets the unique durable object instance. This is the crucial line.
If it's a get request, then we just list all the messages. If it's a post request, then we call room.send message.
These are RPCs, remote procedure calls.
As you can see, this is all there is for the worker. Let's go to this durable object here.
We export a class called chat room and it extends a durable object.
Now every time I create a durable object, we basically have an instance of this class.
The constructor runs when you initiate the class. We have to call this block concurrency while block here to make sure no other methods are called before this is done.
In here, we create a new table. Notice I'm not creating a database. I'm creating a table.
Next, we have send message. Look how simple this method is. We have the created at, then we create a new row.
We just insert into messages and then we return it.
Simple as that. For get messages, we select all four messages and we order by created.
Now, one more thing I have to show you before we move on is this front end, of course.
It's a form here. Then we get the room ID. Otherwise, it's general.
We just render a message HTML. We have this load function, which calls the get messages.
Then we submit the form. We make a post request with the username and the content.
Then we recall load again. So far, we've seen the first superpower of durable objects, how easy it is to store a little piece of state.
But right now, the browser still has to refresh to see new messages.
That means we have durable state, but we do not have live updates.
The next step is to keep clients connected to the durable object.
This is where WebSockets come in. Every client in the same room can connect to the same durable object.
So when one client sends a message, the durable object can store it and then send it out to everyone currently connected.
So there's no Redis PubSub, no separate WebSocket server, and no cluster to manage.
The room already has one object responsible for its state. We're going to let that same object handle the live connections too.
So let's add WebSockets. Let me walk you through the code.
We have to change three things. First, we now have an additional path slash WS for the WebSocket upgrade.
And if it's WebSocket, we forward the request to the durable object.
This is important. We forward the request to the durable object.
And if it's a get request, we get the current messages.
This is on the initial load. Our worker barely changed. We just added the WebSocket support here.
Now what about the durable object? The durable object had a few changes, but not as many as you might expect.
So first we added this set WebSocket out of response thing.
This uses the hibernateable WebSocket API. That means a durable object does not need to stay active in memory forever just because the socket is connected.
And this is big. When nothing is happening, the durable object can hibernate, but the clients stay connected to Cloudflare.
And when a message arrives, the durable object wakes up and handles it.
We have this new method here, fetch, where we create a new WebSocket pair, which has a client and a server.
And this part here is important. We store the server on the context. Next is the most important addition of our WebSockets.
That's the WebSocket message call.
Anytime you receive a message, this will fire, which is great if the durable object is hibernated, because this will wake it up.
We'll get the user and the content from the WebSocket message.
Then we still insert it into our table and we take the payload.
We take our message. And here is the magic part. We just loop over all the WebSockets and send that payload.
So if we have 100 connected clients, we would send out 100 messages.
Isn't that amazing? And then finally, we had to update our front end.
When it first loads, we still make that GET request that we had initially.
Now we create a new WebSocket and we add a listener that said, whenever there's a new message, append it to our messages.
And instead of doing a POST request, we now say WebSocket.send.
Refresh here and refresh here. So here I'll say, Finlay, no way.
Sent instantaneous. And as you can see, live on the Internet.
To add real-time support, we only changed a few things. We added a WebSocket route.
We accepted the WebSocket inside of the durable object. And we changed message sending from HTTP POST to WebSocket.send.
The entire chatroom durable object, including SQLite storage, WebSocket broadcasting, and hibernation support is under 70 lines.
This is why durable objects are more than just storage.
They are stateful serverless functions. They can store state, they can run code, and they can accept WebSockets.
And because requests for the same ID go to the same object, they are a natural fit for real-time and multi-layer systems.
It's not one durable object for the whole app.
That would be the wrong mental model. One room per object, one document per object, one game session per object, or one user session per object.
A million chatrooms can be a million durable object IDs spread across the network.
The thing that needs state gets its own durable object.
Now, this doesn't mean that every app should become a giant durable object. Again, that is the wrong model.
Durable objects scale best when you naturally split the application into many independent objects.
You could split by room, like we did here, or by document.
Because if you put your entire app behind one durable object, you have created one very important bottleneck.
One very busy room is still one very busy object.
So the question is not, how do I put the whole database into a durable object?
The question is, what part of my app needs state, real-time connections, and strong per object consistency?
Also, this demo leaves out some production concerns, right?
In a real app, you would add authentication, validation, rate limiting, and probably a better data model.
But I left those out on purpose.
The goal here is to make the durable object mental model obvious.
Now, you do not need to move your entire application to Cloudflare in order to use durable objects.
You can use them for a specific part of your system, the part that needs durable state, live connections, or per object consistency.
Remember, same ID, same durable object.
Different ID, different durable object. And once you understand that, the WebSocket example and the chat example start to feel simple.
Thanks for watching. I hope that this was useful and that you can now start building with durable objects.
It is an incredible primitive, and there's a reason that D1 and Container and a lot of Cloudflare primitives are built on top of it.
See you next time.
