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

The editor's autosave: the working document written to a **draft** row, not the live one.

## Why drafts and not the live record

Every write to a live runtime resource enqueues a compile job through `SendToOban`. An editor that
saved to the live row as you worked would therefore recompile the site on every change and publish
half-finished work to visitors. The draft resources exist precisely for this — they deliberately
carry no `SendToOban`, so writing one is cheap and invisible until someone publishes it.

A row belongs to the session that made it, and a session is one sitting, so the count grows with
use. `prune/3` caps it per document — `keep/0` is the number — dropping the oldest as a new one is
made.

That also makes the draft the shareable preview: `/_preview/:resource/:token` already serves a
draft by its `preview_token`, so "open in a new tab" needs no new route and no new auth story.

Component drafts do not exist as a resource, so a component's autosave is a no-op and its Save
button is the only persistence — reported honestly rather than silently doing nothing.

# `autosave`

```elixir
@spec autosave(
  String.t(),
  map() | nil,
  String.t(),
  map(),
  String.t() | nil,
  String.t() | nil,
  integer() | nil
) :: {:ok, struct()} | :unsupported | {:error, term()}
```

Writes the current document to this session's draft, creating it on the first call and updating it
afterwards.

`label` is the name to keep. `nil` falls back to the autosave's own label, but when the editor has
a NAMED draft open it passes that name through — otherwise the automatic save would quietly rename
the author's draft to "Autosave" on the next keystroke.

It takes no source to copy from, unlike `save_as/8`: a sitting with no row yet is one that started
from the live document, and from its first write on it is the row itself that is read.

`status` says which of an error page's templates is being written, and is `nil` for every section
that keeps one. IT IS ASKED FOR RATHER THAN DEFAULTED because a caller who leaves it off is not
writing nothing — `Section.save_fields/3` falls back to the 404 and replaces THAT entry, so the
work lands on a template nobody opened and buries the one that was there. A default made that
silent, and the one caller that took it up was "Keep it as a draft" on the way out.

Returns `{:ok, draft}`, `:unsupported` for a section with no draft resource, or `{:error, reason}`.

# `autosave_label`

```elixir
@spec autosave_label() :: String.t()
```

The label the automatic save owns.

# `discard`

```elixir
@spec discard(String.t(), String.t(), map()) :: :ok | {:error, term()}
```

Deletes a draft — what "Discard" on the resume bar does to the autosave.

Deleting rather than ignoring is deliberate: a bar that offers to restore work you have already
said you do not want, every time you open the page, is a bar people learn to dismiss without
reading.

# `display_label`

```elixir
@spec display_label(String.t() | nil) :: String.t() | nil
```

The name to show for a draft: the automatic save's label in the caller's language, any other name as it was typed.

# `hash`

```elixir
@spec hash(String.t(), map()) :: non_neg_integer()
```

A fingerprint of `original`, a section's live document: `:erlang.phash2/1` of every field its draft
carries. A draft keeps the one it was made from (`source_hash`), so a different one now means the
original was changed since — see `original_changed?/3`.

# `keep`

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

How many drafts a document keeps.

# `list`

```elixir
@spec list(String.t(), map() | nil, map()) :: [struct()]
```

Every draft of this record, newest first — what the editor's draft picker lists.

Returns `[]` for a section with no draft resource rather than raising, so a caller does not have to
ask `supported?/1` first.

# `named`

```elixir
@spec named(String.t(), map() | nil, map(), String.t()) :: struct() | nil
```

The draft this record already keeps under `label`, or `nil`.

What "a draft called X already exists" is asked with, so the editor can offer to update it instead
of quietly making a second one with the same name.

# `original_changed?`

```elixir
@spec original_changed?(struct()) :: boolean()
```

`original_changed?/3` for a draft on its own: the document it was made from is read by its
source id, as the system.

# `original_changed?`

```elixir
@spec original_changed?(String.t(), map() | nil, map() | nil) :: boolean()
```

Whether the document a pending draft was made from has changed since — a notice for its author,
nothing more. A draft that holds no fingerprint, or was published, answers `false`.

# `preview_segment`

```elixir
@spec preview_segment(String.t()) :: String.t() | nil
```

The `/_preview/:resource/:token` segment for a section.

# `prune`

```elixir
@spec prune(String.t(), map() | nil, map()) :: :ok
```

Drops everything past the newest `20` drafts of a record.

Runs where the count can grow and nowhere else — the moment a row is CREATED — so the realtime
save, which writes the same row over and over, costs one update and no extra read.

Newest is by `updated_at`, so a draft someone is working in right now cannot be the one dropped:
every change moves it back to the front of the list. A session whose row is pruned while it sits
idle is not broken by it either — its next write finds the id gone and starts a new row, which is
the same thing that happens when a colleague discards it from the picker.

A published draft is never touched: `list/3` reads pending drafts only, and a row with an
`applied_at` is the record of a publish rather than work in progress.

# `resource`

```elixir
@spec resource(String.t()) :: module() | nil
```

The Ash resource holding a section's drafts — what the editor points its drafts table at.

Returns `nil` for a section that keeps none, the same answer `supported?/1` gives.

# `save_as`

```elixir
@spec save_as(
  String.t(),
  map() | nil,
  map() | nil,
  String.t(),
  map(),
  String.t() | nil,
  String.t(),
  integer() | nil
) :: {:ok, struct()} | :unsupported | {:error, term()}
```

Saves the document under a NAME, creating that draft or updating it.

`draft_id` names the row to update — pass `nil` to create. Autosave has its own entry point,
`autosave/7`, and its own label, so naming a draft never collides with it.

`source` is the document a NEW row copies its fields from — the draft being forked or saved under
another name, and `nil` when the save starts from the live document, which is then the source. It
is asked for rather than defaulted for the reason `attrs/8` gives: a copy taken from the live
record hands the author today's published fields back with a success flash. Pass only a document
the author chose; the row the editor's own autosave made for a sitting is the live document's
working copy, not another document to copy from.

`status` says which of an error page's templates is being written, and is `nil` for every section
that keeps one template — see `autosave/7` for why it is asked for rather than defaulted.

# `source_key`

```elixir
@spec source_key(String.t()) :: atom() | nil
```

The draft attribute pointing back at the live record — `:source_page_id` and its siblings.

The drafts table scopes itself to one document by this key, so it has to be asked for by section
rather than assumed.

# `supported?`

```elixir
@spec supported?(String.t()) :: boolean()
```

Whether this section keeps drafts at all.

---

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