Current section
Files
Jump to
Current section
Files
lib/a2ui/surface_manager.ex
defmodule A2UI.SurfaceManager do
@moduledoc """
Pure functional state manager for A2UI surfaces.
Manages a `%{surface_id => %Surface{}}` map by applying protocol messages.
No GenServer — surfaces live in LiveView assigns.
"""
alias A2UI.{DataModel, Surface}
alias A2UI.Protocol.Messages.{
CreateSurface,
DeleteSurface,
UpdateComponents,
UpdateDataModel
}
@type surfaces :: %{String.t() => Surface.t()}
@doc """
Creates an empty surfaces map.
"""
@spec new() :: surfaces()
def new, do: %{}
@doc """
Applies a protocol message to the surfaces map.
Returns `{:ok, updated_surfaces}` or `{:error, reason}`.
"""
@spec apply_message(surfaces(), struct()) :: {:ok, surfaces()} | {:error, atom()}
def apply_message(surfaces, %CreateSurface{} = msg) do
surface = %Surface{
id: msg.surface_id,
catalog_id: msg.catalog_id,
theme: msg.theme,
send_data_model: msg.send_data_model,
components: %{},
data_model: DataModel.new()
}
{:ok, Map.put(surfaces, msg.surface_id, surface)}
end
def apply_message(surfaces, %UpdateComponents{} = msg) do
with {:ok, surface} <- fetch_surface(surfaces, msg.surface_id) do
updated_components =
Enum.reduce(msg.components, surface.components, fn component, acc ->
Map.put(acc, component.id, component)
end)
{:ok, Map.put(surfaces, msg.surface_id, %{surface | components: updated_components})}
end
end
def apply_message(surfaces, %UpdateDataModel{} = msg) do
with {:ok, surface} <- fetch_surface(surfaces, msg.surface_id) do
result =
if msg.has_value do
DataModel.set(surface.data_model, msg.path, msg.value)
else
DataModel.delete(surface.data_model, msg.path)
end
case result do
{:ok, data_model} ->
{:ok, Map.put(surfaces, msg.surface_id, %{surface | data_model: data_model})}
{:error, _} = error ->
error
end
end
end
def apply_message(surfaces, %DeleteSurface{} = msg) do
if Map.has_key?(surfaces, msg.surface_id) do
{:ok, Map.delete(surfaces, msg.surface_id)}
else
{:error, :surface_not_found}
end
end
defp fetch_surface(surfaces, surface_id) do
case Map.fetch(surfaces, surface_id) do
{:ok, surface} -> {:ok, surface}
:error -> {:error, :surface_not_found}
end
end
end