# Rules for working with Reactor

## Understanding Reactor

Reactor is a dynamic, concurrent, dependency resolving saga orchestrator for Elixir. It provides:

- **Saga orchestration** - Transaction-like semantics across multiple distinct resources with rollback capabilities
- **Dependency resolution** - Automatic calculation of execution order based on step dependencies using a directed acyclic graph (DAG)
- **Concurrent execution** - Runs as many steps as possible concurrently while respecting dependencies
- **Dynamic workflows** - Build workflows at runtime and add steps while the reactor is running
- **Composable DSL** - Declarative approach to defining workflows

Read documentation *before* attempting to use Reactor features. Do not assume prior knowledge of the framework or its conventions.

## Core Concepts

### Reactors
A Reactor is a workflow definition that contains inputs, steps, and their dependencies. Reactors can be defined using the DSL or built programmatically.

### Steps
Steps are the unit of work in a Reactor. Each step:
- Has a unique name
- Can depend on inputs or results from other steps
- Can run synchronously or asynchronously
- Can be compensated (handle errors) or undone (rollback on failure)
- Returns a result that other steps can use

### Arguments
Arguments define dependencies between steps. They specify:
- What data a step needs
- Where that data comes from (inputs or other step results)
- Optional transformations to apply to the data

## Code Structure & Organization

- Define Reactors as modules using the DSL for static workflows
- Use `Reactor.Builder` for dynamic workflow construction
- Create custom steps by implementing the `Reactor.Step` behaviour
- Organize complex workflows into composable sub-reactors
- Use meaningful names for inputs, steps, and arguments

## Basic Reactor DSL

Define a Reactor using the DSL:

```elixir
defmodule MyApp.UserRegistrationReactor do
  use Reactor

  # Define inputs (like function arguments)
  input :email
  input :password
  input :plan_name

  # Define steps with dependencies
  step :validate_email do
    argument :email, input(:email)
    run fn %{email: email}, _ ->
      if String.contains?(email, "@") do
        {:ok, email}
      else
        {:error, "Invalid email"}
      end
    end
  end

  step :hash_password do
    argument :password, input(:password)
    run fn %{password: password}, _ ->
      {:ok, Bcrypt.hash_pwd_salt(password)}
    end
  end

  step :create_user, MyApp.Steps.CreateUser do
    argument :email, result(:validate_email)
    argument :password_hash, result(:hash_password)
  end

  # Specify what to return
  return :create_user
end
```

## Step Implementation

### Using Anonymous Functions

For simple steps, use anonymous functions directly in the DSL:

```elixir
step :transform_data do
  argument :input, input(:raw_data)

  run fn %{input: data}, _ ->
    {:ok, String.upcase(data)}
  end
end
```

### Using Step Modules

For complex logic, implement the `Reactor.Step` behaviour:

```elixir
defmodule MyApp.Steps.CreateUser do
  use Reactor.Step

  @impl true
  def run(arguments, context, options) do
    case create_user(arguments.email, arguments.password_hash) do
      {:ok, user} -> {:ok, user}
      {:error, reason} -> {:error, reason}
    end
  end

  @impl true
  def compensate(reason, arguments, context, options) do
    # Handle errors - decide whether to retry or continue with rollback
    case reason do
      %DBConnection.ConnectionError{} -> :retry
      _other -> :ok
    end
  end

  @impl true
  def undo(user, arguments, context, options) do
    # Rollback successful execution
    case delete_user(user) do
      :ok -> :ok
      {:error, reason} -> {:error, reason}
    end
  end

  defp create_user(email, password_hash) do
    # Implementation here
  end

  defp delete_user(user) do
    # Implementation here
  end
end
```

## Step Return Values

Steps can return various values to control reactor execution:

- `{:ok, value}` - Success with result value
- `{:ok, value, [step]}` - Success with additional steps to add
- `{:error, reason}` - Failure (triggers compensation/undo)
- `:retry` or `{:retry, reason}` - Retry the step
- `{:halt, reason}` - Pause reactor execution

## Arguments and Dependencies

### Basic Arguments

```elixir
step :example do
  # Use input directly
  argument :email, input(:email)

  # Use result from another step
  argument :user, result(:create_user)

  # Use a static value
  argument :timeout, value(5000)
end
```

### Argument Transformations

Transform argument values before passing to steps:

