# `MishkaCmsCore.Runtime.PageBuilderSnapshot`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/resources/page_builder_snapshot.ex#L5)

The document a history row can restore — gzipped HEEx, in a table of its own.

Split from `MishkaCmsCore.Runtime.PageBuilderEvent` on a measurement rather than a preference.
Postgres only moves a value out of the row past ~2 kB, and a typical snapshot is around 1.3 kB, so
keeping it on the event row left it INLINE: 1,638 bytes of heap per row against 122, thirteen times
the heap the timeline's own query walks on every page it draws.

## Why HEEx and not the tree

The editor's tree is 5.5–12.7× the size of the HEEx it came from, and a compressed tree is still
2.3–4.5× a gzipped HEEx while costing 3–5× the CPU to produce. `Serializer.to_heex/1` already runs
on every save, so gzipping its output adds a few percent to work already being done, where encoding
the tree adds ten to fifteen.

## What a round trip loses

Node ids. They are minted fresh by every `Tree.parse/1`, so a restored document is structurally and
stylistically identical and its ids are new. Anything that must survive a restore anchors by
`Tree.address/2` instead — the same answer `MishkaCmsCore.Runtime.PageComment` reached when it hit
this first.

## Refusal

A document past `max_bytes/0` gets no snapshot at all: the event row is still written and simply
cannot be reverted into, which the timeline says out loud. Storing a multi-megabyte payload on a
path that runs while somebody is typing is the wrong trade, and replay was rejected for making
every restore depend on every earlier row staying correct forever.

# `t`

```elixir
@type t() :: %MishkaCmsCore.Runtime.PageBuilderSnapshot{
  __lateral_join_source__: term(),
  __meta__: term(),
  __metadata__: term(),
  __order__: term(),
  aggregates: term(),
  byte_size: term(),
  calculations: term(),
  id: term(),
  inserted_at: term(),
  payload: term()
}
```

# `compress`

```elixir
@spec compress(String.t(), pos_integer()) ::
  {:ok, binary(), non_neg_integer()} | :too_large
```

Compress a document for storage, or refuse it for being too large.

Returns `{:ok, payload, byte_size}` or `:too_large`. The size recorded is the ORIGINAL's, because
that is the number a threshold is about and the compressed length says nothing useful to a reader.

# `decompress`

```elixir
@spec decompress(binary()) :: {:ok, String.t()} | :error
```

The document back, or `:error` when the payload will not decompress.

# `default_short_name`

# `input`

```elixir
@spec input(values :: map() | Keyword.t()) :: map() | no_return()
```

Validates that the keys in the provided input are valid for at least one action on the resource.

Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g `%MyResource{value: 1, value: 2}`. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g

```elixir
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
```

# `input`

```elixir
@spec input(values :: map() | Keyword.t(), action :: atom()) :: map() | no_return()
```

Same as `input/1`, except restricts the keys to values accepted by the action provided.

# `max_bytes`

```elixir
@spec max_bytes(atom() | nil) :: pos_integer()
```

Largest HEEx a snapshot will be taken of, in bytes, for a change made by `author_type`.

The assistant's rows are allowed a bigger document than anybody else's; see the note above
`@ai_max_bytes`.

# `primary_key_matches?`

---

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