StatifierPersistence.Storage.InMemory (StatifierPersistence v0.12.0)

Copy Markdown View Source

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}.

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

state()

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

fetch_chart(opts, content_hash)

Fetches the chart stored under content_hash, or :chart_not_found.

fetch_execution(opts, execution_id)

Fetches the execution stored under execution_id, or :execution_not_found.

fetch_position(opts, session_id)

Fetches the position stored for session_id, or :position_not_found.

init(opts)

Starts the backing Agent and returns opts with :pid merged in - the handle every other callback expects as its first argument.

insert_execution(opts, execution_record)

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(opts, metadata)

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.

list_executions_by_metadata(opts, metadata)

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.

lock_execution(opts, execution_id, fun)

@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).

save_chart(opts, chart_record)

Stores chart_record under its content_hash, idempotent on repeat writes of the same hash.

save_position(opts, position_record)

Stores position_record under its session_id, overwriting any position already stored for that session.

supports_execution_outcome?(opts)

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

supports_metadata?(opts)

@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).

update_execution(opts, execution_record)

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.