Skip to content

Commit 83bcb79

Browse files
authored
Merge pull request #91 from nsk90/multiple-targets
Allow specify multiple target states for transitions
2 parents 3849beb + 24b91ad commit 83bcb79

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+677
-82
lines changed

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,8 @@ createStateMachine(scope, childMode = ChildMode.PARALLEL) {
577577
```
578578

579579
Currently, there is no way to process multiple transitions for one event by using parallel states, only one transition
580-
may be triggered for each event.
580+
may be triggered for each event. Such behaviour might be easily emulated using separated events for each
581+
parallel branch (region).
581582

582583
## Pseudo states
583584

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/BaseStateImpl.kt

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,13 @@ open class BaseStateImpl(override val name: String?, override val childMode: Chi
178178
}
179179

180180
override suspend fun recursiveEnterStatePath(
181-
path: MutableList<InternalState>,
181+
path: ListIterator<InternalState>,
182182
transitionParams: TransitionParams<*>
183183
) {
184-
if (path.isEmpty()) {
184+
if (!path.hasPrevious()) {
185185
recursiveEnterInitialStates(transitionParams)
186186
} else {
187-
val state = path.removeLast()
187+
val state = path.previous()
188188
when (childMode) {
189189
EXCLUSIVE -> {
190190
setCurrentState(state, transitionParams)
@@ -204,6 +204,40 @@ open class BaseStateImpl(override val name: String?, override val childMode: Chi
204204
}
205205
}
206206

207+
override suspend fun recursiveEnterStatePath(
208+
pathHead: PathNode,
209+
transitionParams: TransitionParams<*>
210+
) {
211+
if (pathHead.children.isEmpty()) {
212+
recursiveEnterInitialStates(transitionParams)
213+
} else {
214+
when (childMode) {
215+
EXCLUSIVE -> {
216+
val exclusivePath = pathHead.children.single()
217+
val state = exclusivePath.state as InternalState
218+
setCurrentState(state, transitionParams)
219+
if (state !is StateMachine) // inner state machine manages its internal state by its own
220+
state.recursiveEnterStatePath(exclusivePath, transitionParams)
221+
}
222+
PARALLEL -> data.states.forEach { childState ->
223+
val paths = pathHead.children
224+
handleStateEntry(childState, transitionParams)
225+
if (childState !is StateMachine) { // inner state machine manages its internal state by its own
226+
val regionPath = paths.find { it.state === childState }
227+
if (regionPath != null) {
228+
childState.recursiveEnterStatePath(
229+
regionPath,
230+
transitionParams.repackForRegion(regionPath.requireFirstLeaf().state as IState)
231+
)
232+
} else {
233+
childState.recursiveEnterInitialStates(transitionParams)
234+
}
235+
}
236+
}
237+
}
238+
}
239+
}
240+
207241
override suspend fun recursiveExit(transitionParams: TransitionParams<*>) {
208242
getCurrentStates().forEachState { it.recursiveExit(transitionParams) }
209243
doExit(transitionParams)
@@ -277,15 +311,38 @@ open class BaseStateImpl(override val name: String?, override val childMode: Chi
277311
FinishedEvent(this)
278312
}
279313

280-
internal suspend fun switchToTargetState(
281-
targetState: InternalState,
314+
internal suspend fun switchToTargetStates(
315+
targetStates: Set<InternalState>,
282316
fromState: InternalState,
283317
transitionParams: TransitionParams<*>
284318
) {
285-
val path = fromState.findPathFromTargetToLca(targetState)
286-
if (transitionParams.transition.type == EXTERNAL)
287-
path.last().internalParent?.let { path.add(it) }
288-
val lca = path.removeLast()
289-
lca.recursiveEnterStatePath(path, transitionParams)
319+
when {
320+
targetStates.isEmpty() -> return
321+
targetStates.size == 1 -> {
322+
@Suppress("UNCHECKED_CAST")
323+
val path = fromState.findPathFromTargetToLca(
324+
targetStates.single(),
325+
transitionParams.transition.type == EXTERNAL
326+
) as List<InternalState>
327+
val iterator = path.listIterator(path.size)
328+
iterator.previous().recursiveEnterStatePath(iterator, transitionParams)
329+
}
330+
else -> {
331+
val pathHead = fromState.findTreePathFromTargetsToLca(
332+
targetStates,
333+
transitionParams.transition.type == EXTERNAL
334+
)
335+
val state = pathHead.state as InternalState
336+
state.recursiveEnterStatePath(pathHead, transitionParams)
337+
}
338+
}
339+
}
340+
}
341+
342+
private fun TransitionParams<*>.repackForRegion(regionTargetState: IState): TransitionParams<*> {
343+
return if (direction.targetState === regionTargetState) {
344+
this
345+
} else {
346+
copy(direction = TargetState(direction.targetStates, regionTargetState))
290347
}
291348
}

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/DataExtractor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ interface DataExtractor<D : Any> {
1212
}
1313

1414
inline fun <reified D : Any> defaultDataExtractor() = object : DataExtractor<D> {
15-
override suspend fun extractFinishedEvent(transitionParams: TransitionParams<*>, event: FinishedEvent) = event.data as? D
15+
override suspend fun extractFinishedEvent(transitionParams: TransitionParams<*>, event: FinishedEvent) =
16+
event.data as? D
17+
1618
override suspend fun extract(transitionParams: TransitionParams<*>) = null
1719
}

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/InternalState.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,54 @@ package ru.nsk.kstatemachine
22

33
import ru.nsk.kstatemachine.TransitionDirectionProducerPolicy.DefaultPolicy
44

5+
/**
6+
* Contains tree composition api for [InternalState].
7+
* Necessary for tree algorithms testing purpose, to be able not create full states in tests.
8+
* This is safe to cast any [InternalNode] to [InternalState] by design.
9+
*/
10+
internal interface InternalNode {
11+
val internalParent: InternalNode?
12+
}
13+
14+
internal fun InternalNode.requireParentNode(): InternalNode =
15+
requireNotNull(internalParent) { "$this parent is not set" }
16+
517
/**
618
* Defines state API for internal library usage. All states must implement this class.
719
* Unfortunately cannot use interface for this purpose.
20+
* This is safe to cast any [IState] to [InternalState] by design.
821
*/
9-
abstract class InternalState : IState {
22+
abstract class InternalState : IState, InternalNode {
1023
override val parent: IState? get() = internalParent
11-
internal abstract val internalParent: InternalState?
24+
abstract override val internalParent: InternalState?
1225
internal abstract fun setParent(parent: InternalState)
1326

1427
internal abstract fun getCurrentStates(): List<InternalState>
1528

1629
internal abstract suspend fun doEnter(transitionParams: TransitionParams<*>)
1730
internal abstract suspend fun doExit(transitionParams: TransitionParams<*>)
18-
internal abstract suspend fun afterChildFinished(finishedChild: InternalState, transitionParams: TransitionParams<*>)
31+
internal abstract suspend fun afterChildFinished(
32+
finishedChild: InternalState,
33+
transitionParams: TransitionParams<*>
34+
)
35+
1936
internal open fun onParentCurrentStateChanged(currentState: InternalState) = Unit
2037

2138
internal abstract suspend fun <E : Event> recursiveFindUniqueResolvedTransition(
2239
eventAndArgument: EventAndArgument<E>
2340
): ResolvedTransition<E>?
2441

2542
internal abstract suspend fun recursiveEnterInitialStates(transitionParams: TransitionParams<*>)
43+
44+
/** Enters single branch path */
45+
internal abstract suspend fun recursiveEnterStatePath(
46+
path: ListIterator<InternalState>,
47+
transitionParams: TransitionParams<*>
48+
)
49+
50+
/** Enters path with multiple branches */
2651
internal abstract suspend fun recursiveEnterStatePath(
27-
path: MutableList<InternalState>,
52+
pathHead: PathNode,
2853
transitionParams: TransitionParams<*>
2954
)
3055

@@ -38,12 +63,12 @@ abstract class InternalState : IState {
3863
internal abstract suspend fun cleanup()
3964
}
4065

41-
internal fun InternalState.requireInternalParent() = requireNotNull(internalParent) { "$this parent is not set" }
66+
internal fun InternalState.requireInternalParent(): InternalState =
67+
requireNotNull(internalParent) { "$this parent is not set" }
4268

4369
internal suspend fun <E : Event> InternalState.findTransitionsByEvent(event: E): List<InternalTransition<E>> {
44-
val triggeringTransitions = transitions.filter { it.isMatchingEvent(event) }
4570
@Suppress("UNCHECKED_CAST")
46-
return triggeringTransitions as List<InternalTransition<E>>
71+
return transitions.filter { it.isMatchingEvent(event) } as List<InternalTransition<E>>
4772
}
4873

4974
internal suspend fun <E : Event> InternalState.findUniqueResolvedTransition(eventAndArgument: EventAndArgument<E>): ResolvedTransition<E>? {

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/InternalTransition.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ru.nsk.kstatemachine
22

33
/**
44
* Defines transition API for internal library usage. All transitions must implement this interface.
5+
* This is safe to cast any [Transition] to [InternalTransition] by design.
56
*/
67
interface InternalTransition<E : Event> : Transition<E> {
78
override val sourceState: InternalState

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,21 +232,23 @@ internal class StateMachineImpl(
232232

233233
val transitionParams = TransitionParams(transition, direction, event, argument)
234234

235-
val targetState = (direction.targetState as? InternalState)?.also {
236-
check(it === this || it.isSubStateOf(this)) {
237-
"Transitioning to state $it from another state machine is not possible"
235+
@Suppress("UNCHECKED_CAST")
236+
val targetStates = (direction.targetStates as Set<InternalState>).also { targetStates ->
237+
val alienState = targetStates.find { it !== this && !it.isSubStateOf(this) }
238+
check(alienState == null) {
239+
"Transitioning to targetState $alienState from another state machine is not possible"
238240
}
239241
}
240242

241243
log {
242-
val targetText = if (targetState != null) "to $targetState" else "[target-less]"
243-
"${event::class.simpleName} triggers $transition from ${transition.sourceState} $targetText"
244+
val targetsText = if (targetStates.isNotEmpty()) "to [${targetStates.joinToString()}]" else "[target-less]"
245+
"${event::class.simpleName} triggers $transition from ${transition.sourceState} $targetsText"
244246
}
245247

246248
transition.transitionNotify { onTriggered(transitionParams) }
247249
machineNotify { onTransitionTriggered(transitionParams) }
248250

249-
targetState?.let { switchToTargetState(it, transition.sourceState, transitionParams) }
251+
switchToTargetStates(targetStates, transition.sourceState, transitionParams)
250252

251253
recursiveAfterTransitionComplete(transitionParams)
252254

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/Transition.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package ru.nsk.kstatemachine
22

3+
import ru.nsk.kstatemachine.TransitionType.EXTERNAL
4+
import ru.nsk.kstatemachine.TransitionType.LOCAL
35
import ru.nsk.kstatemachine.visitors.CoVisitor
46
import ru.nsk.kstatemachine.visitors.Visitor
57
import ru.nsk.kstatemachine.visitors.VisitorAcceptor
@@ -50,9 +52,10 @@ interface Transition<E : Event> : VisitorAcceptor {
5052

5153
/**
5254
* Most of the cases [EXTERNAL] and [LOCAL] transition are functionally equivalent except in cases where transition
53-
* is happening between super and sub states. Local transition doesn't cause exit and entry to source state if
55+
* is happening between super and sub-states. Local transition doesn't cause exit and entry to source state if
5456
* target state is a sub-state of a source state.
55-
* Other way around, local transition doesn't cause exit and entry to target state if target is a superstate of a source state.
57+
* Other way around, local transition doesn't cause exit and entry to target state if target is a superstate
58+
* of a source state.
5659
*/
5760
enum class TransitionType {
5861
/** Default */

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

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,91 @@
11
package ru.nsk.kstatemachine
22

3-
sealed class TransitionDirection {
3+
sealed interface TransitionDirection {
44
/**
55
* Already resolved target state of conditional transition or [PseudoState] computation
6+
* This is always one of [targetStates] list elements or null, if the list is empty.
67
*/
7-
open val targetState: IState? = null
8+
val targetState: IState? get() = null
9+
10+
/**
11+
* Transition can target multiple states if they all are located at different regions of a parallel state.
12+
*/
13+
val targetStates: Set<IState>
814
}
915

1016
/**
1117
* [Transition] is triggered, but state is not changed
1218
*/
13-
internal object Stay : TransitionDirection()
19+
internal object Stay : TransitionDirection {
20+
override val targetStates = emptySet<IState>()
21+
}
1422

1523
fun stay(): TransitionDirection = Stay
1624

1725
/**
1826
* [Transition] should not be triggered
1927
*/
20-
internal object NoTransition : TransitionDirection()
28+
internal object NoTransition : TransitionDirection {
29+
override val targetStates = emptySet<IState>()
30+
}
2131

2232
fun noTransition(): TransitionDirection = NoTransition
2333

2434
/**
25-
* [Transition] is triggered with a [targetState].
35+
* [Transition] is triggered with [targetStates] (usually with single [targetState])
2636
*/
27-
internal open class TargetState(override val targetState: IState) : TransitionDirection()
37+
internal data class TargetState(
38+
override val targetStates: Set<IState>,
39+
override val targetState: IState = targetStates.first()
40+
) : TransitionDirection {
41+
init {
42+
require(targetStates.contains(targetState)) {
43+
"Internal logical error, invalid ${TargetState::class.simpleName} construction, this should never happen"
44+
}
45+
}
46+
}
2847

2948
/**
3049
* [Transition] is triggered with a targetState, resolving it in place if it is a [PseudoState]
3150
*/
3251
suspend fun EventAndArgument<*>.targetState(targetState: IState): TransitionDirection = resolveTargetState(targetState)
3352

53+
suspend fun EventAndArgument<*>.targetParallelStates(targetStates: Set<IState>): TransitionDirection {
54+
require(targetStates.size >= 2) {
55+
"There should be at least two targetStates, current amount ${targetStates.size}," +
56+
" check that you are not using the same state multiple times"
57+
}
58+
val resolvedStates = mutableSetOf<IState>()
59+
targetStates.mapNotNullTo(resolvedStates) { recursiveResolveTargetState(it) }
60+
if (resolvedStates.isEmpty()) return NoTransition
61+
62+
@Suppress("UNCHECKED_CAST")
63+
val lca = findLca(resolvedStates as Set<InternalNode>) as InternalState
64+
check(lca.findParallelAncestor() != null) {
65+
"Resolved states does not have common ancestor with ${ChildMode.PARALLEL} child mode. " +
66+
"Only children of a state with ${ChildMode.PARALLEL} child mode" +
67+
" might be used as effective (resolved) targets here."
68+
}
69+
return TargetState(resolvedStates)
70+
}
71+
72+
private fun InternalState.findParallelAncestor(): InternalState? {
73+
return if (childMode == ChildMode.PARALLEL) this else internalParent?.findParallelAncestor()
74+
}
75+
76+
suspend fun EventAndArgument<*>.targetParallelStates(
77+
targetState1: IState,
78+
targetState2: IState,
79+
vararg targetStates: IState
80+
) = targetParallelStates(setOf(targetState1, targetState2, *targetStates))
81+
3482
private suspend fun EventAndArgument<*>.resolveTargetState(targetState: IState): TransitionDirection {
3583
val resolvedState = recursiveResolveTargetState(targetState)
36-
return if (resolvedState != null) TargetState(resolvedState) else NoTransition
84+
return if (resolvedState != null) TargetState(setOf(resolvedState)) else NoTransition
3785
}
3886

3987
private suspend fun EventAndArgument<*>.recursiveResolveTargetState(targetState: IState): IState? {
40-
val resolvedTarget = when (targetState) {
88+
val resolvedTarget = when (targetState) {
4189
is RedirectPseudoState -> recursiveResolveTargetState(targetState.resolveTargetState(this))
4290
is HistoryState -> targetState.storedState
4391
is UndoState -> targetState.popState()
@@ -56,7 +104,7 @@ private suspend fun EventAndArgument<*>.recursiveResolveTargetState(targetState:
56104
/**
57105
* Internal use only. TODO remove it when possible
58106
*/
59-
internal fun unresolvedTargetState(targetState: IState): TransitionDirection = TargetState(targetState)
107+
internal fun unresolvedTargetState(targetState: IState): TransitionDirection = TargetState(setOf(targetState))
60108

61109
/**
62110
* Transition that matches event and has a meaningful direction (except [NoTransition])

0 commit comments

Comments
 (0)