Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol terminal sync sync_manager.ex
Raw

lib/raxol/terminal/sync/sync_manager.ex

defmodule Raxol.Terminal.Sync.Manager do
@moduledoc """
Manages synchronization between different terminal components (splits, windows, tabs).
Provides a high-level interface for component synchronization and state management.
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Core.Runtime.Log
alias Raxol.Terminal.Sync.{Component, System}
defstruct [
:components,
:sync_id
]
@type t :: %__MODULE__{
components: %{String.t() => Component.t()},
sync_id: String.t()
}
# Types
@type component_id :: String.t()
@type component_type :: :split | :window | :tab
@type sync_state :: %{
component_id: component_id(),
component_type: component_type(),
state: term(),
metadata: %{
version: non_neg_integer(),
timestamp: non_neg_integer(),
source: String.t()
}
}
# Client API
@doc """
Starts the sync manager.
"""
# BaseManager provides start_link/1 which calls init_manager/1
def register_component(component_id, component_type, initial_state \\ %{}) do
GenServer.call(
__MODULE__,
{:register_component, component_id, component_type, initial_state}
)
end
def unregister_component(component_id) do
GenServer.call(__MODULE__, {:unregister_component, component_id})
end
@doc """
Syncs a component's state.
"""
@spec sync_state(String.t(), String.t(), term(), keyword()) :: :ok
def sync_state(component_id, component_type, new_state, opts \\ []) do
GenServer.call(
__MODULE__,
{:sync_state, component_id, component_type, new_state, opts}
)
end
@doc """
Syncs a component's state with default options.
"""
@spec sync_state(String.t(), term()) :: :ok
def sync_state(component_id, new_state) do
GenServer.call(__MODULE__, {:sync_state_simple, component_id, new_state})
end
def get_state(component_id) do
GenServer.call(__MODULE__, {:get_state, component_id})
end
def get_component_stats(component_id) do
GenServer.call(__MODULE__, {:get_component_stats, component_id})
end
# BaseManager Callbacks
@impl Raxol.Core.Behaviours.BaseManager
def init_manager(opts) do
state = %__MODULE__{
components: %{},
sync_id: generate_sync_id(opts)
}
{:ok, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(
{:register_component, component_id, component_type, initial_state},
_from,
state
) do
case Map.get(state.components, component_id) do
nil ->
now = Elixir.System.monotonic_time(:millisecond)
component = %Component{
id: component_id,
type: component_type,
state: initial_state,
version: now,
timestamp: System.system_time(:millisecond),
metadata: %{},
sync_count: 0,
conflict_count: 0
}
new_state = %{
state
| components: Map.put(state.components, component_id, component)
}
{:reply, :ok, new_state}
_existing ->
{:reply, {:error, :already_registered}, state}
end
end
def handle_manager_call({:unregister_component, component_id}, _from, state) do
case Map.get(state.components, component_id) do
nil ->
{:reply, {:error, :not_found}, state}
_component ->
new_state = %{
state
| components: Map.delete(state.components, component_id)
}
{:reply, :ok, new_state}
end
end
def handle_manager_call(
{:sync_state, component_id, component_type, new_state, opts},
_from,
state
) do
case Map.get(state.components, component_id) do
nil ->
{:reply, {:error, :not_found}, state}
existing_component ->
case do_sync_state(
state,
component_id,
component_type,
new_state,
opts,
existing_component
) do
{:ok, new_component} ->
new_components =
Map.put(state.components, component_id, new_component)
{:reply, :ok, %{state | components: new_components}}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
end
def handle_manager_call(
{:sync_state_simple, component_id, new_state},
_from,
state
) do
case Map.get(state.components, component_id) do
nil ->
{:reply, {:error, :not_found}, state}
existing_component ->
case do_sync_state(
state,
component_id,
existing_component.type,
new_state,
[],
existing_component
) do
{:ok, new_component} ->
new_components =
Map.put(state.components, component_id, new_component)
{:reply, :ok, %{state | components: new_components}}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
end
def handle_manager_call({:get_state, component_id}, _from, state) do
case Map.get(state.components, component_id) do
nil -> {:reply, {:error, :not_found}, state}
component -> {:reply, {:ok, component.state}, state}
end
end
def handle_manager_call({:get_component_stats, component_id}, _from, state) do
case Map.get(state.components, component_id) do
nil ->
{:reply, {:error, :not_found}, state}
component ->
# Return stats based on the component's internal state
stats = %{
sync_count: component.sync_count,
conflict_count: component.conflict_count,
last_sync: component.timestamp,
consistency_levels: %{strong: 0, eventual: 0, causal: 0}
}
{:reply, {:ok, stats}, state}
end
end
# Private Functions
def generate_sync_id(_state) do
timestamp = Elixir.System.monotonic_time(:millisecond)
random = :rand.uniform(1_000_000)
"#{timestamp}-#{random}"
end
def do_sync_state(
_state,
component_id,
component_type,
new_state,
opts,
existing_component
) do
# Use version from new_state if present, otherwise increment existing_version
existing_version = existing_component.version
new_version = Map.get(new_state, :version, existing_version + 1)
Log.debug(
"[Manager] do_sync_state: component_id=#{component_id}, type=#{component_type}, new_version=#{inspect(new_version)}, existing_version=#{inspect(existing_version)}"
)
# Apply consistency rules based on component type
case should_update_state(component_type, new_version, existing_version) do
:update ->
Log.debug("Updating state for #{component_id}")
component = %Component{
id: component_id,
type: component_type,
state: new_state,
version: new_version,
timestamp: System.system_time(:millisecond),
metadata: Keyword.get(opts, :metadata, %{}),
sync_count: existing_component.sync_count + 1,
conflict_count: existing_component.conflict_count
}
{:ok, component}
:keep_existing ->
Log.debug("Keeping existing state for #{component_id}")
{:ok, existing_component}
:conflict ->
Log.debug("Version conflict for #{component_id}")
# Increment conflict count and keep existing state
%Component{} = existing_component
_component_with_conflict = %{
existing_component
| conflict_count: existing_component.conflict_count + 1
}
{:error, :version_conflict}
end
end
defp should_update_state(component_type, new_version, existing_version) do
case component_type do
:split -> check_strong_consistency(new_version, existing_version)
:window -> check_strong_consistency(new_version, existing_version)
:tab -> check_eventual_consistency(new_version, existing_version)
_ -> check_strong_consistency(new_version, existing_version)
end
end
defp check_strong_consistency(new_version, existing_version)
when new_version > existing_version,
do: :update
defp check_strong_consistency(_new_version, _existing_version),
do: :keep_existing
defp check_eventual_consistency(new_version, existing_version)
when new_version > existing_version,
do: :update
defp check_eventual_consistency(new_version, existing_version)
when new_version == existing_version,
do: :conflict
defp check_eventual_consistency(_new_version, _existing_version),
do: :keep_existing
end