Skip to content

Commit b61b4ab

Browse files
committed
Improve StartEvent processing
1 parent e593df0 commit b61b4ab

File tree

6 files changed

+246
-170
lines changed

6 files changed

+246
-170
lines changed

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/state/pseudo/UndoState.kt

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ru.nsk.kstatemachine.state.IState
66
import ru.nsk.kstatemachine.transition.EventAndArgument
77
import ru.nsk.kstatemachine.transition.TransitionParams
88

9-
private data class StateAndEvent(val state: IState, val eventAndArgument: EventAndArgument<*>)
9+
private data class StateAndEvent(val states: Set<IState>, val eventAndArgument: EventAndArgument<*>)
1010

1111
internal class UndoState : BasePseudoState("undoState") {
1212
private val stack = mutableListOf<StateAndEvent>()
@@ -15,8 +15,9 @@ internal class UndoState : BasePseudoState("undoState") {
1515
super.recursiveAfterTransitionComplete(transitionParams)
1616
if (transitionParams.event !is WrappedEvent) { // do not record self-made transition
1717
// check target-less transition
18-
val targetState = transitionParams.direction.targetState ?: transitionParams.transition.sourceState
19-
stack += StateAndEvent(targetState, EventAndArgument(transitionParams.event, transitionParams.argument))
18+
val targetStates = transitionParams.direction.targetStates.takeIf { it.isNotEmpty() }
19+
?: setOf(transitionParams.transition.sourceState)
20+
stack += StateAndEvent(targetStates, EventAndArgument(transitionParams.event, transitionParams.argument))
2021
}
2122
}
2223

@@ -31,11 +32,11 @@ internal class UndoState : BasePseudoState("undoState") {
3132
WrappedEvent(UndoEvent, null)
3233
}
3334

34-
fun popState(): IState? = if (stack.size >= 2) {
35+
fun popState(): Set<IState> = if (stack.size >= 2) {
3536
stack.removeLast()
36-
stack.last().state
37+
stack.last().states
3738
} else {
38-
null
39+
emptySet()
3940
}
4041

4142
override suspend fun onStopped(): Unit = stack.clear()

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/StateMachine.kt

+9
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,23 @@ interface StateMachine : State {
133133
}
134134

135135
fun interface IgnoredEventHandler {
136+
/**
137+
* It is up to user to throw exception from this method or not
138+
*/
136139
suspend fun onIgnoredEvent(eventAndArgument: EventAndArgument<*>)
137140
}
138141

139142
fun interface PendingEventHandler {
143+
/**
144+
* It is up to user to throw exception from this method or not
145+
*/
140146
suspend fun onPendingEvent(eventAndArgument: EventAndArgument<*>)
141147
}
142148

143149
fun interface ListenerExceptionHandler {
150+
/**
151+
* It is up to user to throw exception from this method or not
152+
*/
144153
suspend fun onException(exception: Exception)
145154
}
146155

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/StateMachineImpl.kt

+63-28
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,15 @@ internal class StateMachineImpl(
132132
private suspend fun doStartFrom(event: StartEvent, argument: Any?): Unit =
133133
coroutineAbstraction.withContext {
134134
checkBeforeRunMachine()
135-
// fixme loosing this params (but similary (not same target) will be recreated on transition)
135+
// fixme loosing this params (but similar (not same target) will be recreated on transition)
136136
val eventAndArgument = EventAndArgument(event, argument)
137137
eventProcessingScope {
138-
runCheckingExceptions {
138+
val step1Result = runCheckingExceptions {
139139
val transitionParams = makeStartTransitionParams(event, this, event.startState, argument)
140140
runMachine(transitionParams)
141-
doProcessEvent(eventAndArgument)
141+
processStep1(eventAndArgument)
142142
}
143+
processStep2(eventAndArgument, step1Result)
143144
}
144145
}
145146

@@ -154,7 +155,7 @@ internal class StateMachineImpl(
154155
requireInitialState()
155156
}
156157

157-
/** To be called only from [runCheckingExceptions] */
158+
/** Should be called only from [runCheckingExceptions] */
158159
private suspend fun runMachine(transitionParams: TransitionParams<*>) {
159160
_isRunning = true
160161
_hasProcessedEvents = false
@@ -163,7 +164,7 @@ internal class StateMachineImpl(
163164
doEnter(transitionParams)
164165
}
165166

166-
/** To be called only from [runCheckingExceptions] */
167+
/** Should be called only from [runCheckingExceptions] */
167168
private suspend fun doStop() {
168169
_isRunning = false
169170
recursiveStop()
@@ -202,33 +203,52 @@ internal class StateMachineImpl(
202203
}
203204

204205
private suspend fun process(eventAndArgument: EventAndArgument<*>): ProcessingResult {
205-
if (eventAndArgument.event !is StartEvent)
206-
_hasProcessedEvents = true
206+
val step1Result = runCheckingExceptions {
207+
processStep1(eventAndArgument)
208+
}
209+
return processStep2(eventAndArgument, step1Result)
210+
}
207211

208-
_eventRecorder?.onProcessEvent(eventAndArgument) // should be done before wrapping to record not wrapped event
212+
/**
213+
* Should be called only from [runCheckingExceptions]
214+
*/
215+
private suspend fun processStep1(eventAndArgument: EventAndArgument<*>): Step1Result {
216+
_eventRecorder?.onProcessEvent(eventAndArgument) // should be called with not wrapped event
209217

210218
val wrappedEventAndArgument = eventAndArgument.wrap()
219+
val eventProcessed = when (val event = wrappedEventAndArgument.event) {
220+
is StopEvent -> {
221+
doStop()
222+
true
223+
}
211224

212-
val eventProcessed = runCheckingExceptions {
213-
when (val event = wrappedEventAndArgument.event) {
214-
is StopEvent -> {
215-
doStop()
216-
true
217-
}
218-
is DestroyEvent -> {
219-
if (event.stop && isRunning) doStop()
220-
doDestroy()
221-
true
222-
}
223-
else -> doProcessEvent(wrappedEventAndArgument)
225+
is DestroyEvent -> {
226+
if (event.stop && isRunning) doStop()
227+
doDestroy()
228+
true
224229
}
230+
231+
else -> doProcessEvent(wrappedEventAndArgument)
225232
}
233+
return Step1Result(eventProcessed, wrappedEventAndArgument)
234+
}
226235

227-
if (!eventProcessed) {
228-
log { "$this ignored ${wrappedEventAndArgument.event::class.simpleName}" }
229-
ignoredEventHandler.onIgnoredEvent(wrappedEventAndArgument)
236+
/**
237+
* Possible exceptions from this step should reach the user (caller)
238+
*/
239+
private suspend fun processStep2(
240+
eventAndArgument: EventAndArgument<*>,
241+
step1Result: Step1Result,
242+
): ProcessingResult {
243+
return if (step1Result.eventProcessed) {
244+
if (eventAndArgument.event !is StartEvent)
245+
_hasProcessedEvents = true
246+
ProcessingResult.PROCESSED
247+
} else {
248+
log { "$this ignored ${step1Result.wrappedEventAndArgument.event::class.simpleName}" }
249+
ignoredEventHandler.onIgnoredEvent(step1Result.wrappedEventAndArgument)
250+
ProcessingResult.IGNORED
230251
}
231-
return if (eventProcessed) ProcessingResult.PROCESSED else ProcessingResult.IGNORED
232252
}
233253

234254
/**
@@ -266,7 +286,11 @@ internal class StateMachineImpl(
266286
}
267287

268288
/**
269-
* Runs block of code that triggers notification listeners
289+
* Runs block of code that internally triggers notification listeners.
290+
*
291+
* As listeners exceptions are delayed and may be rethrown to a caller (user) and this should not cause machine
292+
* destruction.
293+
* Watch [runCheckingExceptions] blocks are not nested it is not supported and wrong.
270294
*/
271295
private suspend fun <R> runCheckingExceptions(block: suspend () -> R): R {
272296
val result: R
@@ -284,6 +308,9 @@ internal class StateMachineImpl(
284308
return result
285309
}
286310

311+
/**
312+
* @return true if event was processed (transition was performed), false if it was ignored
313+
*/
287314
private suspend fun <E : Event> doProcessEvent(eventAndArgument: EventAndArgument<E>): Boolean {
288315
val (event, argument) = eventAndArgument
289316
if (isFinished) {
@@ -326,15 +353,15 @@ internal class StateMachineImpl(
326353
* Starts machine if it is inner state of another one machine
327354
*/
328355
override suspend fun doEnter(transitionParams: TransitionParams<*>) {
329-
if (!isRunning) startBlocking() else super.doEnter(transitionParams)
356+
if (!isRunning) start() else super.doEnter(transitionParams)
330357
}
331358

332359
override suspend fun cleanup() {
333360
_machineListeners.clear()
334361
super.cleanup()
335362
}
336363

337-
/** To be called only from [runCheckingExceptions] */
364+
/** Should be called only from [runCheckingExceptions] */
338365
private suspend fun doDestroy() {
339366
_isDestroyed = true
340367
machineNotify { onDestroyed() }
@@ -345,6 +372,9 @@ internal class StateMachineImpl(
345372

346373
internal fun StateMachine.checkNotDestroyed() = check(!isDestroyed) { "$this is already destroyed" }
347374

375+
/**
376+
* Method should be used for running code that sends notifications for outer world (calls listeners)
377+
*/
348378
internal suspend inline fun InternalStateMachine.runDelayingException(crossinline block: suspend () -> Unit) =
349379
try {
350380
block()
@@ -376,4 +406,9 @@ internal suspend inline fun <reified E : StartEvent> makeStartTransitionParams(
376406
}
377407

378408
private fun StateMachine.checkPropertyNotMutedOnRunningMachine(propertyType: KClass<*>) =
379-
check(!isRunning) { "Can not change ${propertyType.simpleName} after state machine started" }
409+
check(!isRunning) { "Can not change ${propertyType.simpleName} after state machine started" }
410+
411+
private data class Step1Result(
412+
val eventProcessed: Boolean,
413+
val wrappedEventAndArgument: EventAndArgument<*>,
414+
)

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/transition/TransitionDirection.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private suspend fun EventAndArgument<*>.recursiveResolveTargetState(targetState:
9797
// as initialPseudoState resolution is already done inside RedirectPseudoState::resolveTargetState()
9898
is RedirectPseudoState -> return targetState.resolveTargetState(DefaultPolicy(this)).targetState
9999
is HistoryState -> targetState.storedState
100-
is UndoState -> targetState.popState()
100+
is UndoState -> targetState.popState().firstOrNull() // fixme this is a bug, should use all set items, add test for undo multi-target transition
101101
else -> targetState
102102
}
103103
// when target state calculated we need to check if its entry will trigger another redirection

0 commit comments

Comments
 (0)