# `Sourced.DecisionModel`
[🔗](https://gitlab.com/dimakula/sourced/-/blob/0.2.0/lib/sourced/decision_model.ex#L1)

The state a decision is made on, and the condition that keeps it valid.

Building a decision model reads the slice of the stream a
`Sourced.Projection` describes, folds it into a state, and remembers the
sequence that slice was at. Deciding on that state and appending with
`append/3` is the whole DCB write path: the append lands only if nothing
matching the query arrived in between.

    {:ok, %{state: %{placed: placed?, shipped: shipped?}} = model} =
      Sourced.DecisionModel.build(store, %{
        placed: MyApp.Orders.placed?(1),
        shipped: MyApp.Orders.shipped?(1)
      })

    if placed? and not shipped? do
      Sourced.DecisionModel.append(store, model, [%OrderShipped{id: 1}])
    end

A competing writer that got there first makes that append fail with
`Sourced.EventStore.OptimisticConcurrencyError`. The model is stale in exactly
the events that beat it: `refresh/2` folds those in, and the decision runs
again on the state the winner left behind.

The consistency boundary is the query, so it is exactly as wide as the
projections the decision reads — two decisions over different slices never
contend.

# `t`

```elixir
@type t() :: %Sourced.DecisionModel{
  expected_sequence: non_neg_integer(),
  projection: Sourced.Projection.t(),
  query: Sourced.EventStore.Query.t(),
  state: Sourced.Projection.state()
}
```

`:state` is what the projection folded to, `:query` the slice it was folded
from, and `:expected_sequence` the sequence that slice was at — `0` when no
event matched.

`:projection` is the composed projection that produced `:state`, kept so the
model can fold further events into itself. State alone cannot advance itself.

# `append`

```elixir
@spec append(Sourced.EventStore.t(), t(), [
  Sourced.EventStore.Behaviour.event() | struct()
]) ::
  {:ok, non_neg_integer()}
  | {:error, Sourced.EventStore.Behaviour.append_error() | term()}
```

Appends `events` under the condition that `model` is still current.

The write half of the DCB cycle, and the one to reach for: the query and the
sequence it was read at travel together, which is what makes the append
conflict on exactly the slice the decision was made over.

    case Sourced.DecisionModel.append(store, model, [%OrderShipped{id: 1}]) do
      {:ok, sequence} -> ...
      {:error, %Sourced.EventStore.OptimisticConcurrencyError{}} -> # refresh/2 and decide again
    end

Raises `ArgumentError` if `events` is empty — a decision with nothing to
append should not reach the store at all.

# `build`

```elixir
@spec build(
  Sourced.EventStore.t(),
  Sourced.Projection.t()
  | %{optional(Sourced.Projection.key()) =&gt; Sourced.Projection.t()}
) :: {:ok, t()} | {:error, term()}
```

Reads `store` and folds a decision model out of `projections`.

Takes either a `Sourced.Projection` or a map of them, composed with
`Sourced.Projection.compose/1` so the state is keyed the same way.

The whole slice is read, and there are deliberately no `:from`, `:to` or
`:limit` options: a model built from part of its slice would carry an
`:expected_sequence` from the middle of the stream and admit an append that
should have conflicted.

# `refresh`

```elixir
@spec refresh(Sourced.EventStore.t(), t()) :: {:ok, t()} | {:error, term()}
```

Folds whatever has matched since `model` was last read into `model`.

A model that lost an append is stale only in the events that beat it, so the
retry path is a bounded read from where the model stopped rather than another
full read of the slice:

    with {:error, %OptimisticConcurrencyError{}} <- append_under(model),
         {:ok, model} <- Sourced.DecisionModel.refresh(store, model) do
      # decide again on the state the winner left behind
    end

Same round trip as `build/2` and a shorter fold, by as much as the slice is
long.

An empty batch leaves the model exactly as it was. A read reports the last
sequence *it saw*, which is `0` when nothing matched, and taking that as the
new `:expected_sequence` would assert that nothing has ever matched the query
— a condition no append can satisfy.

Folding forward is sound only because nothing can become visible *below* a
sequence already read. That comes from the adapter clamping reads to its
watermark, so it holds for anything built on `Sourced.EventStore.query/2`,
and only as far as the watermark has advanced.

---

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