Real-Time Backend

Undo/Redo in collaborative

Ctrl+Z is one of the most-used key combinations. In a collaborative editor, it hides one of the trickiest algorithms: how to undo your own work without touching anyone else's, in a document being edited in parallel.

  • **Google Docs** uses local undo with a captureTimeout of ~1.5s, so words group into units. On undo the document only rolls back the current user's operations even if teammate edits sit between them
  • **Figma** clears the redo stack on incoming remote operations - a trade-off between correctness and simpler implementation. The team consciously chose this simplification
  • **Notion** keeps CRDT tombstone history per page. This allows restoring deleted blocks via 'Page history' even days after deletion
  • **Yjs UndoManager** is the open-source implementation used by Hocuspocus, BlockNote, and Tiptap. It supports undo through remote operations without clearing the stack

Collaborative undo

Ctrl+Z in a single-user editor is trivial: pop the last operation off the stack. In a collaborative editor it becomes non-trivial. Picture this: Alice types a word, Bob deletes the next sentence, Alice hits Ctrl+Z. What should happen? Undo Alice's word - fine. But Bob's operation is already in the shared document history.

The fix is **local undo**: each user only undoes their own operations and doesn't touch anyone else's. This breaks the intuitive 'rewind time' model, but it's the only option that doesn't destroy other people's work.

**captureTimeout** in Yjs UndoManager is the undo-equivalent of debounce: fast typing collapses into one group, otherwise every letter would be its own undo step. Google Docs uses similar logic: words group into one undo unit, and pauses >1.5s start a new group.

Alice and Bob edit a document. Alice types 'Hello', Bob deletes the next paragraph, Alice presses Ctrl+Z. What happens with local undo?

Selective undo

Local undo reverts in LIFO order. Selective undo goes further: you can undo any specific operation from history without touching the later ones. This is needed for example in Figma: 'undo only the color change on this object, keep all other edits'.

Selective undo requires computing the **inverse operation** for the target op with respect to every later op. For text ops this is non-trivial: if you deleted the character at position 5 and then 10 chars were inserted before position 5, the inverse of the delete must insert at position 15, not 5.

**Figma** implements selective undo for object properties: every `setProperty` is a separate operation you can undo independently. This works because object property changes don't depend on each other (no positional shifts like in text). Selective undo for text CRDTs is significantly harder.

Selective undo of an operation from the middle of history requires something to be done with the later operations. What exactly?

History branches

In a single-user editor undo creates linear history: undo -> undo -> redo returns you to the same place. In a collaborative environment history becomes a **tree**: after undo plus new changes, the redo branch becomes unreachable. That's history branching.

The problem goes deeper: two users can hold different local undo stacks. When Alice hits undo, she reverts her own operation, but the document already contains Bob's changes. This produces a **non-linear tree of states**, where each node is a possible document state.

**Git** has a similar problem: after `git rebase`, old commits become unreachable. In collaborative editors, Figma doesn't support redo after remote changes - the redo stack is cleared when a remote operation arrives while the cursor is mid-stack. It's a simplification, but users don't notice.

Alice pressed Undo, then Bob inserted new text (remote operation). Can Alice now press Redo?

Undo in CRDT

Undo in CRDT data structures has its specifics: operations are not just 'reverted', they are **marked deleted** (tombstone). An inserted character doesn't physically disappear from the CRDT on undo - it gets a deleted flag. This ensures convergence: if the deleted character hasn't reached another client yet, that client will process it and also mark it.

**Yjs tombstones** live forever (as long as the document exists) - that's the cost of convergence. Real Notion and Google Docs documents accumulate thousands of tombstone characters. For optimization there's garbage collection: a tombstone can be removed once all clients confirm receipt of that version (all-acked). Yjs supports GC via `ydoc.gc = true`.

Ctrl+Z in a collaborative editor should revert the last change to the document regardless of who made it

Collaborative undo only touches the current user's operations (local undo) - undoing someone else's work without their consent is unacceptable

Global undo in a collaborative environment is destructive: Bob spent an hour on edits, Alice accidentally hits Ctrl+Z, and everything is gone. Local undo is the only model that respects everyone's work. That's why Google Docs, Notion, and Figma implement local undo.

Why is undo of an insert in CRDT done via tombstone rather than physical removal?

Key takeaways

  • Local undo is the only correct model: each user undoes only their own operations, others are untouched
  • Selective undo requires transforming the inverse against every later operation - same math as OT
  • CRDT undo uses tombstones instead of physical removal: convergence matters more than compactness

Related topics

Undo in a collaborative environment is tightly tied to the base consistency algorithms:

  • Operational Transformation — Selective undo needs the same transform functions OT uses to converge concurrent operations
  • CRDT tombstones — The physical basis for undo in CRDT: deleted items are kept as tombstones to maintain eventual consistency
  • Conflict resolution — Undo creates a new class of conflicts. The next lesson covers strategies for resolving them

Вопросы для размышления

  • What should Undo do if the user deleted a paragraph and a teammate edited it in the meantime? On undo, do you restore the original paragraph or the teammate-edited version?
  • Why does Google Docs limit undo history depth? What technical and UX reasons drive that decision?
  • CRDT tombstones grow forever. How should garbage collection decide when a tombstone can safely be removed?

Связанные уроки

  • rt-52 — Awareness and selection sync set the stage for understanding why undo must be per-user
  • rt-54 — Undo creates a new class of conflicts that the next lesson addresses through resolution strategies
  • net-01-intro
Undo/Redo in collaborative

0

1

Sign In