# `TypeSafeAPI.FanOut`
[🔗](https://github.com/typesend/typesafe_ai/blob/v0.1.0-alpha.3/lib/typesafe_api/fan_out.ex#L1)

Evaluate many states against one question set, concurrently.

The question set is validated and encoded once; each state then gets its own
`TypeSafeAPI.evaluate/4` call in a task. Results come back in input order by
default, one `{:ok, result}` or `{:error, error}` per state, so a single
failure never hides the other results.

## Concurrency

`max_concurrency` defaults to 8. That number is a guess: TypeSafe has not
published rate limits or an SLA at the time of writing. Raise it if your
account allows, and watch for `:rate_limited` errors (the retry policy
absorbs short bursts of 429s before they surface).

Tasks run under `TypeSafeAPI.TaskSupervisor`, started by
`TypeSafeAPI.Application`, and are *not* linked to the caller. A task that
raises - a `Req` adapter blowing up, a state that will not JSON-encode -
becomes one `{:error, %TypeSafeAPI.Error{type: :unexpected}}` in that state's
position instead of killing the calling process and every other in-flight
request with it.

## Two timeouts

`:timeout` means the same thing here as in `TypeSafeAPI.evaluate/4`: the
timeout of a single HTTP attempt, passed straight through to each call.

`:task_timeout` is the cap on one state's whole task, every retry and every
backoff delay included. It defaults to the call's retry budget plus one
attempt timeout, or 60 000 ms when the budget is disabled, which is the
longest a well-behaved call can take. A state that overruns it comes back as
`{:error, %TypeSafeAPI.Error{type: :timeout}}`.

## When retries run out

A state whose retries are exhausted is not special. It comes back as an
ordinary `{:error, %TypeSafeAPI.Error{type: :rate_limited, retry_after_ms: ms}}`
in that state's position, next to the states that succeeded, and the batch
keeps going. With `on_error: :raise` that same error is raised instead, once
every task has been collected.

With `ordered: false` the outcomes arrive as they finish rather than in
input order, so under `on_error: :raise` the error that raises is the first
one to *finish*, which is not necessarily the earliest state in the input.
If you need the failure to be reproducible, keep `ordered: true`.

`retry_after_ms` is carried on the error so the caller can back off before
the next batch. The per-call retry policy has already given up by the time
you see it, so the useful move is to sleep for the longest `retry_after_ms`
in the batch, then resend just the failed states.

## The one failure that is not per-state

An invalid *question set* fails before any request is sent, so there is no
per-state outcome to put it in. `evaluate_many/4` then returns a bare
`{:error, %TypeSafeAPI.Error{type: :validation}}` rather than a list - the
reason the return type is a union. Match on it, or pass `on_error: :raise`,
which raises that error too rather than handing back a tuple from the mode
whose whole point is not to.

Passing a `%TypeSafeAPI.SystemOne.Prepared{}` from
`TypeSafeAPI.SystemOne.prepare/1` instead of a question set skips both the
re-validation and that failure mode.

# `outcome`

```elixir
@type outcome() :: {:ok, TypeSafeAPI.Result.t()} | {:error, TypeSafeAPI.Error.t()}
```

# `evaluate_many`

```elixir
@spec evaluate_many(
  TypeSafeAPI.Client.t(),
  Enumerable.t(),
  TypeSafeAPI.Question.input() | TypeSafeAPI.SystemOne.Prepared.t(),
  keyword()
) :: [outcome()] | {:error, TypeSafeAPI.Error.t()}
```

Evaluates each state in `states` against `questions`.

`questions` is a question set, or a `%TypeSafeAPI.SystemOne.Prepared{}` from
`TypeSafeAPI.SystemOne.prepare/1`.

Returns a list of outcomes (in input order unless `ordered: false`), or
`{:error, error}` when the question set itself is invalid.

## Options

* `:max_concurrency` (`t:pos_integer/0`) - Maximum number of in-flight requests. A guess; see the module docs. The default value is `8`.

* `:task_timeout` - Cap in milliseconds on one state's whole task, covering every retry and delay. Defaults to the retry budget plus one attempt timeout, or 60 000 when the budget is disabled. Not to be confused with `:timeout`, which is one HTTP attempt, as in `TypeSafeAPI.evaluate/4`.

* `:ordered` (`t:boolean/0`) - Return results in input order. `false` yields them as they finish. The default value is `true`.

* `:on_error` - `:collect` returns error tuples in place; `:raise` raises the first error. The default value is `:collect`.

Every other option (`:model`, `:timeout`, `:retry`, `:req_options`,
`:telemetry`) is passed to each call and means what it means in
`TypeSafeAPI.evaluate/4`; see `TypeSafeAPI.SystemOne.options_schema/0`.

# `options_schema`

```elixir
@spec options_schema() :: NimbleOptions.t()
```

The options `evaluate_many/4` handles itself.

Every other option is passed through to each individual call; see
`TypeSafeAPI.SystemOne.options_schema/0`.

* `:max_concurrency` (`t:pos_integer/0`) - Maximum number of in-flight requests. A guess; see the module docs. The default value is `8`.

* `:task_timeout` - Cap in milliseconds on one state's whole task, covering every retry and delay. Defaults to the retry budget plus one attempt timeout, or 60 000 when the budget is disabled. Not to be confused with `:timeout`, which is one HTTP attempt, as in `TypeSafeAPI.evaluate/4`.

* `:ordered` (`t:boolean/0`) - Return results in input order. `false` yields them as they finish. The default value is `true`.

* `:on_error` - `:collect` returns error tuples in place; `:raise` raises the first error. The default value is `:collect`.

---

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