I was re-reading the classic Figma blog on this problem and thought more carefully about this section:

https://www.figma.com/blog/how-figmas-multiplayer-technology-works/#implementing-undo

Everytime I’ve read this in the past, I kind of skipped over this part thinking it was saying something I already understood. Also it's hard to read because the movie loops and you can't pause, so I'll repeat the key point here.

<aside> ⚠️ Important Invariant if the user presses undo n times, then redo n times, and nothing else has happened in between, then the document should end up how it was before pressing undo.

</aside>

Example:

  1. Two users have a Repliear issue open. The priority is 3.
  2. User A changes the prio to 0
  3. User B changes the prio to 5
  4. User A presses undo. The prio should change back to 3.
  5. User A presses redo. The prio should change back to 5. Because that is the state the issue was in before the user A pressed undo! Redo should go back there.

In our current API, at step 5, the prio changes to 0, not 5.

Here’s a table to help keep this straight:

user action state comment
initial cantaloupe
me set blue
collab set sausage
me undo cantaloupe it was the state before the action i’m undoing
collab set burrito
me redo sausage it was the state before undo
me undo burrito it was the state before the action i’m undoing

The Figma blog points out where we went wrong: we can't set the redo function when the undo entry is created. It has to be generated when undo happens.

Thinking more abstractly, if we have a history like:

t0 - t1 - t2

And the user undoes t1, we now have a history like:

t0 - t1 - t2 - t1'