MishkaCmsCore.Runtime.PageBuilder.EditorSession.Journal (Mishka CMS Core v0.0.1)

Copy Markdown View Source

What happened to the document, in the words a person would use, and the stack that lets them go back.

Three modules share the editing record and each owns one part of it. EditorHistoryState keeps the DOCUMENTS — the way back — off the session row, because ETS flattens what it stores and entries that share nearly every node in memory would become independent copies. History writes the durable rows anyone can read later. This one owns the ENTRY: what it says, when two of them are really one gesture, and where it sits on the undo stack.

An entry carries a reference, never a document

Every entry holds a ref into EditorHistoryState and its own id, minted here rather than by the database — the flush is batched and retried, so a row has to be identifiable before it exists or a timeout that actually succeeded grows a duplicate on the retry.

The ref is handed in rather than made here. A change to a session can be worked out more than once when another write lands first, and every function below writes its step under the reference its caller reserved before the first try — so a second try replaces the row the first one wrote instead of leaving it behind.

One gesture is one change

Two adjacent entries merge when they are the same kind of edit, to the same node, to the same FIELD of it, close together in time. Dragging a padding slider from 20px to 14px is one entry rather than seven, while padding and colour stay two. Coalescing keeps what the OLDER entry remembered and its id — the state before the whole gesture, rebuilt under the new change's reference, and the id because a row already written for the first keystroke should be updated by the finished sentence rather than joined by a second row saying almost the same thing.

Stepping back and forward is folded the same way: five taps are one row saying net movement, not five saying one each, and a run that lands where it started is dropped before it is ever written.

Trimming is by memory, not by count

A count is wrong in both directions at once — fifty steps is a few minutes on a small page and 57 MB of ETS on a large one — and being capped is what made the editor stop recording anything at all once it filled. The far end goes one step at a time until the session is inside its budget, and a single step bigger than the whole budget is kept anyway: a history that cannot go back at all is worse than being briefly over.

Every path that drops an entry drops what it remembered with it. A trimmed entry, a discarded redo stack, a coalesced duplicate — each is a row in EditorHistoryState that nothing can reach afterwards, charged to the session's budget for the rest of its life. Nothing here deletes one, though: the session that still points at it is the stored one until this change is stored, so MishkaCmsCore.Runtime.PageBuilder.EditorSession deletes whatever left both stacks once it is.

See MishkaCmsCore.Runtime.PageBuilder.EditorSession, which owns the session these operate on and is the only caller.

Summary

Functions

The entry as it stands after coalescing, waiting to be written.

Records one op, returning the session with the entry on its stack.

Records many ops as ONE entry — a bulk action, a find-and-replace, an assistant's answer.

The entry for a wholesale replacement of the document — a save, a draft, a revert.

The row a run of undos or redos writes — one row for the whole run, saying net movement.

The entries between where the document is now and the version ref names, or nil when it is on neither stack.

Drops entries from the far end until the session is inside its memory budget.

The selection if it still exists in tree, otherwise nil.

Functions

queue(pending, entry)

@spec queue(map() | nil, map()) :: map()

The entry as it stands after coalescing, waiting to be written.

record(session, op, tree, ref)

Records one op, returning the session with the entry on its stack.

A tree identical to the one already held is not an edit and passes straight through — tabbing through the style panel blurs every field on the way, each blur arriving as a set_style with the value it already had, and recording those made the author press undo once per field they walked over to take back one real change.

record_run(session, tree, label, ref, opts)

Records many ops as ONE entry — a bulk action, a find-and-replace, an assistant's answer.

opts names who made it (:author_type, :prompt, :backend), which is where an assistant's row gets its own sentence and the model behind it. Defaulting to :human leaves every other caller what it was. A run producing the very same tree is not an edit, for the reason record/4 gives.

replacement_entry(session, tree, reason, ref)

@spec replacement_entry(
  map(),
  MishkaCmsCore.Runtime.PageBuilder.Tree.t(),
  atom(),
  reference()
) :: map()

The entry for a wholesale replacement of the document — a save, a draft, a revert.

step_entry(session, direction, ref)

@spec step_entry(map(), :undo | :redo, reference()) :: map()

The row a run of undos or redos writes — one row for the whole run, saying net movement.

Five taps are one thing a person did, not five, and stepping back and forward again is nothing at all. The in-memory stack is untouched by this: undo/1 still only moves an entry between past and future, which is what makes Ctrl+Z instant.

steps_to(session, ref)

@spec steps_to(map(), reference()) :: [map()] | nil

The entries between where the document is now and the version ref names, or nil when it is on neither stack.

A patch reverses exactly one step, so a version several steps away is reached by applying each patch in between — which is why this lives here rather than in the store: the session is the thing that knows the order.

trim_history(entries, token)

@spec trim_history([map()], String.t()) :: [map()]

Drops entries from the far end until the session is inside its memory budget.

What the dropped entries remembered is not deleted here — the session still pointing at them stays the stored one until the trimmed one replaces it.

valid_selection(tree, selection)

@spec valid_selection(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), String.t() | nil) ::
  String.t() | nil

The selection if it still exists in tree, otherwise nil.