# `MishkaCmsCore.Runtime.States.EditorHistoryState`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/states/editor_history_state.ex#L5)

What the undo history holds, kept beside the session rather than inside it.

## Why they are not in the session

`MishkaCmsCore.Runtime.States.EditorSessionState` stores one row per editing session, and ETS
copies a whole row on every read and every write. One placed component reads and writes that row
about thirteen times.

## Patches, not documents

Each row is a `MishkaCmsCore.Runtime.PageBuilder.Tree.Patch` — the nodes as they were before one
step, and nothing else.

It used to be a whole document per entry. In memory that is nearly free, because fifty versions
share almost every node, but ETS discards structural sharing: a term written to a table is
flattened, so fifty entries landed as fifty independent copies. Measured on a 5,001-node document
that was 1.2 MB an entry and **57.5 MB for one person's session** — and the fifty-entry cap that
bounded it was itself the bug that made the editor stop recording after fifty changes.

A patch is proportional to what changed, not to what exists: **290 bytes** for a typical edit
whatever the document's size, 84× to 4,160× smaller than the document it belongs to. So the depth
of the history became a question about memory, which is answerable, instead of a count, which was
a guess.

## The budget

`@max_bytes` per session, not a number of entries. A count is wrong in both directions at once —
fifty entries is a few minutes' work on a small page and 57 MB on a large one. A budget gives a
small document thousands of steps and a large one as many as it can afford, and either way the
memory a session can hold is a number you can multiply by your editor count.

    config :mishka_cms_core, builder_history_bytes: 8_388_608

## Reading one back

A patch reverses one step from the version after it, so reaching a version several steps away means
applying each patch in between. `MishkaCmsCore.Runtime.PageBuilder.EditorSession` does that walking
because it is the thing that knows the order; here they are only stored and fetched by reference.
Measured at 79 µs for a hundred steps.

Same crash-safe shape as its sibling: a GenServer owns a `:named_table, :public` table and every
read guards on `:ets.whereis/1`, so racing the table's creation returns `:error` rather than
raising. Rows are dropped with their session, and swept hourly for any session that vanished
without saying so.

# `bytes`

```elixir
@spec bytes(String.t()) :: non_neg_integer()
```

What this session's history is holding, in bytes.

What the budget is measured against, and what the runtime-health panel reports.

# `bytes`

```elixir
@spec bytes(String.t(), reference()) :: non_neg_integer()
```

What one step is holding, in bytes — `0` for one that is not held.

What trimming counts down by: the steps it drops are deleted only once the session that forgot
them is stored, so the total cannot be asked again after each one.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `count`

```elixir
@spec count() :: non_neg_integer()
```

How many steps are held right now, across every session. For the health panel and tests.

# `delete`

```elixir
@spec delete(String.t(), reference()) :: :ok
```

Drops one step — what falls off the far end when a session is over its budget.

# `delete_all`

```elixir
@spec delete_all(String.t()) :: :ok
```

Drops every step a session kept — what closing the editor does.

# `fetch`

```elixir
@spec fetch(String.t(), reference()) ::
  {:ok,
   {:patch, MishkaCmsCore.Runtime.PageBuilder.Tree.Patch.t()}
   | {:tree, MishkaCmsCore.Runtime.PageBuilder.Tree.t()}}
  | :error
```

What a reference remembers — `{:patch, patch}` for a step in the stack, `{:tree, tree}` for one
that stands alone — or `:error`.

`:error` is a real answer rather than a crash: the sweep, a closed session or a step dropped for
the budget can all take a row away, and an undo that finds nothing should leave the document alone
rather than take the editor down.

# `keep`

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

Remembers a whole document under `ref`, and returns `ref`.

For the steps that do not sit in the undo stack at all — the row an undo RUN writes, which
describes net movement rather than one transition. A patch only means something relative to the
version after it, and those have no version after them, so they keep the document itself.

Rare by construction: one per run of undos, not one per change. The reference is the caller's for
the reason `put/4` gives.

# `max_bytes`

```elixir
@spec max_bytes() :: pos_integer()
```

The memory one session's history may hold, in bytes.

# `put`

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

Remembers `before` as one step back from `during` under `ref`, and returns `ref`.

Both documents are wanted because a patch is the difference between them; the caller has each of
them at the moment it decides to remember one, and asking for both here is what keeps this from
having to guess what the previous version was.

The reference is the CALLER'S, made with `make_ref/0` before the change that records it is first
tried. A session change can be worked out more than once — see
`MishkaCmsCore.Runtime.States.EditorSessionState.update/2` — and a reference minted here on every
try left a row behind for each try that lost. Writing under one the caller already holds replaces
what it remembered instead, so a retry costs nothing and leaves nothing.

# `start_link`

---

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