Skip to content

Commit 7fc1ee5

Browse files
committed
Merge branch 'transition-types'
2 parents 56e8211 + 13c48db commit 7fc1ee5

20 files changed

+370
-90
lines changed

docs/index.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,25 @@ greenState {
187187
}
188188
```
189189

190+
_Note: Such transitions are also called internal._
191+
192+
### Transition type
193+
194+
There are two types of transitions `TransitionType.LOCAL` (default) and `TransitionType.EXTERNAL`.
195+
Most of the cases both transitions are functionally equivalent except in cases where transition
196+
is happening between super and sub states. Local transition doesn't cause exit and entry to source state if
197+
target state is a sub-state of a source state.
198+
Local transition doesn't cause exit and entry to target state if target is a superstate of a source
199+
state.
200+
201+
Use `type` argument or property of transition builder functions to set transition type:
202+
```kotlin
203+
transition<SwitchEvent> {
204+
type = EXTERNAL
205+
targetState = state2
206+
}
207+
```
208+
190209
### Listen to all transitions in one place
191210

192211
There might be many transitions from one state to another. It is possible to listen to all of them in state machine
@@ -392,7 +411,8 @@ Notifications about finishing are available in two forms:
392411
Transition for `FinishedEvent` is detected by the library and matched by special kind of `EventMatcher`,
393412
so such transition is triggered only for `FinishedEvent` that corresponds to this state.
394413
`FinishingEvent` generated by finishing of another state will not trigger such transition.
395-
See [transition on FinishedEvent sample](https://github.com/nsk90/kstatemachine/tree/master/samples/src/main/kotlin/ru/nsk/samples/FinishedEventSample.kt).
414+
See [transition on FinishedEvent sample](https://github.com/nsk90/kstatemachine/tree/master/samples/src/main/kotlin/ru/nsk/samples/FinishedEventSample.kt)
415+
.
396416

397417
## Nested states
398418

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

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ru.nsk.kstatemachine
22

3-
import ru.nsk.kstatemachine.TransitionDirectionProducerPolicy.DefaultPolicy
3+
import ru.nsk.kstatemachine.TransitionType.EXTERNAL
44
import ru.nsk.kstatemachine.TreeAlgorithms.findPathFromTargetToLca
55
import ru.nsk.kstatemachine.visitors.GetActiveStatesVisitor
66

@@ -146,10 +146,14 @@ open class BaseStateImpl(override val name: String?, override val childMode: Chi
146146
.filter { it !is StateMachine } // exclude nested machines
147147
.mapNotNull { it.recursiveFindUniqueResolvedTransition(eventAndArgument) }
148148
.ifEmpty { listOfNotNull(findUniqueResolvedTransition(eventAndArgument)) } // allow transition override
149-
check(resolvedTransitions.size <= 1) {
150-
"Multiple transitions match ${eventAndArgument.event}, $transitions in $this"
149+
return if (!machine.doNotThrowOnMultipleTransitionsMatch) {
150+
check(resolvedTransitions.size <= 1) {
151+
"Multiple transitions match ${eventAndArgument.event}, $transitions in $this"
152+
}
153+
resolvedTransitions.singleOrNull()
154+
} else {
155+
resolvedTransitions.firstOrNull()
151156
}
152-
return resolvedTransitions.singleOrNull()
153157
}
154158

155159
override fun recursiveEnterInitialStates(transitionParams: TransitionParams<*>) {
@@ -213,7 +217,7 @@ open class BaseStateImpl(override val name: String?, override val childMode: Chi
213217
require(childMode == ChildMode.EXCLUSIVE) { "Cannot set current state in child mode $childMode" }
214218
require(states.contains(state)) { "$state is not a child of $this" }
215219

216-
if (data.currentState == state) return
220+
if (data.currentState == state && transitionParams.transition.type != EXTERNAL) return
217221
data.currentState?.recursiveExit(transitionParams)
218222
data.currentState = state
219223

@@ -258,32 +262,9 @@ open class BaseStateImpl(override val name: String?, override val childMode: Chi
258262
transitionParams: TransitionParams<*>
259263
) {
260264
val path = fromState.findPathFromTargetToLca(targetState)
265+
if (transitionParams.transition.type == EXTERNAL)
266+
path.last().internalParent?.let { path.add(it) }
261267
val lca = path.removeLast()
262268
lca.recursiveEnterStatePath(path, transitionParams)
263269
}
264-
265-
/**
266-
* Initial event which is processed on state machine start
267-
*/
268-
internal object StartEvent : Event
269-
270-
internal fun makeStartTransitionParams(
271-
sourceState: IState,
272-
targetState: IState = sourceState,
273-
argument: Any?
274-
): TransitionParams<*> {
275-
val transition = DefaultTransition(
276-
"Starting",
277-
EventMatcher.isInstanceOf<StartEvent>(),
278-
sourceState,
279-
targetState,
280-
)
281-
282-
return TransitionParams(
283-
transition,
284-
transition.produceTargetStateDirection(DefaultPolicy(EventAndArgument(StartEvent, argument))),
285-
StartEvent,
286-
argument,
287-
)
288-
}
289-
}
270+
}

kstatemachine/src/main/kotlin/ru/nsk/kstatemachine/DefaultTransition.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package ru.nsk.kstatemachine
33
open class DefaultTransition<E : Event>(
44
override val name: String?,
55
override val eventMatcher: EventMatcher<E>,
6-
sourceState: IState
6+
override val type: TransitionType,
7+
sourceState: IState,
78
) : InternalTransition<E> {
89
private val _listeners = mutableSetOf<Transition.Listener>()
910
override val listeners: Collection<Transition.Listener> get() = _listeners
@@ -23,18 +24,20 @@ open class DefaultTransition<E : Event>(
2324
constructor(
2425
name: String?,
2526
eventMatcher: EventMatcher<E>,
27+
type: TransitionType,
2628
sourceState: IState,
2729
targetState: IState?
28-
) : this(name, eventMatcher, sourceState) {
30+
) : this(name, eventMatcher, type, sourceState) {
2931
targetStateDirectionProducer = { it.targetStateOrStay(targetState) }
3032
}
3133

3234
constructor(
3335
name: String?,
3436
eventMatcher: EventMatcher<E>,
37+
type: TransitionType,
3538
sourceState: IState,
3639
targetStateDirectionProducer: TransitionDirectionProducer<E>
37-
) : this(name, eventMatcher, sourceState) {
40+
) : this(name, eventMatcher, type, sourceState) {
3841
this.targetStateDirectionProducer = targetStateDirectionProducer
3942
}
4043

kstatemachine/src/main/kotlin/ru/nsk/kstatemachine/IState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ inline fun <reified S : IState> IState.requireState(recursive: Boolean = true) =
180180
operator fun <S : IState> S.invoke(block: StateBlock<S>) = block()
181181

182182
/**
183-
* Most common methods [onEntry] and [onExit] are shipped with [once] argument, to remove listener
183+
* The most commonly used methods [onEntry] and [onExit] are shipped with [once] argument, to remove listener
184184
* after it is triggered the first time.
185185
* Looks that it is not necessary in other similar methods.
186186
*/

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ internal fun <E : Event> InternalState.findUniqueResolvedTransition(eventAndArgu
5656
val transitions = findTransitionsByEvent(eventAndArgument.event)
5757
.map { it to it.produceTargetStateDirection(policy) }
5858
.filter { it.second !is NoTransition }
59-
check(transitions.size <= 1) { "Multiple transitions match ${eventAndArgument.event}, $transitions in $this" }
60-
return transitions.singleOrNull()
59+
return if (!machine.doNotThrowOnMultipleTransitionsMatch) {
60+
check(transitions.size <= 1) { "Multiple transitions match ${eventAndArgument.event}, $transitions in $this" }
61+
transitions.singleOrNull()
62+
} else {
63+
transitions.firstOrNull()
64+
}
6165
}

kstatemachine/src/main/kotlin/ru/nsk/kstatemachine/StateMachine.kt

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

3+
import ru.nsk.kstatemachine.StateMachine.PendingEventHandler
34
import ru.nsk.kstatemachine.visitors.Visitor
45

56
@DslMarker
@@ -34,6 +35,13 @@ interface StateMachine : State {
3435

3536
val isUndoEnabled: Boolean
3637

38+
/**
39+
* If set to true, when multiple transitions match event the first matching transition is selected.
40+
* if set to false, when multiple transitions match event exception is thrown.
41+
* Default if false.
42+
*/
43+
val doNotThrowOnMultipleTransitionsMatch: Boolean
44+
3745
fun <L : Listener> addListener(listener: L): L
3846
fun removeListener(listener: Listener)
3947

@@ -48,9 +56,13 @@ interface StateMachine : State {
4856
fun stop()
4957

5058
/**
51-
* Machine must be started to process events
59+
* Processes [Event].
60+
* Machine must be started to be able to process events.
61+
* @return [ProcessingResult] for current event.
62+
* If more events will be queued while this method is working, there results will not be taken to account.
63+
* Their [processEvent] calls will return [ProcessingResult.PENDING] in this case.
5264
*/
53-
fun processEvent(event: Event, argument: Any? = null)
65+
fun processEvent(event: Event, argument: Any? = null): ProcessingResult
5466

5567
/**
5668
* Destroys machine structure clearing all listeners, states etc.
@@ -111,6 +123,9 @@ interface StateMachine : State {
111123
}
112124
}
113125

126+
/**
127+
* Shortcut for [StateMachine.stop] and [StateMachine.start] sequence calls
128+
*/
114129
fun StateMachine.restart(argument: Any? = null) {
115130
stop()
116131
start(argument)
@@ -166,8 +181,22 @@ fun createStateMachine(
166181
start: Boolean = true,
167182
autoDestroyOnStatesReuse: Boolean = true,
168183
enableUndo: Boolean = false,
184+
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
169185
init: StateMachineBlock
170-
): StateMachine = StateMachineImpl(name, childMode, autoDestroyOnStatesReuse, enableUndo).apply {
171-
init()
172-
if (start) start()
186+
): StateMachine {
187+
return StateMachineImpl(name, childMode, autoDestroyOnStatesReuse, enableUndo, doNotThrowOnMultipleTransitionsMatch).apply {
188+
init()
189+
if (start) start()
190+
}
191+
}
192+
193+
enum class ProcessingResult {
194+
/** Event was sent to [PendingEventHandler] */
195+
PENDING,
196+
197+
/** Event was processed */
198+
PROCESSED,
199+
200+
/** Event was ignored */
201+
IGNORED,
173202
}

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

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

3+
import ru.nsk.kstatemachine.TransitionDirectionProducerPolicy.DefaultPolicy
34
import ru.nsk.kstatemachine.visitors.CheckUniqueNamesVisitor
45
import ru.nsk.kstatemachine.visitors.CleanupVisitor
56

@@ -16,6 +17,7 @@ internal class StateMachineImpl(
1617
childMode: ChildMode,
1718
override val autoDestroyOnStatesReuse: Boolean,
1819
override val isUndoEnabled: Boolean,
20+
override val doNotThrowOnMultipleTransitionsMatch: Boolean,
1921
) : InternalStateMachine(name, childMode) {
2022
private val _machineListeners = mutableSetOf<StateMachine.Listener>()
2123
override val machineListeners: Collection<StateMachine.Listener> get() = _machineListeners
@@ -105,7 +107,7 @@ internal class StateMachineImpl(
105107
}
106108
}
107109

108-
override fun processEvent(event: Event, argument: Any?) {
110+
override fun processEvent(event: Event, argument: Any?): ProcessingResult {
109111
check(!isDestroyed) { "$this is already destroyed" }
110112
check(isRunning) { "$this is not started, call start() first" }
111113

@@ -116,10 +118,10 @@ internal class StateMachineImpl(
116118
// pending event cannot be processed while previous event is still processing
117119
// even if PendingEventHandler does not throw. QueuePendingEventHandler implementation stores such events
118120
// to be processed later.
119-
return
121+
return ProcessingResult.PENDING
120122
}
121123

122-
eventProcessingScope {
124+
return eventProcessingScope {
123125
process(eventAndArgument)
124126
}
125127
}
@@ -133,32 +135,32 @@ internal class StateMachineImpl(
133135
}
134136
}
135137

136-
private fun process(eventAndArgument: EventAndArgument<*>) {
137-
var eventProcessed: Boolean? = null
138-
138+
private fun process(eventAndArgument: EventAndArgument<*>): ProcessingResult {
139139
val wrappedEventAndArgument = eventAndArgument.wrap()
140140

141-
runCheckingExceptions {
142-
eventProcessed = doProcessEvent(wrappedEventAndArgument)
141+
val eventProcessed = runCheckingExceptions {
142+
doProcessEvent(wrappedEventAndArgument)
143143
}
144144

145-
if (eventProcessed == false) {
145+
if (!eventProcessed) {
146146
log { "$this ignored ${wrappedEventAndArgument.event::class.simpleName}" }
147147
ignoredEventHandler.onIgnoredEvent(wrappedEventAndArgument.event, wrappedEventAndArgument.argument)
148148
}
149+
return if (eventProcessed) ProcessingResult.PROCESSED else ProcessingResult.IGNORED
149150
}
150151

151152
/**
152153
* Runs block of code that processes event, and processes all pending events from queue after it if
153154
* [QueuePendingEventHandler] is used.
154155
*/
155-
private fun eventProcessingScope(block: () -> Unit) {
156+
private fun <R> eventProcessingScope(block: () -> R): R {
156157
val queue = pendingEventHandler as? QueuePendingEventHandler
157158
queue?.checkEmpty()
158159

160+
val result: R
159161
isProcessingEvent = true
160162
try {
161-
block()
163+
result = block()
162164

163165
queue?.let {
164166
var eventAndArgument = it.nextEventAndArgument()
@@ -174,14 +176,16 @@ internal class StateMachineImpl(
174176
} finally {
175177
isProcessingEvent = false
176178
}
179+
return result
177180
}
178181

179182
/**
180183
* Runs block of code that triggers notification listeners
181184
*/
182-
private fun runCheckingExceptions(block: () -> Unit) {
185+
private fun <R> runCheckingExceptions(block: () -> R): R {
186+
val result: R
183187
try {
184-
block()
188+
result = block()
185189
} catch (e: Exception) {
186190
log { "Fatal exception happened, $this machine is in unpredictable state and will be destroyed: $e" }
187191
runCatching { destroy(false) }
@@ -191,6 +195,7 @@ internal class StateMachineImpl(
191195
delayedListenerException = null
192196
listenerExceptionHandler.onException(it)
193197
}
198+
return result
194199
}
195200

196201
private fun <E : Event> doProcessEvent(eventAndArgument: EventAndArgument<E>): Boolean {
@@ -261,4 +266,23 @@ internal fun InternalStateMachine.runDelayingException(block: () -> Unit) =
261266
block()
262267
} catch (e: Exception) {
263268
delayListenerException(e)
264-
}
269+
}
270+
271+
internal fun makeStartTransitionParams(sourceState: IState, targetState: IState = sourceState, argument: Any?):
272+
TransitionParams<*> {
273+
val transition = DefaultTransition(
274+
"Starting",
275+
EventMatcher.isInstanceOf<StartEvent>(),
276+
TransitionType.LOCAL,
277+
sourceState,
278+
targetState,
279+
)
280+
281+
val event = StartEvent()
282+
return TransitionParams(
283+
transition,
284+
transition.produceTargetStateDirection(DefaultPolicy(EventAndArgument(event, argument))),
285+
event,
286+
argument,
287+
)
288+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface Transition<E : Event> : VisitorAcceptor {
1010
val name: String?
1111
val eventMatcher: EventMatcher<E>
1212
val sourceState: IState
13+
val type: TransitionType
1314

1415
/**
1516
* This parameter may be used to pass arbitrary data with a transition to targetState.
@@ -40,3 +41,15 @@ inline fun <reified E : Event> Transition<E>.onTriggered(
4041
@Suppress("UNCHECKED_CAST")
4142
override fun onTriggered(transitionParams: TransitionParams<*>) = block(transitionParams as TransitionParams<E>)
4243
})
44+
45+
/**
46+
* Most of the cases external and local transition are functionally equivalent except in cases where transition
47+
* is happening between super and sub states. Local transition doesn't cause exit and entry to source state if
48+
* target state is a sub-state of a source state.
49+
* Other way around, local transition doesn't cause exit and entry to target state if target is a superstate of a source state.
50+
*/
51+
enum class TransitionType {
52+
/** Default */
53+
LOCAL,
54+
EXTERNAL
55+
}

0 commit comments

Comments
 (0)