[statemachine] initial steps for declarative transitions
# Conflicts: # codex/utils/asyncstatemachine.nim
This commit is contained in:
parent
2035568b88
commit
127adc0b8a
|
@ -2,36 +2,55 @@ import pkg/questionable
|
|||
import pkg/chronos
|
||||
import pkg/upraises
|
||||
|
||||
push: {.upraises:[].}
|
||||
|
||||
type
|
||||
Machine* = ref object of RootObj
|
||||
# TODO: context?
|
||||
state: State
|
||||
running: Future[void]
|
||||
scheduled: AsyncQueue[Event]
|
||||
scheduling: Future[void]
|
||||
transitions: seq[Transition]
|
||||
State* = ref object of RootObj
|
||||
Event* = proc(state: State): ?State {.gcsafe, upraises:[].}
|
||||
Event = proc(state: State): ?State {.gcsafe, upraises:[].}
|
||||
TransitionCondition* = proc(machine: Machine, state: State): bool {.gcsafe, upraises:[].}
|
||||
Transition* = object of RootObj
|
||||
prevState: State
|
||||
nextState: State
|
||||
condition: TransitionCondition
|
||||
TransitionProperty*[T] = ref object of RootObj
|
||||
machine: Machine
|
||||
value*: T
|
||||
|
||||
proc new*(T: type Transition,
|
||||
prev, next: State,
|
||||
condition: TransitionCondition): T =
|
||||
Transition(prevState: prev, nextState: next, condition: condition)
|
||||
|
||||
proc newTransitionProperty*[T](self: Machine,
|
||||
initialValue: T): TransitionProperty[T] =
|
||||
TransitionProperty[T](machine: self, value: initialValue)
|
||||
|
||||
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) =
|
||||
try:
|
||||
machine.scheduled.putNoWait(event)
|
||||
except AsyncQueueFullError:
|
||||
raiseAssert "unlimited queue is full?!"
|
||||
proc setValue*[T](prop: TransitionProperty[T], value: T) =
|
||||
prop.value = value
|
||||
let machine = prop.machine
|
||||
for transition in machine.transitions:
|
||||
if transition.condition(machine, machine.state) and
|
||||
machine.state == transition.prevState:
|
||||
machine.schedule(Event.transition(machine.state, transition.nextState))
|
||||
|
||||
# TODO: provide context instead of machine?
|
||||
method run*(state: State, machine: Machine): Future[?State] {.base, upraises:[].} =
|
||||
proc schedule*(machine: Machine, event: Event) =
|
||||
machine.scheduled.putNoWait(event)
|
||||
|
||||
method run*(state: State): Future[?State] {.base, upraises:[].} =
|
||||
discard
|
||||
|
||||
proc run(machine: Machine, state: State) {.async.} =
|
||||
try:
|
||||
if next =? await state.run(machine):
|
||||
if next =? await state.run():
|
||||
machine.schedule(Event.transition(state, next))
|
||||
except CancelledError:
|
||||
discard
|
||||
|
@ -50,11 +69,12 @@ proc scheduler(machine: Machine) {.async.} =
|
|||
discard
|
||||
|
||||
proc start*(machine: Machine, initialState: State) =
|
||||
if machine.scheduled.isNil:
|
||||
machine.scheduled = newAsyncQueue[Event]()
|
||||
machine.scheduling = machine.scheduler()
|
||||
machine.schedule(Event.transition(machine.state, initialState))
|
||||
|
||||
proc stop*(machine: Machine) =
|
||||
machine.scheduling.cancel()
|
||||
machine.running.cancel()
|
||||
|
||||
proc new*(T: type Machine, transitions: seq[Transition]): T =
|
||||
T(scheduled: newAsyncQueue[Event](), transitions: transitions)
|
||||
|
|
|
@ -6,6 +6,8 @@ import codex/utils/asyncstatemachine
|
|||
import ../helpers/eventually
|
||||
|
||||
type
|
||||
MyMachine = ref object of Machine
|
||||
slotFilled: TransitionProperty[bool]
|
||||
State1 = ref object of State
|
||||
State2 = ref object of State
|
||||
State3 = ref object of State
|
||||
|
@ -37,8 +39,8 @@ method onMoveToNextStateEvent(state: State3): ?State =
|
|||
some State(State1.new())
|
||||
|
||||
suite "async state machines":
|
||||
var machine: Machine
|
||||
var state1, state2: State
|
||||
var machine: MyMachine
|
||||
var state1, state2, state3: State
|
||||
|
||||
proc moveToNextStateEvent(state: State): ?State =
|
||||
state.onMoveToNextStateEvent()
|
||||
|
@ -46,9 +48,24 @@ suite "async state machines":
|
|||
setup:
|
||||
runs = [0, 0, 0]
|
||||
cancellations = [0, 0, 0]
|
||||
machine = Machine.new()
|
||||
state1 = State1.new()
|
||||
state2 = State2.new()
|
||||
state3 = State3.new()
|
||||
machine = MyMachine.new(@[
|
||||
Transition.new(
|
||||
state3,
|
||||
state1,
|
||||
proc(m: Machine, s: State): bool =
|
||||
MyMachine(m).slotFilled.value
|
||||
)
|
||||
])
|
||||
machine.slotFilled = machine.newTransitionProperty(false)
|
||||
|
||||
# EXAMPLE USAGE ONLY -- can be removed from tests
|
||||
# should represent a typical external event callback, ie event called via
|
||||
# subscription
|
||||
proc externalEventCallback() =# would take params (rid: RequestId, slotIdx: UInt256) =
|
||||
machine.slotFilled.setValue(true)
|
||||
|
||||
test "should call run on start state":
|
||||
machine.start(state1)
|
||||
|
@ -82,3 +99,12 @@ suite "async state machines":
|
|||
await sleepAsync(1.millis)
|
||||
check runs == [0, 1, 0]
|
||||
check cancellations == [0, 1, 0]
|
||||
|
||||
test "can transition to state without next state":
|
||||
machine.start(state3)
|
||||
check eventually runs == [0, 0, 1]
|
||||
|
||||
test "moves states based on declared transitions and conditions":
|
||||
machine.start(state3)
|
||||
machine.slotFilled.setValue(true)
|
||||
check eventually runs == [1, 0, 1]
|
||||
|
|
Loading…
Reference in New Issue