Step 7 - Undo/redo

1 min read

Implement undo/redo as operation stacks, designed to stay correct on a shared realtime board.

type UndoOp = | { kind: 'add'; el: BoardElement } // I added el -> inverse deletes it | { kind: 'update'; before: BoardElement } // I changed it -> inverse restores 'before' | { kind: 'delete'; el: BoardElement }; // I deleted el -> inverse re-adds it

  • Two signal stacks of UndoOp[][] (undoStack, redoStack), capped at 100 entries.
  • Every user gesture produces ONE entry: wrap multi-op gestures with beginUndoBatch()/endUndoBatch() (a whole eraser drag, a multi-element move, a paste - each is one entry).
  • applyHistoryEntry(entry): replay ops newest-first, apply each through addEl/updateEl/deleteEl, and RETURN the inverse entry; undo pushes that inverse onto redoStack, redo pushes onto undoStack - one routine serves both directions.
  • Concurrency guard: before replaying an op, check the element still exists (for update/delete) or still does not exist (for add). If a collaborator deleted or restored it in the meantime, skip that op silently - never resurrect someone else's deletion.
  • Any new user action clears the redo stack. History is local to this client only - never synced.

Wire Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z (plus Ctrl+Y), suppressed while a text editor is focused.

Add vitest specs for the inverse-entry round trip and the skip-if-missing guard.

Acceptance: create/move/resize/delete all undo and redo cleanly, a multi-select drag undoes as one step, and undoing an op on an element that was deleted from another tab does not recreate it.

Discussion

0 comments

No comments yet. Start the discussion.