Reference
Operation Algebra and Shared History
Look up syntax, contracts, layouts, algorithms, and exact behavior.
The most important correctness decision in LiveBoard is moving undo/redo out of the client. If every editor kept a private history stack, two users editing the same object could undo different local stories over one shared state. LiveBoard instead treats history as a server-owned sequence of invertible operations serialized by the canvas row.
The key practical detail is that the server derives the inverse from the state it actually locked, not from the state a browser thought it had. Suppose one client changes a rectangle from blue to red while another client is slightly behind. The undo record must restore blue from the authoritative pre-operation state, not from a stale or optimistic client snapshot. This is why durable edits go through a transaction that reads the canvas row, validates the operation, computes the next state, and records both forward and inverse operations together.
- create_shape records newly drawn rectangles, ellipses, lines, and text boxes.
- update_shape records moves, resizes, rotations, text commits, style changes, grouping metadata changes, and endpoint updates for lines.
- update_canvas records canvas-level changes such as backgroundColor from the paint bucket.
- reorder_shape records bring-to-front, bring-forward, send-backward, and send-to-back context menu actions.
- delete_shape records toolbar delete, keyboard delete, and context-menu delete for unlocked shapes.
- batch records multi-shape gestures: multi-selection movement, group transforms, multi-style edits, grouping, ungrouping, and grouped deletes.
type CanvasOperation =
| { id: string; kind: "batch"; ops: CanvasOperation[] }
| { id: string; kind: "create_shape"; shape: Shape }
| { id: string; kind: "update_canvas"; patch: Partial<CanvasState> }
| { id: string; kind: "update_shape"; shapeId: string; patch: Partial<Shape> }
| { id: string; kind: "delete_shape"; shapeId: string }
| { id: string; kind: "reorder_shape"; shapeId: string; toIndex: number };Multi-shape movement, group transforms, folder-independent shape grouping, and shared style edits all become batch operations. The inverse of a batch is the reversed list of child inverses. That preserves atomic user intent: one drag of six objects creates one undo step rather than six unrelated updates. Operation ids also act as an idempotency surface: duplicate durable submissions can return the already recorded revision rather than applying the same mutation twice.
Batches also make the UI feel honest. If a user rotates a selected cluster, the system should not expose the implementation detail that five individual shape records changed. The history entry should say, in effect, "undo that rotation." The server still stores the precise patches needed to reverse the change, but the user-facing unit remains the gesture that created it.
draw shape -> create_shape drag one shape -> preview_op update_shape, then durable update_shape drag group or selection -> preview_op batch, then durable batch edit text content -> local textarea draft, then durable update_shape change style -> update_shape or batch paint background -> update_canvas bring forward/back -> reorder_shape group/ungroup -> batch of groupIds patches undo/redo -> server applies stored inverse/forward op
BEGIN;
SELECT state, revision FROM canvases WHERE id = $1 FOR UPDATE;
-- validate operation and derive inverse from locked state
UPDATE canvases SET state = $next_state, revision = revision + 1 WHERE id = $1;
INSERT INTO canvas_ops(canvas_id, revision, op) VALUES ($1, $next_revision, $op);
INSERT INTO canvas_history(canvas_id, forward_op, inverse_op, applied_revision)
VALUES ($1, $op, $inverse, $next_revision);
COMMIT;