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.
I think both of these come down to the same core issue: mutating state.
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.
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.
But if you design your application for undo, you isolate the document state from the rest of the application so that the only 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.
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.
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.
https://gameprogrammingpatterns.com/command.html#undo-and-re...
Would love to nerd out on the implementation too at some point if you'd like. :)
Let's imagine that we work together on a design file that contains hundreds of icons. I'm responsible for adjusting the color palette while you're removing the old icons that are not used anymore. If I undo my last color update on all icons, I probably don't want to recreate the icon inadvertently and override the triage you're doing! So IMO, it's more of UX question than a technical question. Is there a better default solution that we can find to improve these edge cases? My initial thought is to have a small "toast" letting the user know that the undo operation couldn't be applied because X does not exist anymore, with a link to the version history panel to allow him to reinsert X if it wants to.
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.
I think reseting the time position would also make sense for video editing.
So basically, it's a way to get back as much as possible in the context you were at the time of the operation that is undone.
Now that I'm thinking about it, when I used to use Autodesk Fusion 360 I think it did this a bit, but until now I thought it was just another bug: they had accidentally added some UI state changes to the undo stack.
Miro (the collaborative whiteboard)/Figjam is another... the boards can grow pretty big in x-y space, and losing your place in space is very easy to do, especially if you ctrl-z a few times and forget what you were even working on. Just knowing that "somewhere on this board, something changed" isn't helpful if you can't see what it was.
Analytics is another such use case, for when you're composing queries or search filters, date ranges, etc. Google Analytics does something similar by giving each set of criteria its own unique URL, so that you can use the back/forward in your browser to go between states. Not exactly the same as an undo/redo button, but pretty much the same idea.
Another example: I work for a solar company, and we're building a web app that equipment installers can use to monitor the performance of rooftop solar arrays they've installed. It tracks, for example, each solar module's power production on a heatmap, arranged in the same layout as on the actual roof. There's a lot of interaction that can happen... selecting a particular unit, choosing a particular timeframe, deciding which metric you want to look at, search terms, table filters, sort order, etc.
None of that needs to be written to our database as actual changes to the layout and device pairings. But it would still be handy handy for users to be able to undo/redo, bookmark, permalink, etc.
I think of it this way: A set of UI states like that, taken as a whole, is in and of itself a form of "data"... it is a human user's carefully framed view of some point in time, and some selection of data, that they have deemed important. Being able to save and retrieve that state in case they make a mistake (cat jumped on the mouse) or want to share it with others ("hey, check out what's happening to this module on X date at 4pm") is helpful, even if it doesn't ever need to be written to the server (since it can be stored as local state and/or URL strings).
We'll be here today to answer any questions you may have. Hope you enjoy the article!
Thanks, Steven
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?
Also, excellent explanation and visualizations :)
Good luck with all of it
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?
I'm not sure this makes sense for other cases besides object deletion.
But I'm still not sure this is the default behavior that we want, even it's only for deletion. I think it's more of a UX problem than an engineering problem as explained here: https://news.ycombinator.com/item?id=31682073
To take another example, for rich text editing, I don't think undoing a formatting operation on text that has been deleted at the same time should reinsert the text.
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)
We would technically be able to go beyond that but will likely require bigger servers. Depends on what you’re trying to build.
Our most advanced Svelte example is this one : https://pixelart.liveblocks.app/
If there is enough demand, we'll make an official wrapper for Vue and Svelte!
If you're looking for WebSocket without all the state synchronization we provide, there are a few well-known providers like Ably or Pusher.