MishkaCmsCore.Runtime.PageBuilder.Tree.Patch (Mishka CMS Core v0.0.1)

Copy Markdown View Source

What you apply to one version of a document to get an earlier one back.

Values, not operations

A patch holds the nodes as they actually were — not an instruction for undoing what happened. That distinction is the whole reason this is safe:

  • nothing has to be invertible. "Delete this section", "replace the whole document", "paste forty elements" all reverse the same way, because reversing them is just putting the old nodes back.
  • no operation is ever re-run. Re-running is how a replay scheme mints fresh node ids and quietly detaches every id the session was holding.

Why the editor stores these instead of documents

An undo entry used to hold a whole tree. In memory that is nearly free — fifty versions of a document share almost every node — but ETS flattens what it stores, so fifty entries landed as fifty independent copies. Measured on a 5,001-node document: 1.2 MB per entry, and the fifty the editor kept came to 57.5 MB for one person's session.

The same edit as a patch is 290 bytes, whatever the document's size, because a patch is proportional to what CHANGED rather than to what exists. Measured against whole trees: 84× smaller on a 101-node document, 1,041× on 1,251 nodes, 4,160× on 5,001. Building one is also cheaper than flattening a tree was — 3 ms against 5.7 ms on that largest document — because comparing two versions that share their nodes is pointer comparison, not a walk.

That is what makes the depth of the history a question about memory rather than an arbitrary count.

Reading one back

A patch is relative: it reverses exactly one step, from the version that came after it. So getting to a version several steps away means applying each patch between here and there, which MishkaCmsCore.Runtime.PageBuilder.EditorSession does because it is the thing that knows the order. Measured at 79 µs to walk back a hundred of them.

Summary

Functions

Applies a patch, giving back the document it was built from.

The patch that turns during back into before.

Roughly what this patch costs to keep, in bytes.

How many nodes this patch carries — what the history's memory budget is spent on.

Types

t()

@type t() :: %MishkaCmsCore.Runtime.PageBuilder.Tree.Patch{
  drop: [String.t()],
  restore: %{
    required(String.t()) => MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()
  },
  roots: [String.t()]
}

Functions

apply_to(tree, patch)

Applies a patch, giving back the document it was built from.

build(before, during)

The patch that turns during back into before.

restore carries every node that differs, as it was; drop names the nodes that did not exist yet; roots is the root list, which a move or a delete at the top level changes on its own.

Compared by identity first (!==), so two versions that share a node — which is nearly all of them — cost a pointer comparison rather than a structural one.

bytes(patch)

@spec bytes(t()) :: non_neg_integer()

Roughly what this patch costs to keep, in bytes.

weight(patch)

@spec weight(t()) :: non_neg_integer()

How many nodes this patch carries — what the history's memory budget is spent on.