The reference StatifierPersistence.Storage.Adapter: an Agent holding
three maps - charts keyed by content hash, positions keyed by session id,
and executions keyed by execution id.
The chart map is keyed by the content hash alone, as every adapter's is:
byte-identical charts stored by two tenants are one entry, and tenant
scoping is the host's own (StatifierPersistence.Storage's moduledoc).
It ships in lib/, not the test-only support/ directory, for two
reasons: the conformance template this package ships in lib/ (this
package's own Testing namespace) needs a reference implementation to
check against from outside this repository's own test/, and a host
prototyping the stepper wants an adapter with no database to stand up.
Summary
Types
This adapter's state: the three record maps init/1 starts the Agent
with, plus the per-execution lock table lock_execution/3 acquires through.
Functions
Fetches the chart stored under content_hash, or :chart_not_found.
Fetches the execution stored under execution_id, or :execution_not_found.
Fetches the position stored for session_id, or :position_not_found.
Starts the backing Agent and returns opts with :pid merged in - the
handle every other callback expects as its first argument.
Inserts execution_record under its execution_id, refusing a duplicate with
{:error, :execution_exists}.
The status projection over a metadata match (the optional
StatifierPersistence.Storage.Adapter.list_execution_states_by_metadata/2).
Lists the executions whose stored metadata contains every key/value pair
in metadata, recursively for a nested map (the optional
StatifierPersistence.Storage.Adapter.list_executions_by_metadata/2,
ADR-0008 decision 5) - the same subset semantics
StatifierPersistence.Storage.Ecto's jsonb @> gives, and the same
ArgumentError on an empty or non-string-keyed map.
Executions fun under this adapter's per-execution mutual exclusion for execution_id
(the optional StatifierPersistence.Storage.Adapter.lock_execution/3).
Stores chart_record under its content_hash, idempotent on repeat
writes of the same hash.
Stores position_record under its session_id, overwriting any position
already stored for that session.
Declares outcome support (the optional
StatifierPersistence.Storage.Adapter.supports_execution_outcome?/1): this
adapter keeps the blob on the execution record like every other field.
Declares metadata support (the optional
StatifierPersistence.Storage.Adapter.supports_metadata?/1): this
adapter stores the map with the execution record and returns it verbatim
(ADR-0006 decision 3).
Overwrites the execution stored under execution_record's execution_id with the full
record, or refuses with :execution_not_found when no execution exists for the id.
Types
@type state() :: %{ charts: %{ required(StatifierPersistence.Storage.Adapter.content_hash()) => StatifierPersistence.Storage.Adapter.chart_record() }, positions: %{ required(StatifierPersistence.Storage.Adapter.session_id()) => StatifierPersistence.Storage.Adapter.position_record() }, executions: %{ required(StatifierPersistence.Storage.Adapter.execution_id()) => StatifierPersistence.Storage.Adapter.execution_record() }, locks: %{ required(StatifierPersistence.Storage.Adapter.execution_id()) => reference() } }
This adapter's state: the three record maps init/1 starts the Agent
with, plus the per-execution lock table lock_execution/3 acquires through.
Functions
@spec fetch_chart( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.content_hash() ) :: {:ok, StatifierPersistence.Storage.Adapter.chart_record()} | {:error, StatifierPersistence.Storage.Adapter.error()}
Fetches the chart stored under content_hash, or :chart_not_found.
@spec fetch_execution( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.execution_id() ) :: {:ok, StatifierPersistence.Storage.Adapter.execution_record()} | {:error, StatifierPersistence.Storage.Adapter.error()}
Fetches the execution stored under execution_id, or :execution_not_found.
@spec fetch_position( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.session_id() ) :: {:ok, StatifierPersistence.Storage.Adapter.position_record()} | {:error, StatifierPersistence.Storage.Adapter.error()}
Fetches the position stored for session_id, or :position_not_found.
@spec init(StatifierPersistence.Storage.Adapter.opts()) :: {:ok, StatifierPersistence.Storage.Adapter.opts()} | {:error, StatifierPersistence.Storage.Adapter.error()}
Starts the backing Agent and returns opts with :pid merged in - the
handle every other callback expects as its first argument.
@spec insert_execution( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.execution_record() ) :: :ok | {:error, StatifierPersistence.Storage.Adapter.error()}
Inserts execution_record under its execution_id, refusing a duplicate with
{:error, :execution_exists}.
The exists-check and the write run inside one Agent.get_and_update/2
call, so they are a single atomic state transition: two concurrent
inserts of the same execution_id cannot both return :ok.
This adapter supports the optional metadata map (ADR-0006 decision 3):
the map is stored with the record and returned by fetch_execution/2 verbatim,
whatever Elixir terms it holds - an Agent has no type system to refuse
one.
@spec list_execution_states_by_metadata( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.metadata() ) :: {:ok, [StatifierPersistence.Storage.Adapter.execution_state()]} | {:error, StatifierPersistence.Storage.Adapter.error()}
The status projection over a metadata match (the optional
StatifierPersistence.Storage.Adapter.list_execution_states_by_metadata/2).
The same containment list_executions_by_metadata/2 applies, projected down
to the three StatifierPersistence.Storage.Adapter.execution_state/0
fields. There is no index to serve it from here - an Agent holds a map -
so this is the reference implementation of the contract, not of the
performance the contract exists for; the Ecto adapter is where the
projection is a projection.
@spec list_executions_by_metadata( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.metadata() ) :: {:ok, [StatifierPersistence.Storage.Adapter.execution_record()]} | {:error, StatifierPersistence.Storage.Adapter.error()}
Lists the executions whose stored metadata contains every key/value pair
in metadata, recursively for a nested map (the optional
StatifierPersistence.Storage.Adapter.list_executions_by_metadata/2,
ADR-0008 decision 5) - the same subset semantics
StatifierPersistence.Storage.Ecto's jsonb @> gives, and the same
ArgumentError on an empty or non-string-keyed map.
@spec lock_execution( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.execution_id(), (-> result) ) :: {:ok, result} | {:error, StatifierPersistence.Storage.Adapter.error()} when result: term()
Executions fun under this adapter's per-execution mutual exclusion for execution_id
(the optional StatifierPersistence.Storage.Adapter.lock_execution/3).
Acquisition is an insert-if-absent on the Agent's lock table, one atomic
Agent.get_and_update/2 transition; contention spins with a small
bounded sleep (5ms) between attempts. The lock is
released in an after block, so any exit from fun - a raise included -
releases it; the raise itself propagates to the caller.
Simple and honest for a reference adapter. A production adapter should prefer its backend's native lock - the Ecto adapter implements this as a transaction-scoped advisory-plus-row lock (ADR-0004 decision 5 as amended 2026-08-22).
@spec save_chart( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.chart_record() ) :: :ok | {:error, StatifierPersistence.Storage.Adapter.error()}
Stores chart_record under its content_hash, idempotent on repeat
writes of the same hash.
@spec save_position( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.position_record() ) :: :ok | {:error, StatifierPersistence.Storage.Adapter.error()}
Stores position_record under its session_id, overwriting any position
already stored for that session.
@spec supports_execution_outcome?(StatifierPersistence.Storage.Adapter.opts()) :: boolean()
Declares outcome support (the optional
StatifierPersistence.Storage.Adapter.supports_execution_outcome?/1): this
adapter keeps the blob on the execution record like every other field.
@spec supports_metadata?(StatifierPersistence.Storage.Adapter.opts()) :: boolean()
Declares metadata support (the optional
StatifierPersistence.Storage.Adapter.supports_metadata?/1): this
adapter stores the map with the execution record and returns it verbatim
(ADR-0006 decision 3).
@spec update_execution( StatifierPersistence.Storage.Adapter.opts(), StatifierPersistence.Storage.Adapter.execution_record() ) :: :ok | {:error, StatifierPersistence.Storage.Adapter.error()}
Overwrites the execution stored under execution_record's execution_id with the full
record, or refuses with :execution_not_found when no execution exists for the id.
metadata is the documented exception to the full overwrite: it is
write-once (ADR-0006 decision 1 grants no way to change it after create),
so the stored map is carried forward and the given record's metadata
is ignored. outcome_blob is the second exception: a nil in the given
record carries the stored value forward, and a binary sets it.