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:

simulate.new(model)
|> simulate.given([UsernameReserved(username: "renata")])
|> simulate.dispatch(
  decision_context: username_context("renata"),
  command: RegisterUser(username: "renata"),
)
|> simulate.assert_events([
  UsernameReserved(username: "renata"),
  UserRegistered(username: "renata"),
])
|> simulate.assert_errors([])

Domain, codec, subscription, and subscription-store failures are accumulated for assert_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 assert_errors(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
  errors: List(
    factos.Error(
      domain_error,
      subscription_error,
      store_error,
      json.DecodeError,
    ),
  ),
) -> Simulation(
  event,
  command,
  state,
  domain_error,
  subscription_error,
  store_error,
)

Assert collected Factos errors in scenario order.

simulation
|> simulate.assert_errors([
  factos.DomainError(UsernameAlreadyTaken),
])
pub fn assert_events(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
  events: List(event),
) -> Simulation(
  event,
  command,
  state,
  domain_error,
  subscription_error,
  store_error,
)

Assert the complete decoded event history in append order.

A mismatch fails immediately with Gleam’s structural assertion diff. Codec failures are accumulated for assert_errors.

simulation
|> simulate.assert_events([
  UserRegistered(username: "renata"),
])
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 given(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
  events: List(event),
) -> Simulation(
  event,
  command,
  state,
  domain_error,
  subscription_error,
  store_error,
)

Seed facts that were accepted before the simulated scenario.

Seeded events receive deterministic ids and positions. A configured subscription runner projects the complete seed batch transactionally.

let simulation =
  simulate.new(user_model)
  |> simulate.given([UserRegistered(username: "renata")])
pub fn new(
  model: factos.Model(command, state, event, domain_error),
) -> Simulation(event, command, state, domain_error, Nil, Nil)

Create an empty simulation from a store-independent Factos model.

The model’s decision functions and codec are reused for every dispatch.

let simulation = simulate.new(user_model)
pub fn to_string(
  simulation: Simulation(
    event,
    command,
    state,
    domain_error,
    subscription_error,
    store_error,
  ),
) -> String

Render decoded domain events, their descriptors, and accumulated errors.

The output is intended for snapshot tools such as Birdie. Backend recording ids and positions, model functions, and the subscription runner are omitted because they are simulator plumbing rather than domain behavior. A codec failure is rendered separately without changing the simulation.

Search Document