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
endThe 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
endThen 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.
Summary
Callbacks
Loads the most recently saved versioned layout for the given canvas.
Persists the current versioned layout for the given canvas.
Functions
Calls load/1 defensively and normalizes failures.
Calls save/2 defensively and normalizes failures.
Types
Callbacks
@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 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.
@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.
Functions
@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.
@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.