I've implemented undo/redo a number of times. I agree whole-heartedly that the Command pattern is the way to do it. Undo has a reputation for being difficult, but my experience is that it's smooth sailing <i>as long as you build it into the tool on day 1</i>. If the app is architected around undo, it's easy, but trying to retrofit it onto an application later is always a nightmare.<p>This is very similar to the experience of writing a networked multiplayer game. If you build a single-player game and try to bolt multiplayer on later, you're gonna have a bad time. But if you design it for multiplayer initially and treat single-player mode as essentially just a multiplayer game with only one player, it's relatively easy.<p>I think both of these come down to the same core issue: mutating state.<p>When playing a game, or editing a document, you are mutating some state. To support undo, you need to capture all of those mutations so that you can reverse them. To support multiplayer, you need to capture them so that they can be synchronized with the other players.<p>It's trivially easy in most programs to just directly mutate some state by setting fields or by calling methods that do that under the hood. So, if you just start coding, you will end up with mutation happening everywhere. At that point, you have already lost.<p>But if you design your application for undo, you isolate the document state from the rest of the application so that the <i>only</i> way to modify it is by going through the undo/redo mechanism. (In other words, the only way to apply a change is to create a Command object which does it on your behalf.) Likewise, if you design for multiplayer, you'll build a separation between game state and the rest of the application. Then the program has a well-defined interface that can modify the state.<p>Once all mutation goes through a narrow well-defined interface, it's relatively easy to grow the application over time without compromising undo or multiplayer.<p>But if you're adding that afterwards, you have to dig through the program to find every single piece of code that changes some state. It's hell.
I know slightly too much about the problem space to have questions about how it's implemented, but I just want to say this is one of the neatest presentations I've seen on this site. It's a great article on an interesting topic and made 100x better by the visualisations
Nice work! Regarding your question on how to handle undoing a command on a shape that doesn't exist anymore, is there a way we could automatically recreate the shape?
> Depending on the use case, the state of features like user selection, user page selection, user zoom setting, etc. could be included in the undo/redo stack to provide a great experience.<p>Does anyone have an idea what these use cases could be? My mental model for undo/redo is based on productivity applications and I’m at a loss; I’m genuinely curious though since this is something I’ve been implementing.
Light mode for the site please, those of us with Astigmatism can't use it. Supabase launched the same and they eventually had to add it because of demand, not sure why dark mode is so heavily default nowadays. <a href="https://jessicaotis.com/academia/never-use-white-text-on-a-black-background-astygmatism-and-conference-slides/" rel="nofollow">https://jessicaotis.com/academia/never-use-white-text-on-a-b...</a>
As someone who's built this kind of multi-player undo/redo in the past all I can say is this looks amazing and I can't wait to try it in one of my projects - Thanks for building this!
> In a multiplayer command-based undo/redo system, we can also solve [intermediary commands] by pausing and resuming the history stack at the right time.<p>Suppose that instead of managing the overall state of the history stack, you gave each command a unique ID and allowed commands to be updated/overwritten as they develop? Apart from a few bytes of memory overhead per command, what am I missing?
Nice to see someone make a product out of Cloudflare Workers (and Durable Objects for sync?).<p>Also, excellent explanation and visualizations :)<p>Good luck with all of it
For deleted items, could you not store a memo of the object in addition to the user-local changes they did?<p>So if you undo a change to an object that was delete by a different user, you can restore that object first to the last known state, then apply the undo?<p>I'm not sure this makes sense for other cases besides object deletion.
What a fantastic, well-written blog post - well done.<p>Out of curiosity, what's the max # of simultaneous connections per room that LiveBlocks can support? (it's hidden behind the enterprise signup flow today)
Is there a provider that has "websockets all over the world on edge" ? Right off the bat I am not a target customer because I don't use React. Coming from Vue/Svelte