# `StatifierPersistence.Storage.InMemory`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.12.0/lib/statifier_persistence/storage/in_memory.ex#L1)

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.

# `state`

```elixir
@type state() :: %{
  charts: %{
    required(StatifierPersistence.Storage.Adapter.content_hash()) =&gt;
      StatifierPersistence.Storage.Adapter.chart_record()
  },
  positions: %{
    required(StatifierPersistence.Storage.Adapter.session_id()) =&gt;
      StatifierPersistence.Storage.Adapter.position_record()
  },
  executions: %{
    required(StatifierPersistence.Storage.Adapter.execution_id()) =&gt;
      StatifierPersistence.Storage.Adapter.execution_record()
  },
  locks: %{
    required(StatifierPersistence.Storage.Adapter.execution_id()) =&gt; 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.

# `fetch_chart`

```elixir
@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`.

# `fetch_execution`

```elixir
@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`.

# `fetch_position`

```elixir
@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`.

# `init`

```elixir
@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.

# `insert_execution`

```elixir
@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.

# `list_execution_states_by_metadata`

```elixir
@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
`c:StatifierPersistence.Storage.Adapter.list_execution_states_by_metadata/2`).

The same containment `list_executions_by_metadata/2` applies, projected down
to the three `t: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.

# `list_executions_by_metadata`

```elixir
@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
`c: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.

# `lock_execution`

```elixir
@spec lock_execution(
  StatifierPersistence.Storage.Adapter.opts(),
  StatifierPersistence.Storage.Adapter.execution_id(),
  (-&gt; 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 `c: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).

# `save_chart`

```elixir
@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.

# `save_position`

```elixir
@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.

# `supports_execution_outcome?`

```elixir
@spec supports_execution_outcome?(StatifierPersistence.Storage.Adapter.opts()) ::
  boolean()
```

Declares outcome support (the optional
`c:StatifierPersistence.Storage.Adapter.supports_execution_outcome?/1`): this
adapter keeps the blob on the execution record like every other field.

# `supports_metadata?`

```elixir
@spec supports_metadata?(StatifierPersistence.Storage.Adapter.opts()) :: boolean()
```

Declares metadata support (the optional
`c:StatifierPersistence.Storage.Adapter.supports_metadata?/1`): this
adapter stores the map with the execution record and returns it verbatim
(ADR-0006 decision 3).

# `update_execution`

```elixir
@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.

---

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