# `LivePanels.Canvas.LayoutStore`
[🔗](https://github.com/livepanels/livepanels/blob/v0.1.0/lib/livepanels/canvas/layout_store.ex#L1)

Behaviour for server-side canvas layout persistence.

The default implementation (`LivePanels.Canvas.LayoutStore.Registry`) stores layouts in
an ETS table that lives for the lifetime of the server process. For durable
persistence across restarts, use `LivePanels.Canvas.LayoutStore.Ecto` or
implement this behaviour for another database or external cache.

## Ecto store

    defmodule MyApp.CanvasLayouts do
      use LivePanels.Canvas.LayoutStore.Ecto,
        repo: MyApp.Repo,
        schema: MyApp.LivePanelsCanvasLayout
    end

The schema should have a unique canvas id field and a layout field that stores
the versioned layout list unchanged. See
`LivePanels.Canvas.LayoutStore.Ecto` for schema and migration examples.

## Implementing another store

    defmodule MyApp.DbLayoutStore do
      @behaviour LivePanels.Canvas.LayoutStore

      @impl true
      def save(canvas_id, layout) do
        MyApp.Repo.upsert_layout(%{canvas_id: canvas_id, layout: layout})
        :ok
      end

      @impl true
      def load(canvas_id) do
        case MyApp.Repo.get_layout(canvas_id) do
          nil -> :not_found
          %{layout: layout} -> {:ok, layout}
        end
      end
    end

Then wire it into the canvas:

    use MyAppWeb, :live_view

    use LivePanels.Canvas,
      pubsub: MyApp.PubSub,
      session: [layout_store: MyApp.DbLayoutStore]

## Layout format

Both callbacks work with the canonical versioned layout payload emitted by
`LivePanels.Canvas.State.serialize_layout/1`:

    [
      %{"__livepanels_schema__" => 10},
      %{
        item_id: "item-1",
        type: "panel_item",
        x: 100,
        y: 80,
        w: 440,
        h: 340,
        placement_space: :world,
        placement_anchor: :top_left,
        persistent: true,
        metadata: %{},
        opts: %{},
        capabilities_snapshot: %{}
      }
    ]

The payload is a layout codec, not a hand-authored application item list:

- the first entry is the schema header
- item ids are preserved on restore
- item entries may include topology fields such as attachment data
- connector topology persists as connector capability state

Stores should persist and return this payload unchanged.

## Persistence policy

`save/2` is called after mutations that affect recoverable item state:

- **Persisted:** add, remove, move, resize, layout,
  restore_layout, durable item props, and finalized drag/resize geometry

- **Ephemeral:** in-flight pointer state, browser-only previews, loading
  indicators, and other display state that can be rebuilt

The guiding rule: geometry changes, lifecycle events, and durable item props
are persisted. Ephemeral display state is not.

`load/1` is called during `mount/3`, before any content-bearing items are rendered.
Returning `{:ok, layout}` causes the compositor to restore that versioned
layout immediately. The runtime validates loaded layouts before restore. During
persisted bootstrap, valid entries can load while rejected entries are skipped
and logged; explicit `restore_layout` commands remain all-or-nothing.

# `canvas_id`

```elixir
@type canvas_id() :: String.t()
```

# `layout_entry`

```elixir
@type layout_entry() :: map()
```

# `load`

```elixir
@callback load(canvas_id()) :: {:ok, [layout_entry()]} | :not_found | {:error, term()}
```

Loads the most recently saved versioned layout for the given canvas.

Return `{:ok, layout}` where `layout` is a list of `t:layout_entry/0`
maps if a layout exists, `:not_found` if no layout is stored for this
canvas (e.g. first visit), or `{:error, reason}` when the store cannot
answer.

Called during `mount/3` — keep implementations synchronous and fast to
avoid blocking the initial render.

# `save`

```elixir
@callback save(canvas_id(), [layout_entry()]) :: :ok | {:error, term()}
```

Persists the current versioned layout for the given canvas.

`layout` is the exact list returned by
`LivePanels.Canvas.State.serialize_layout/1`. Called frequently; keep
implementations fast.

Return `:ok` on success or `{:error, reason}` on failure.
The default `LivePanels.Canvas.LayoutStore.Registry` implementation persists immediately to ETS.
A failed save must never crash the compositor process.

# `safe_load`

```elixir
@spec safe_load(module(), canvas_id()) ::
  {:ok, [layout_entry()]} | :not_found | {:error, term()}
```

Calls `load/1` defensively and normalizes failures.

This helper accepts the canonical `{:ok, layout}`, `:not_found`, and
`{:error, reason}` returns. All other returns are treated as callback
contract errors.

# `safe_save`

```elixir
@spec safe_save(module(), canvas_id(), [layout_entry()]) :: :ok | {:error, term()}
```

Calls `save/2` defensively and normalizes failures.

This helper catches callback exceptions/exits, accepts `:ok` and
`{:error, reason}`, and treats all other returns as callback contract errors.

---

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