```elixir
step :example do
  # Transform with anonymous function
  argument :user_id do
    source result(:create_user)
    transform &(&1.id)
  end

  # Extract nested values
  argument :birth_year, input(:user_data, [:birth_date, :year])

  # Transform input
  argument :age do
    source input(:birth_year)
    transform fn year -> Date.utc_today().year - year end
  end
end
```

## Built-in Step Types

### Debug Steps

Log information during execution:

```elixir
debug :log_user do
  argument :user, result(:create_user)
  argument :message, value("User created successfully")
end
```

### Map Steps

Process collections by applying steps to each element:

```elixir
map :process_users do
  source input(:user_list)
  batch_size 10
  allow_async? true

  step :validate_user do
    argument :user, element(:process_users)
    run fn %{user: user}, _ ->
      validate_user(user)
    end
  end
end
```

### Compose Steps

Embed one reactor inside another:

```elixir
compose :sub_workflow, MyApp.SubReactor do
  argument :input_data, result(:prepare_data)
end
```

### Switch Steps

Conditional execution based on predicates:

```elixir
switch :handle_user_type do
  on result(:user)

  matches? &(&1.type == :premium) do
    step :setup_premium_features do
      argument :user, result(:user)
      # Premium setup logic
    end
  end

  default do
    step :setup_basic_features do
      argument :user, result(:user)
      # Basic setup logic
    end
  end
end
```

### Group Steps

Execute related steps together with shared setup/teardown:

```elixir
group :user_setup do
  before_all &MyApp.setup_database/3
  after_all &MyApp.cleanup_database/1

  step :create_profile do
    # Profile creation logic
  end

  step :send_welcome_email do
    # Email logic
  end
end
```

### Around Steps

Wrap step execution with custom logic:

```elixir
around :transaction, &MyApp.with_transaction/4 do
  step :create_user do
    # User creation in transaction
  end

  step :create_profile do
    # Profile creation in transaction
  end
end
```

### Collect Steps

Gather multiple values into a single structure:

```elixir
collect :user_summary do
  argument :user, result(:create_user)
  argument :profile, result(:create_profile)
  argument :settings, result(:create_settings)

  transform fn inputs ->
    %{
      user: inputs.user,
      profile: inputs.profile,
      settings: inputs.settings
    }
  end
end
```

## Error Handling and Compensation

### Compensation
Handle step failures and decide how to proceed:

```elixir
defmodule MyApp.Steps.ApiCall do
  use Reactor.Step

  def run(arguments, context, options) do
    case make_api_call(arguments.url) do
      {:ok, result} -> {:ok, result}
      {:error, reason} -> {:error, reason}
    end
  end

  def compensate(reason, arguments, context, options) do
    case reason do
      # Retry on network errors
      %HTTPoison.Error{reason: :timeout} -> :retry
      %HTTPoison.Error{reason: :econnrefused} -> :retry

      # Continue with error for other failures
      _other -> :ok
    end
  end
end
```

### Undo Operations
Rollback successful operations when later steps fail:

```elixir
defmodule MyApp.Steps.CreateResource do
  use Reactor.Step

  def run(arguments, context, options) do
    {:ok, create_resource(arguments)}
  end

  def undo(resource, arguments, context, options) do
    case delete_resource(resource) do
      :ok -> :ok
      {:error, :not_found} -> :ok  # Already deleted
      {:error, reason} -> {:error, reason}
    end
  end
end
```

## Async vs Sync Execution

### Async Steps (Default)
Steps run asynchronously by default:

```elixir
step :async_operation do
  # Runs asynchronously
  run fn _, _ -> {:ok, "result"} end
end
```

### Sync Steps
Force synchronous execution:

```elixir
step :sync_operation do
  async? false
  run fn _, _ -> {:ok, "result"} end
end
```

### Conditional Async
Make async behavior conditional:

```elixir
step :conditional_async do
  async? fn options -> options[:force_sync] != true end
  run fn _, _ -> {:ok, "result"} end
end
```

## Running Reactors

### Basic Execution

```elixir
# Run with inputs
{:ok, result} = Reactor.run(MyApp.UserRegistrationReactor,
  email: "user@example.com",
  password: "secret123",
  plan_name: "premium"
)

# Run with options
{:ok, result} = Reactor.run(reactor, inputs, context,
  async?: false,
  max_concurrency: 10
)
```

### Halting and Resuming

