diff --git a/codex/utils/statemachine.nim b/codex/utils/statemachine.nim index 9b7a4309..51c3ac15 100644 --- a/codex/utils/statemachine.nim +++ b/codex/utils/statemachine.nim @@ -1,3 +1,5 @@ +import std/typetraits +import pkg/chronicles import pkg/questionable import pkg/chronos import ./optionalcast @@ -62,6 +64,9 @@ type State* = ref object of RootObj context: ?StateMachine +method `$`*(state: State): string {.base.} = + (typeof state).name + method enter(state: State) {.base.} = discard @@ -88,6 +93,8 @@ proc switch*(oldState, newState: State) = type AsyncState* = ref object of State + activeTransition: ?Future[void] + StateMachineAsync* = ref object of StateMachine method enterAsync(state: AsyncState) {.base, async.} = discard @@ -100,3 +107,26 @@ method enter(state: AsyncState) = method exit(state: AsyncState) = asyncSpawn state.exitAsync() + +proc switchAsync*(machine: StateMachineAsync, newState: AsyncState) {.async.} = + if state =? (machine.state as AsyncState): + trace "Switching sales state", `from`=state, to=newState + debugEcho "switching from ", state, " to ", newState + if activeTransition =? state.activeTransition and + not activeTransition.completed: + await activeTransition.cancelAndWait() + # should wait for exit before switch. could add a transition option during + # switch if we don't need to wait + await state.exitAsync() + state.context = none StateMachine + else: + trace "Switching sales state", `from`="no state", to=newState + debugEcho "switching from no state to ", newState + + machine.state = some State(newState) + newState.context = some StateMachine(machine) + newState.activeTransition = some newState.enterAsync() + +proc switchAsync*(oldState, newState: AsyncState) {.async.} = + if context =? oldState.context: + await StateMachineAsync(context).switchAsync(newState) diff --git a/tests/codex/testutils.nim b/tests/codex/testutils.nim index 3b3b7c24..e174d07f 100644 --- a/tests/codex/testutils.nim +++ b/tests/codex/testutils.nim @@ -1,4 +1,5 @@ import ./utils/teststatemachine +import ./utils/teststatemachineasync import ./utils/testoptionalcast import ./utils/testkeyutils diff --git a/tests/codex/utils/teststatemachineasync.nim b/tests/codex/utils/teststatemachineasync.nim new file mode 100644 index 00000000..38431b41 --- /dev/null +++ b/tests/codex/utils/teststatemachineasync.nim @@ -0,0 +1,30 @@ +import pkg/asynctest +import pkg/chronos +import pkg/questionable +import codex/utils/statemachine + +type + AsyncMachine = ref object of StateMachineAsync + LongRunningStart = ref object of AsyncState + LongRunningFinish = ref object of AsyncState + LongRunningError = ref object of AsyncState + Callback = proc(): Future[void] {.gcsafe.} + +proc triggerIn(time: Duration, cb: Callback) {.async.} = + await sleepAsync(time) + await cb() + +method enterAsync(state: LongRunningStart) {.async.} = + proc cb() {.async.} = + await state.switchAsync(LongRunningFinish()) + asyncSpawn triggerIn(500.milliseconds, cb) + await sleepAsync(1.seconds) + await state.switchAsync(LongRunningError()) + +suite "async state machines": + + test "can cancel a state": + let am = AsyncMachine() + await am.switchAsync(LongRunningStart()) + await sleepAsync(2.seconds) + check (am.state as LongRunningFinish).isSome