Running FSMs

FSMRuntime[Id, S, E] is the unified execution surface:

Simple runtime

machine.start(initialState) creates an in-memory runtime. The FSM stops when the scope closes. send returns a transition outcome (Goto, Stay, or Stop). Missing transitions raise InvalidTransitionError.

{
  enum MyState derives Finite:
    case Initial, Running, Done

  enum MyEvent derives Finite:
    case Start, Finish

  import MyState.*, MyEvent.*

  val machine = Machine(
    assembly[MyState, MyEvent](
      Initial via Start to Running,
      Running via Finish to Done,
    )
  )

  Mermoid.diagram(
    MermaidVisualizer.stateDiagram(machine, Some(Initial)),
    DocsDiagrams.diagramConfig,
  )
}
StartFinish
{
  enum MyState derives Finite:
    case Initial, Running, Done

  enum MyEvent derives Finite:
    case Start, Finish

  import MyState.*, MyEvent.*

  val machine = Machine(
    assembly[MyState, MyEvent](
      Initial via Start to Running,
      Running via Finish to Done,
    )
  )

  ZIO.scoped {
    for
      fsm     <- machine.start(Initial)
      outcome <- fsm.send(Start)
      state   <- fsm.currentState
      hist    <- fsm.history
    yield (outcome.result.toString, state.toString, hist.length)
  }.asDoc
}
(Goto(Running),Running,1)
{
  enum MyState derives Finite:
    case Initial, Running, Done

  enum MyEvent derives Finite:
    case Start, Finish

  import MyState.*, MyEvent.*

  val machine = Machine(
    assembly[MyState, MyEvent](
      Initial via Start to Running,
      Running via Finish to Done,
    )
  )

  ZIO.scoped {
    for
      fsm    <- machine.start(Initial)
      failed <- fsm.send(Finish).either
    yield failed
  }.asDoc
}
Left(InvalidTransitionError(Initial,Finish,No transition defined))

Persistent runtime

FSMRuntime(id, machine, initial) needs three environment services:

ServiceRole
EventStore[Id, S, E]Events and snapshots
TimeoutStrategy[Id]Fiber or durable timeouts
LockingStrategy[Id]Optimistic or distributed locking
{
  enum OrderState derives Finite:
    case Pending, Paid, Shipped

  enum OrderEvent derives Finite:
    case Pay, Ship

  import OrderState.*, OrderEvent.*

  val orderMachine = Machine(
    assembly[OrderState, OrderEvent](
      Pending via Pay to Paid,
      Paid via Ship to Shipped,
    )
  )

  val orderId: OrderId = "order-1"

  ZIO
    .scoped {
      for
        fsm   <- FSMRuntime(orderId, orderMachine, Pending)
        _     <- fsm.send(Pay)
        state <- fsm.currentState
      yield state
    }
    .provide(
      InMemoryEventStore.layer[OrderId, OrderState, OrderEvent],
      TimeoutStrategy.fiber[OrderId],
      LockingStrategy.optimistic[OrderId],
    )
    .asDoc
}
Paid

Next: Persistence for recover-on-construct and snapshots.