```elixir
# Step returns {:halt, reason}
{:halted, reactor_state} = Reactor.run(MyReactor, inputs)

# Resume later
{:ok, result} = Reactor.run(reactor_state, %{}, %{})
```

## Middleware

Add cross-cutting concerns with middleware:

```elixir
defmodule MyApp.LoggingMiddleware do
  use Reactor.Middleware

  def init(context) do
    Logger.info("Reactor starting")
    {:ok, context}
  end

  def complete(result, context) do
    Logger.info("Reactor completed successfully")
    {:ok, result}
  end

  def error(errors, context) do
    Logger.error("Reactor failed: #{inspect(errors)}")
    :ok
  end

  def event({:run_start, args}, step, context) do
    Logger.debug("Step #{step.name} starting with #{inspect(args)}")
  end

  def event({:run_complete, result}, step, context) do
    Logger.debug("Step #{step.name} completed with #{inspect(result)}")
  end
end

# Add to reactor
defmodule MyApp.ReactorWithMiddleware do
  use Reactor

  middlewares do
    middleware MyApp.LoggingMiddleware
    middleware Reactor.Middleware.Telemetry
  end

  # Steps...
end
```

## Guards and Conditions

Control step execution with guards:

```elixir
step :conditional_step do
  argument :user, result(:create_user)

  # Only run if guard passes
  guard &(&1.user.active?)

  run fn %{user: user}, _ ->
    {:ok, "Processing active user: #{user.name}"}
  end
end
```

Use where clauses for complex conditions:

```elixir
step :premium_feature do
  argument :user, result(:create_user)

  where fn %{user: user} ->
    user.plan == :premium and user.active?
  end

  run fn %{user: user}, _ ->
    enable_premium_features(user)
  end
end
```

## Wait For Dependencies

Explicit dependencies without data flow:

```elixir
step :send_notification do
  argument :user, result(:create_user)

  # Wait for email verification to complete
  wait_for :verify_email

  run fn %{user: user}, _ ->
    send_welcome_notification(user)
  end
end
```

## Best Practices

### Error Handling
- Implement `compensate/4` for retryable errors
- Implement `undo/4` for operations that need rollback
- Use specific error types to guide compensation logic
- Log errors appropriately for debugging
- If available, do work transactionally if possible
- If building an Ash application, use `Ash.Reactor` steps

### Performance
- Use `async? false` sparingly - only when order matters
- Set appropriate `batch_size` for map operations
- Use `strict_ordering? false` in map steps when order doesn't matter
- Consider `max_concurrency` limits for resource-constrained operations

### Code Organization
- Create reusable step modules for common operations
- Use compose steps to break complex workflows into smaller reactors
- Group related steps with shared setup/teardown
- Use meaningful names for all components

### Testing
- Test steps in isolation using the step module directly
- Test reactors with various input combinations
- Test error scenarios and compensation logic
- Use `async? false` in tests for deterministic execution

### Debugging
- Use debug steps to log intermediate values
- Add telemetry middleware for observability
- Use descriptive names and descriptions for steps
- Test with `async? false` to simplify debugging

## Advanced Features

### Context Usage
Pass data through the reactor context:

```elixir
# In a step
def run(arguments, context, options) do
  user = context[:current_user]
  # Use context data
end

# Run with context
Reactor.run(reactor, inputs, %{current_user: user})
```

### Transform Functions
Apply transformations to inputs and arguments:

```elixir
input :birth_date do
  transform &Date.from_iso8601!/1
end

step :calculate_age do
  argument :birth_date do
    source input(:birth_date)
    transform fn date -> Date.diff(Date.utc_today(), date) end
  end
end
```

### Retries and Limits
Control retry behavior:

```elixir
step :api_call do
  max_retries 3

  run fn args, _ ->
    # May fail and retry up to 3 times
  end

  compensate fn reason, _, _, _ ->
    case reason do
      %HTTPError{status: 503} -> :retry
      _ -> :ok
    end
  end
end
```

### Reactor Composition
Build larger workflows from smaller ones:

```elixir
defmodule MyApp.MainWorkflow do
  use Reactor

  input :user_data

  # Run sub-workflow
  compose :user_setup, MyApp.UserSetupReactor do
    argument :data, input(:user_data)
  end

  # Continue with more steps
  step :finalize do
    argument :setup_result, result(:user_setup)
    # Finalization logic
  end
end
```
