[HN Gopher] How to build undo/redo in a multiplayer environment ...
       ___________________________________________________________________
        
       How to build undo/redo in a multiplayer environment by Liveblocks
        
       Author : moritzplassnig
       Score  : 149 points
       Date   : 2022-06-09 13:27 UTC (9 hours ago)
        
 (HTM) web link (liveblocks.io)
 (TXT) w3m dump (liveblocks.io)
        
       | upupandup wrote:
       | 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
        
         | guillaumesalles wrote:
         | Our code client does not depend on any front-end technology, so
         | you can use it with Svelte or Vue. We have a few examples that
         | use Vue and Svelte here:
         | https://github.com/liveblocks/liveblocks/tree/main/examples
         | 
         | 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.
        
           | upupandup wrote:
           | love it! thanks for the response. So the state synchro is not
           | possible in Ably? Trying to understand what liveblocks does
           | on top of what Ably/Pusher provides
        
             | guillaumesalles wrote:
             | To summarize the differences between Ably/Pusher and
             | Liveblocks, Ably/Pusher use a centralized Redis to
             | broadcast messages to channels. Liveblocks has tiny
             | isolated servers on the edge for every room. These two
             | different architectures create the following trade-offs:
             | 
             | - Ably/Pusher are lower level than Liveblocks. It doesn't
             | have any storage associated to a channel and does not solve
             | any conflicts.
             | 
             | - Liveblocks provides API to migrate existing app into
             | collaborative ones via integration with state management
             | library like Redux/Zustand. I have a POC somewhere that try
             | integrate with Vuex, would love to release that at some
             | point!
             | 
             | - Ably/Pusher charges per WebSocket message sent.
             | Liveblocks charges per WebSocket connection. Because of
             | that, building features like cursors can become quite
             | expensive on Pusher.
             | 
             | - Ably/Pusher lets you connect to multiple channels with a
             | single WebSocket connection (because they're using a
             | centralized Redis IIRC). Liveblocks requires a single
             | WebSocket connection per room. Pusher is good for
             | notifications systems, Liveblocks shines when you need to
             | build an app like Figma or Google spreadsheet.
             | 
             | Hope it helps!
        
         | jkarneges wrote:
         | Fastly announced Fanout today, which is exactly this
         | https://www.fastly.com/blog/unlocking-real-time-at-the-edge
        
       | kbyatnal wrote:
       | What a fantastic, well-written blog post - well done.
       | 
       | 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)
        
         | stevenfabre wrote:
         | We support 20 simultaneous connections per room on the Pro
         | plan. With the organization plan, we've been able to increase
         | this to about 50 simultaneous connections per room depending on
         | the use case.
         | 
         | We would technically be able to go beyond that but will likely
         | require bigger servers. Depends on what you're trying to build.
        
       | stevenfabre wrote:
       | Hi HN,
       | 
       | We'll be here today to answer any questions you may have. Hope
       | you enjoy the article!
       | 
       | Thanks, Steven
        
         | guillaumesalles wrote:
         | I worked on the implementation so don't hesitate if you have
         | deeper technical questions that the article is not covering :)
        
       | floflow wrote:
       | this is great!
        
       | munificent wrote:
       | 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 _as long as you build it into the tool on day 1_.
       | If the app is architected around undo, it 's easy, but trying to
       | retrofit it onto an application later is always a nightmare.
       | 
       | 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.
        
         | kupopuffs wrote:
         | This is some great nugget of knowledge, thanks for sharing.
         | it's too bad all my programming enthusiasm is going into a 9-5
        
       | tmikaeld wrote:
       | Nice to see someone make a product out of Cloudflare Workers (and
       | Durable Objects for sync?).
       | 
       | Also, excellent explanation and visualizations :)
       | 
       | Good luck with all of it
        
         | guillaumesalles wrote:
         | Yes! We're using Durable Object under the hood :) Thanks to you
         | for providing such a great platform!
        
           | tmikaeld wrote:
           | Oh, I dont work at cloudflare I'm just a user, have been
           | building workers for about 3 years.
        
       | mfester wrote:
       | 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?
        
         | guillaumesalles wrote:
         | It's technically possible by caching only deleted shapes if any
         | operations in the undo stack are associated with them. But is
         | it really the UX we want every time it's happening?
         | 
         | 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.
        
       | cyral wrote:
       | Love this style of interactive posts. I recently had to solve the
       | same problem for my own app and did it pretty similarly.
        
         | stevenfabre wrote:
         | Thank you!
        
       | vinodsantharam wrote:
        
       | maccard wrote:
       | 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
        
         | stevenfabre wrote:
         | Thank you so much!
         | 
         | Would love to nerd out on the implementation too at some point
         | if you'd like. :)
        
       | yodon wrote:
       | 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!
        
         | stevenfabre wrote:
         | Thank you! Can't wait to see what you build with it.
        
       | felix-martinez wrote:
       | this is amazing!!! thank you for sharing
        
       | schneegansmarie wrote:
       | Love the interactive visuals. Would be awesome to see how that
       | would work with other use cases at some point. Is that something
       | you're planning to do?
        
         | guillaumesalles wrote:
         | By "other use cases", are you talking about apps that are not
         | design tools like Notion or Google Spreadsheet? Or other edge
         | cases related to undo/redo?
        
           | schneegansmarie wrote:
           | Yes, mainly text collaboration.
        
       | Xeoncross wrote:
       | For deleted items, could you not store a memo of the object in
       | addition to the user-local changes they did?
       | 
       | 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.
        
         | guillaumesalles wrote:
         | Yes that's good way to implement it!
         | 
         | 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.
        
       | aranchelk wrote:
       | > 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.
       | 
       | 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.
        
         | hamtr wrote:
         | In design tools, like Figma, undo sometimes just walks
         | backwards through your selection (even if you didn't change
         | anything). It'll even switch between pages. I'm interesting in
         | seeing if there are other ones people can bring up
        
           | aranchelk wrote:
           | Interesting, that's a really good one, thank you.
           | 
           | 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.
        
           | guillaumesalles wrote:
           | For rich text editing, undo should probably reset the scroll
           | position and the cursor close the edit you're undoing.
           | 
           | 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.
        
             | aranchelk wrote:
             | Thank you, this is both a good and nuanced example.
             | 
             | It would likely be a better UX if that scrolling (etc)
             | brought the user back to the exact context they made that
             | edit, but in the interest of laziness and impatience I'm
             | just using a generic focus function.
             | 
             | It might be nice to have more exact context data available
             | for the life of a session as a progressive enhancement,
             | though I think it would have to be a clever encoding, else
             | that data would be invalid in cases such as window resizes
             | and edits made by collaborators that change position of the
             | object being edited.
        
         | [deleted]
        
       | pierrelv wrote:
       | Great content. The interactive visuals are really cool!
        
       ___________________________________________________________________
       (page generated 2022-06-09 23:01 UTC)