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

Runs a decision: read the slice, decide on it, append under the condition that
it is still current — and when it is not, fold in whatever beat you and decide
again.

    Sourced.Decider.decide(store, %{placed: placed?(1), shipped: shipped?(1)}, fn
      %{placed: false} -> {:error, :not_placed}
      %{shipped: true} -> :noop
      _state -> {:ok, [%OrderShipped{id: 1}]}
    end)

Sugar over `Sourced.DecisionModel`, which stays fully usable by hand — this
closes the loop for the common case, it is not the only way to write one.

## The decision function

It is handed the folded state and returns one of:

  * `{:ok, events}` — append them under the model's condition.
  * `:noop` — there is nothing to do. An empty `{:ok, []}` means the same
    thing: on a retry the writer that beat you may have already done the work,
    and a decision that correctly concludes so is a success, not a failure.
  * `{:error, reason}` — the decision refuses. Returned as-is, and never
    retried; a domain rejection is not a race.

**It must be pure.** It runs once per attempt, so anything with an effect in it
happens once per attempt too. Effects belong after `decide/4` returns, on the
events it appended.

## Retrying

Only `Sourced.EventStore.OptimisticConcurrencyError` is retryable — every other
error comes straight back. A conflict means a competing append *already
committed*, so the stale model is refreshed with `Sourced.DecisionModel.refresh/2`
rather than rebuilt, and the decision runs again on the state the winner left
behind.

There is no backoff, deliberately: the contended state is readable the instant
the error arrives, and waiting does not improve the odds. Jitter to break up a
herd of writers conflicting on one slice is a different thing, and belongs to
the application.

Attempts default to three, and giving up returns the last conflict.

## Not inside your own transaction

`decide/4` owns the append, so it cannot be called from inside a transaction
you opened — see "Appending inside your own transaction" in
`Sourced.EventStore.Postgres`. A conflict there aborts the whole transaction,
which leaves the loop nothing to retry into, and the failure is invisible until
two writers actually collide. Use `Sourced.DecisionModel.append/3` directly in
that case and let the transaction be the unit that gets retried.

# `decision`

```elixir
@type decision() :: (Sourced.Projection.state() -&gt; verdict())
```

# `opts`

```elixir
@type opts() :: [{:attempts, pos_integer()}]
```

# `projections`

```elixir
@type projections() ::
  Sourced.Projection.t()
  | %{optional(Sourced.Projection.key()) =&gt; Sourced.Projection.t()}
```

# `verdict`

```elixir
@type verdict() ::
  {:ok, [Sourced.EventStore.Behaviour.event() | struct()]}
  | :noop
  | {:error, term()}
```

What a decision concludes from the state it was handed.

# `decide`

```elixir
@spec decide(Sourced.EventStore.t(), projections(), decision(), opts()) ::
  {:ok, non_neg_integer()} | :noop | {:error, term()}
```

Decides over `projections` against `store` and appends what `decision` returns.

Returns `{:ok, sequence}` for the last appended event, `:noop` when the
decision concluded there was nothing to do, or `{:error, reason}` — the
decision's own refusal, a store error, or the final
`Sourced.EventStore.OptimisticConcurrencyError` once the attempts run out.

## Options

  * `:attempts` — how many times to decide, counting the first. Defaults to
    `3`. `1` disables retrying.

---

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