# `MishkaCmsCore.Runtime.Authoring.Drafts`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.5-alpha.4/lib/mishka_cms_core/runtime/authoring/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.

## Publishing what a draft holds

A draft is the whole document, its settings included: the layout, the SEO, the path, who may open
it. Its own `:publish` writes all of that over the original, and an editor that publishes its
working copy straight to the original writes `carried/2` beside its markup, so neither publish
leaves a setting behind in a draft it then throws away. The original keeps its own code either
way; the fingerprint `original_changed?/3` compares covers exactly what a publish writes over, and
`follow/5` keeps a draft in step with what its own editor saved to the original meanwhile.

## The element CSS a draft holds

Custom CSS an author applies to an element while the editor's stage says Draft is not written to
the site's stylesheet row, which is the live site. The draft holds it instead, in `element_css`,
next to the markup that carries the scope classes it is keyed on: `autosave/8` writes it with that
markup when the editor hands it over as `element_css:`, and `save_as/8` takes it from the source it
is given. The draft preview compiles it (`MishkaCmsCore.Runtime.DraftPreviewAssets.css/2`), and a
copy of a draft carries it like any other field. The draft's own `:publish` writes it into the
document's element-CSS row (`publish_element_css/3`). It goes when the draft goes, so nothing else
has to be cleaned up.

# `autosave`

```elixir
@spec autosave(
  String.t(),
  map() | nil,
  String.t(),
  map(),
  String.t() | nil,
  String.t() | nil,
  integer() | nil,
  keyword()
) :: {: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.

`opts` takes `element_css:`, the Custom CSS the editor holds for this document's elements as
`%{scope => body}`, a blank body for a rule the author cleared. Given, the draft holds exactly that
map from this write on, beside the markup; left out, the draft keeps the element CSS it holds.

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.

# `carried`

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

What publishing `draft` writes over the document it was made from — every field it carries but
the document's code — exactly as the draft's own `:publish` writes it. `%{}` for anything that is
not a draft of `section`.

For an editor that publishes its working copy straight to the original: written beside the
markup, in the same write, these are the draft's layout, SEO, path, title and permissions, which
would otherwise go with the draft when the publish discards it.

# `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.

# `element_owner`

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

The owner of the element-CSS stylesheet row of the document `draft` was made from
(`MishkaCmsCore.Runtime.Authoring.ElementScopes.owner/2`): the row the editor's publish writes the
draft's `element_css` into. `nil` for a draft of a document not published yet, or for anything
that is not a draft.

# `follow`

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

Keeps `draft` in step with a change its own editor made to the original while it was open — a
setting saved to the page itself, say. `before` is the original as it stood, `now` as it is.

Every setting the change moved is written to the draft too, so publishing the draft does not put
the older one back; and the draft takes `now`'s fingerprint, so the change is not reported as
somebody else's by `original_changed?/3`. The draft's own markup is never replaced: when the
original's moved as well, the settings are carried and the fingerprint is kept, notice and all.

A draft that was already behind `before` — someone else changed the original first — is returned
as it is. Returns `{:ok, draft}` or `{:error, reason}`.

# `get`

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

The draft `draft_id` of `section`, read as `actor`: `{:ok, draft}`, or `{:error, reason}` for a
draft that is gone, is not theirs to read, or a section that keeps none.

# `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 and publishing it writes over. 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`.

The original's code is not in it. The editor's code drawer saves code to the original itself while
a draft is open, and no publish of the draft writes it, so a change to it is nothing the draft
would overwrite.

# `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.

# `publish_element_css`

```elixir
@spec publish_element_css(struct(), struct(), map() | nil) ::
  {:ok, %{required(String.t()) =&gt; String.t()}} | {:error, term()}
```

Writes `draft`'s `element_css` into the element-CSS row of `record`, the document the draft's
`:publish` has just written, as `actor`. It is laid over the row with
`MishkaCmsCore.Runtime.Authoring.ElementCss.merge/2`, as the editor's publish folds in the CSS it
holds and as the draft preview shows it: a rule the draft rewrote replaces the row's, a rule it
cleared goes, and the rest of the row stays. A draft that holds none writes nothing.

# `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/8`, 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/8` for why it is asked for rather than defaulted.

A source that holds `element_css` hands it to the draft, as `autosave/8` takes it: a new row copies
it with the rest, and a named draft written over takes it too. The editor passes the open document
with the element CSS it holds, so the named draft is the document as it stands, its CSS included.

# `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*
