factos/simulate

Deterministic execution of Factos domain scenarios.

The default simulator runs the model, decision-context, and codec semantics entirely in memory. Interactive backend adapters can additionally run application subscriptions against a real projection database.

Every function returns the immutable simulation so a complete scenario can be written as one pipeline:

let simulation =
  simulate.new(model, with: [
    UsernameReserved(username: "renata"),
  ])
  |> simulate.dispatch(
    decision_context: username_context("renata"),
    command: RegisterUser(username: "renata"),
  )
  |> simulate.tap(fn(simulation) {
    assert simulate.events(simulation)
      == Ok([
        UsernameReserved(username: "renata"),
        UserRegistered(username: "renata"),
      ])
  })

assert simulate.errors(simulation) == []

Domain, codec, subscription, and subscription-store failures are accumulated for errors. A rejected batch does not enter simulated history, so later stages can continue the scenario.

Types

An immutable event history and accumulated errors for one domain model.

pub opaque type Simulation(event, command, state, domain_error, subscription_error, store_error)

Values

pub fn dispatch(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
  decision_context decision_context: factos.DecisionContext,
  command command: command,
) -> Simulation(
  event,
  command,
  state,
  domain_error,
  subscription_error,
  store_error,
)

Read the decision context, decide the command, and append accepted events.

Domain, schema, and payload errors are accumulated without stopping the pipeline.

let simulation =
  simulation
  |> simulate.dispatch(
    decision_context: username_context("renata"),
    command: RegisterUser(username: "renata"),
  )
pub fn errors(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
) -> List(
  factos.Error(
    domain_error,
    subscription_error,
    store_error,
    json.DecodeError,
  ),
)

Return collected Factos errors in scenario order.

assert simulate.errors(simulation) == [
  factos.DomainError(UsernameAlreadyTaken),
]
pub fn events(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
) -> Result(
  List(event),
  factos.Error(
    domain_error,
    subscription_error,
    store_error,
    json.DecodeError,
  ),
)

Decode and return the complete event history in append order.

assert simulate.events(simulation)
  == Ok([UserRegistered(username: "renata")])
pub fn new(
  model: factos.Model(command, state, event, domain_error),
  with events: List(event),
) -> Simulation(event, command, state, domain_error, Nil, Nil)

Create a simulation with an initial event history.

Initial events receive deterministic ids and positions. The model’s decision functions and codec are reused for every dispatch.

let simulation =
  simulate.new(user_model, with: [
    UserRegistered(username: "renata"),
  ])
pub fn tap(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
  run: fn(
    Simulation(
      event,
      command,
      state,
      domain_error,
      subscription_error,
      store_error,
    ),
  ) -> a,
) -> Simulation(
  event,
  command,
  state,
  domain_error,
  subscription_error,
  store_error,
)

Run a callback with the current simulation, then return it unchanged.

Use events and errors to inspect the simulation inside the callback.

simulation
|> simulate.tap(fn(simulation) {
  assert simulate.events(simulation)
    == Ok([UserRegistered(username: "renata")])
})
Search Document