A declared transition: the states it leaves, the state it reaches, the
guards that must hold and the effects it performs, each list composed as one
Kleisli arrow. {from, guards} body {to} is the Hoare triple; run/4
discharges it.
from and to are Hoare.State modules. The context is any map or struct
with a record key, the record whose status moves, and a state key, which
check/2 fills by matching from. Each arrow is
ctx -> {:ok, ctx} | {:error, reason} and may refine the context it returns.
The domain declares the transition; the module owning the records supplies
the body of the commit when it runs it, and commit/5 wraps that body in the
store's locked transaction, the from re-check and the to write.
The law: a run ends in to or leaves the record in from, never between.
A bare effect must be idempotent, so a commit that fails after it is
recovered by running again. An effect paired with an undo is reverted,
newest first, when anything after it fails.
Summary
Functions
Matches from into the context's state, then runs the guards.
Commits in one locked transaction: re-reads the record, confirms it is still
tagged by a from state, runs body, writes to's status and asserts to.
Runs the transition end to end: guards, then effects, then the commit.
Types
@type ctx() :: %{ :record => Hoare.Store.subject(), :state => struct() | nil, optional(atom()) => term() }
@type undo() :: (ctx() -> :ok)
Functions
Matches from into the context's state, then runs the guards.
@spec commit(t(), ctx(), body(), boolean(), opts()) :: {:ok, Hoare.Store.subject()} | {:error, :not_found | :status_changed | term()}
Commits in one locked transaction: re-reads the record, confirms it is still
tagged by a from state, runs body, writes to's status and asserts to.
A record already tagged by to is a concurrent run of the same transition:
it commits nothing and is {:ok, record} when converges? (no effect left
anything behind), {:error, :status_changed} otherwise. A violated to
raises, rolling the transaction back. The re-read carries no preloads, so
only the tag is re-checked; the properties held on the context under the
guards. The status is written through the schema's changeset/2.
Runs the transition end to end: guards, then effects, then the commit.
body is the domain's own writes; commit/5 wraps them. On success the
context comes back with record replaced by the committed record. opts
needs :store, a Hoare.Store; :lock defaults to {schema, id}.