# `Reactor`
[🔗](https://github.com/ash-project/reactor/blob/main/lib/reactor.ex/#L6)

Reactor is a dynamic, concurrent, dependency resolving saga orchestrator.

## Usage

You can construct a reactor using the `Reactor` Spark DSL:

```elixir
defmodule HelloWorldReactor do
  @moduledoc false
  use Reactor

  input :whom

  step :greet, Greeter do
    argument :whom, input(:whom)
  end

  return :greet
end
```

    iex> Reactor.run(HelloWorldReactor, %{whom: "Dear Reader"})
    {:ok, "Hello, Dear Reader!"}

or you can build it programmatically:

    iex> reactor = Builder.new()
    ...> {:ok, reactor} = Builder.add_input(reactor, :whom)
    ...> {:ok, reactor} = Builder.add_step(reactor, :greet, Greeter, whom: {:input, :whom})
    ...> {:ok, reactor} = Builder.return(reactor, :greet)
    ...> Reactor.run(reactor, %{whom: nil})
    {:ok, "Hello, World!"}

### Options

* `:extensions` (list of module that adopts `Spark.Dsl.Extension`) - A list of DSL extensions to add to the `Spark.Dsl`

* `:otp_app` (`t:atom/0`) - The otp_app to use for any application configurable options

* `:fragments` (list of `t:module/0`) - Fragments to include in the `Spark.Dsl`. See the fragments guide for more.

# `async_option`

```elixir
@type async_option() :: {:async?, boolean()}
```

When set to `false` forces the Reactor to run every step synchronously,
regardless of the step configuration.

Defaults to `true`.

# `concurrency_key_option`

```elixir
@type concurrency_key_option() :: {:concurrency_key, reference()}
```

Use a `Reactor.Executor.ConcurrencyTracker.pool_key` to allow this Reactor to
share it's concurrency pool with other Reactor instances.

If you do not specify one then the Reactor will initialise a new pool and
place it in it's context for any child Reactors to re-use.

Only used if `async?` is set to `true`.

# `context`

```elixir
@type context() :: %{optional(atom()) =&gt; any()}
```

# `context_arg`

```elixir
@type context_arg() :: Enumerable.t({atom(), any()})
```

# `fully_reversible_option`

```elixir
@type fully_reversible_option() :: {:fully_reversible?, boolean()}
```

When this option is set the Reactor will return a copy of the completed Reactor
struct for potential future undo.

# `halt_timeout_option`

```elixir
@type halt_timeout_option() :: {:halt_timeout, pos_integer() | :infinity}
```

How long to wait for asynchronous steps to complete when halting.

Defaults to 5000ms.

# `inputs`

```elixir
@type inputs() :: %{optional(atom()) =&gt; any()}
```

# `max_concurrency_option`

```elixir
@type max_concurrency_option() :: {:max_concurrency, pos_integer()}
```

Specify the maximum number of asynchronous steps which can be run in parallel.

Defaults to the result of `System.schedulers_online/0`.  Only used if
`async?` is set to `true`.

# `max_iterations_option`

```elixir
@type max_iterations_option() :: {:max_iterations, pos_integer() | :infinity}
```

The maximum number of iterations which after which the Reactor will halt.

Defaults to `:infinity`.

# `run_options`

```elixir
@type run_options() ::
  Enumerable.t(
    max_concurrency_option()
    | timeout_option()
    | max_iterations_option()
    | halt_timeout_option()
    | async_option()
    | concurrency_key_option()
    | fully_reversible_option()
  )
```

# `state`

```elixir
@type state() :: :pending | :executing | :halted | :failed | :successful
```

# `t`

```elixir
@type t() :: %Reactor{
  context: context(),
  description: nil | String.t(),
  id: any(),
  inputs: [Reactor.Input.t()],
  intermediate_results: %{required(any()) =&gt; any()},
  middleware: [Reactor.Middleware.t()],
  plan: nil | Multigraph.t(),
  return: any(),
  state: state(),
  steps: [Reactor.Step.t()],
  undo: [{Reactor.Step.t(), any()}]
}
```

# `timeout_option`

```elixir
@type timeout_option() :: {:timeout, pos_integer() | :infinity}
```

Specify the amount of execution time after which to halt processing.

Note that this is not a hard limit. The Reactor will stop when the first step
completes _after_ the timeout has expired.

Defaults to `:infinity`.

# `undo_options`

```elixir
@type undo_options() :: Enumerable.t(concurrency_key_option())
```

# `is_reactor`
*macro* 

```elixir
@spec is_reactor(any()) :: Macro.t()
```

A guard which returns true if the value is a Reactor struct

# `run`

```elixir
@spec run(t() | module(), inputs(), context_arg(), run_options()) ::
  {:ok, any()} | {:ok, any(), t()} | {:error, any()} | {:halted, t()}
```

Attempt to run a Reactor.

## Arguments

* `reactor` - The Reactor to run, either a Reactor DSL module, or a Reactor
  struct.
* `inputs` - A map of values passed in to satisfy the Reactor's expected
  inputs.
* `context` - An arbitrary map that will be merged into the Reactor context
  and passed into each step.

## Options

* `:max_concurrency` (`t:pos_integer/0`) - The maximum number of processes to use to run the Reactor

* `:timeout` - How long to allow the Reactor to run for The default value is `:infinity`.

* `:max_iterations` - The maximum number of times to allow the Reactor to loop The default value is `:infinity`.

* `:async?` (`t:boolean/0`) - Whether to allow the Reactor to start processes The default value is `true`.

* `:run_id` (`t:term/0`) - A unique identifier for the Reactor run

* `:fully_reversible?` (`t:boolean/0`) - Return the completed reactor as well as the result for possible later reversal The default value is `false`.

# `run!`

```elixir
@spec run!(t() | module(), inputs(), context_arg(), run_options()) ::
  any() | no_return()
```

Raising version of `run/4`.

# `undo`

```elixir
@spec undo(t(), context_arg(), undo_options()) :: :ok | {:error, any()}
```

Attempt to undo a previously successful Reactor.

Undo operations are always executed sequentially in reverse completion order.
This is intentional: while the forward execution graph captures data
dependencies between steps, it cannot capture the full set of constraints
that may apply during rollback. A step's undo logic may have side effects
that affect other steps in ways not expressed in the original dependency
graph, or external systems may have ordering constraints during rollback
that differ from forward execution.

Since undo operations are expected to be infrequent (only triggered on
explicit reversal requests), sequential execution is an acceptable trade-off
that ensures predictable rollback behaviour.

## Arguments

* `reactor` - The previously successful Reactor struct.
* `context` - An arbitrary map that will be merged into the Reactor context and passed into each undo.

## Options

# `undo!`

```elixir
@spec undo!(t(), context_arg(), undo_options()) :: :ok | no_return()
```

A raising version of `undo/2`

---

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