# `MishkaCmsCore.Runtime.PageBuilder.EditorSession.Journal`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/editor_session/journal.ex#L5)

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.

# `queue`

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

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

# `record`

```elixir
@spec record(map(), map(), MishkaCmsCore.Runtime.PageBuilder.Tree.t(), reference()) ::
  map()
```

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`

```elixir
@spec record_run(
  map(),
  MishkaCmsCore.Runtime.PageBuilder.Tree.t(),
  String.t(),
  reference(),
  keyword()
) ::
  map()
```

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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
