nim-dagger/codex/utils/asyncstatemachine.nim

94 lines
2.5 KiB
Nim

import pkg/questionable
import pkg/chronos
import pkg/chronicles
import pkg/upraises
push: {.upraises:[].}
type
Machine* = ref object of RootObj
state: State
running: Future[void]
scheduled: AsyncQueue[Event]
scheduling: Future[void]
started: bool
State* = ref object of RootObj
Event* = proc(state: State): ?State {.gcsafe, upraises:[].}
logScope:
topics = "statemachine"
method `$`*(state: State): string {.base.} =
raiseAssert "not implemented"
proc transition(_: type Event, previous, next: State): Event =
return proc (state: State): ?State =
if state == previous:
return some next
proc schedule*(machine: Machine, event: Event) =
if not machine.started:
return
try:
machine.scheduled.putNoWait(event)
except AsyncQueueFullError:
raiseAssert "unlimited queue is full?!"
method run*(state: State, machine: Machine): Future[?State] {.base, async.} =
discard
method onError*(state: State, error: ref CatchableError): ?State {.base.} =
raise (ref Defect)(msg: "error in state machine: " & error.msg, parent: error)
proc onError(machine: Machine, error: ref CatchableError): Event =
return proc (state: State): ?State =
state.onError(error)
proc run(machine: Machine, state: State) {.async.} =
try:
if next =? await state.run(machine):
machine.schedule(Event.transition(state, next))
except CancelledError:
discard
proc scheduler(machine: Machine) {.async.} =
proc onRunComplete(udata: pointer) {.gcsafe.} =
var fut = cast[FutureBase](udata)
if fut.failed():
machine.schedule(machine.onError(fut.error))
try:
while true:
let event = await machine.scheduled.get()
if next =? event(machine.state):
if not machine.running.isNil:
await machine.running.cancelAndWait()
machine.state = next
debug "enter state", state = machine.state
machine.running = machine.run(machine.state)
machine.running.addCallback(onRunComplete)
except CancelledError:
discard
proc start*(machine: Machine, initialState: State) =
if machine.started:
return
if machine.scheduled.isNil:
machine.scheduled = newAsyncQueue[Event]()
machine.scheduling = machine.scheduler()
machine.started = true
machine.schedule(Event.transition(machine.state, initialState))
proc stop*(machine: Machine) =
if not machine.started:
return
if not machine.scheduling.isNil:
machine.scheduling.cancel()
if not machine.running.isNil:
machine.running.cancel()
machine.started = false