MishkaCmsCore.Runtime.PageBuilderSnapshot (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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.

Summary

Functions

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

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

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

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

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

Types

t()

@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()
}

Functions

compress(heex, max)

@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(payload)

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

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

default_short_name()

input(opts)

@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

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

input(opts, action)

@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(arg1)

@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?(left, right)