# `StatifierPersistence.Storage.InMemory`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.18.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.

# `count_executions_by_content_hash`

```elixir
@spec count_executions_by_content_hash(
  StatifierPersistence.Storage.Adapter.opts(),
  StatifierPersistence.Storage.Adapter.content_hash()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.execution_counts()}
  | {:error, StatifierPersistence.Storage.Adapter.error()}
```

Counts the executions on `content_hash` per stored arm (the optional
`c:StatifierPersistence.Storage.Adapter.count_executions_by_content_hash/2`,
ADR-0012 decision 3).

Every key is present for every hash, so an unknown one answers zeros.
A fold over the execution map is what this adapter has - an Agent holds
no index - so this is the reference implementation of the *contract*,
not of the aggregate the contract exists for; the Ecto adapter is where
the count is a grouped count.

`children` counts the durable-child linkage pins naming `content_hash`
whose parent execution is `:active` or `:needs_migration` (ADR-0012
decision 1, as ADR-0014 decision 4 reads it): a second
pass over the same map, reading each execution's reserved metadata key
through `StatifierPersistence.Execution.Linkage.from_metadata/1` and
looking its parent up by execution id.

# `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`.

A tombstoned hash answers `{:error, {:chart_retired, info}}` instead
(ADR-0012 decision 6): the entry is still there and its blobs are
`nil`, and an entry with `nil` blobs is not a chart this adapter
holds.

# `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`.

# `fetch_retired_info`

```elixir
@spec fetch_retired_info(
  StatifierPersistence.Storage.Adapter.opts(),
  StatifierPersistence.Storage.Adapter.content_hash()
) :: {:ok, StatifierPersistence.Storage.Adapter.retired_info() | nil}
```

Reads the tombstone on `content_hash`, or `nil` for a hash that has
none (the optional
`c:StatifierPersistence.Storage.Adapter.fetch_retired_info/2`).

The Agent answers with the tombstone alone rather than the stored
entry, so the reply carries no chart bytes.

# `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_active_execution_ids_by_content_hash`

```elixir
@spec list_active_execution_ids_by_content_hash(
  StatifierPersistence.Storage.Adapter.opts(),
  StatifierPersistence.Storage.Adapter.content_hash()
) ::
  {:ok, [StatifierPersistence.Storage.Adapter.execution_id()]}
  | {:error, StatifierPersistence.Storage.Adapter.error()}
```

Lists the ids of the `:active` executions on `content_hash` (the
optional
`c:StatifierPersistence.Storage.Adapter.list_active_execution_ids_by_content_hash/2`,
ADR-0012 decision 4): what a pin source is handed as its context.

A filter over the same execution map the counts fold over, in the
order the map yields, which is no order a caller may rely on: the
contract is a list of ids, not a sequence.

# `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()
```

Runs `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).

# `prune_executions`

```elixir
@spec prune_executions(
  StatifierPersistence.Storage.Adapter.opts(),
  DateTime.t(),
  pos_integer()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.prune_counts()}
  | {:error, StatifierPersistence.Storage.Adapter.error()}
```

Prunes one batch of finished executions (the optional
`c:StatifierPersistence.Storage.Adapter.prune_executions/3`,
ADR-0016): one `Agent.get_and_update/2`, this adapter's transaction.

This adapter keeps no input log (ADR-0010 decision 1), so a batch
holds only the executions that still carry a position blob, and it
answers `inputs: 0`.

# `retire_chart`

```elixir
@spec retire_chart(
  StatifierPersistence.Storage.Adapter.opts(),
  StatifierPersistence.Storage.Adapter.content_hash(),
  StatifierPersistence.Storage.Adapter.retirement()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.retired_info()}
  | {:error, StatifierPersistence.Storage.Adapter.error()}
```

Retires the chart on `content_hash` (the optional
`c:StatifierPersistence.Storage.Adapter.retire_chart/3`, ADR-0012
decisions 5 and 6).

The counts and the tombstone are one `Agent.get_and_update/2`, which
is this adapter's whole answer to the callback's atomicity contract:
the Agent serves one message at a time, so nothing can be created on
the hash between the counting and the write.

A refused retirement returns the state unchanged, and the refusal
carries every count - the five execution arms, the durable-child
pins, the position rows, and each source's own counts - while only
ADR-0012 decision 1's blocking set causes one.

The tombstone keeps the entry and its content hash and nulls both
blobs, which is what makes the retired arm of `fetch_chart/2`
answerable at all.

# `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.

A tombstoned hash is refused with `{:error, {:chart_retired, info}}`
and is not revived (ADR-0012 decision 6). The read of the tombstone
and the write are one `Agent.get_and_update/2`, which is this
adapter's transaction.

# `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_chart_retirement?`

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

Declares chart retirement (the optional
`c:StatifierPersistence.Storage.Adapter.supports_chart_retirement?/1`,
ADR-0012 decision 6): an Agent's map has no `NOT NULL` to drop, so
the backend limit V07 records on a database adapter has no
counterpart here.

# `supports_content_hash_query?`

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

Declares the drained query (the optional
`c:StatifierPersistence.Storage.Adapter.supports_content_hash_query?/1`):
this adapter holds every execution record in one map and can count them.

# `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_execution_pruning?`

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

Declares execution pruning (the optional
`c:StatifierPersistence.Storage.Adapter.supports_execution_pruning?/1`,
ADR-0016).

# `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).

# `supports_retired_info?`

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

Declares the narrow tombstone read (the optional
`c:StatifierPersistence.Storage.Adapter.supports_retired_info?/1`).

# `supports_tree_migration?`

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

Declares the tree migration unit (the optional
`c:StatifierPersistence.Storage.Adapter.supports_tree_migration?/1`,
ADR-0015 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.
`ended_at` is the third: a stored stamp is kept whatever the given
record carries, and only a record with no stamp stored takes the given
one.

# `write_tree_migration`

```elixir
@spec write_tree_migration(StatifierPersistence.Storage.Adapter.opts(), [
  StatifierPersistence.Storage.Adapter.tree_write()
]) :: :ok | {:error, StatifierPersistence.Storage.Adapter.error()}
```

Writes a tree migration's re-pins and parks as one unit (the optional
`c:StatifierPersistence.Storage.Adapter.write_tree_migration/2`,
ADR-0015 decision 3).

Every write is applied to one copy of the state inside one
`Agent.get_and_update/2`, which is this adapter's transaction: the copy
replaces the state only when every write applied, and a write naming an
execution that is not stored answers `{:error, :execution_not_found}`
with the state unchanged.

---

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