updates in FSM

This commit is contained in:
InsanusMokrassar 2021-12-26 21:06:26 +06:00
parent 3d2196e35d
commit f037ce4371
6 changed files with 140 additions and 28 deletions

View File

@ -5,6 +5,9 @@
* `Versions`: * `Versions`:
* `AppCompat`: `1.3.1` -> `1.4.0` * `AppCompat`: `1.3.1` -> `1.4.0`
* Android Compile SDK: `31.0.0` -> `32.0.0` * Android Compile SDK: `31.0.0` -> `32.0.0`
* `FSM`:
* `DefaultStatesMachine` now is extendable
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
## 0.8.7 ## 0.8.7

View File

@ -21,8 +21,10 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
data class Optional<T> internal constructor( data class Optional<T> internal constructor(
internal val data: T?, @Warning("It is unsafe to use this data directly")
internal val dataPresented: Boolean val data: T?,
@Warning("It is unsafe to use this data directly")
val dataPresented: Boolean
) { ) {
companion object { companion object {
/** /**
@ -42,17 +44,31 @@ inline val <T> T.optional
/** /**
* Will call [block] when data presented ([Optional.dataPresented] == true) * Will call [block] when data presented ([Optional.dataPresented] == true)
*/ */
fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
} }
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
}
/** /**
* Will call [block] when data absent ([Optional.dataPresented] == false) * Will call [block] when data absent ([Optional.dataPresented] == false)
*/ */
fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
if (!dataPresented) { block() } if (!dataPresented) { block() }
} }
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
}
/** /**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
*/ */
@ -67,9 +83,10 @@ fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Supp
/** /**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/ */
fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
/** /**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/ */
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()

View File

@ -10,6 +10,7 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api project(":micro_utils.common")
api project(":micro_utils.coroutines") api project(":micro_utils.coroutines")
} }
} }

View File

@ -1,8 +1,11 @@
package dev.inmo.micro_utils.fsm.common package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.common.onPresented
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/** /**
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use * Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
@ -42,17 +45,53 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
/** /**
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts * Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
* resolving, and uses [launchStateHandling] for [State] handling * resolving, and uses [launchStateHandling] for [State] handling.
*
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
*/ */
class DefaultStatesMachine <T: State>( open class DefaultStatesMachine <T: State>(
private val statesManager: StatesManager<T>, protected val statesManager: StatesManager<T>,
private val handlers: List<CheckableHandlerHolder<in T, T>> protected val handlers: List<CheckableHandlerHolder<in T, T>>,
) : StatesMachine<T> { ) : StatesMachine<T> {
/** /**
* Will call [launchStateHandling] for state handling * Will call [launchStateHandling] for state handling
*/ */
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers) override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
/**
* This
*/
protected val statesJobs = mutableMapOf<T, Job>()
protected val statesJobsMutex = Mutex()
protected open suspend fun performUpdate(state: T) {
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
statesJobs[actualState] ?.cancel()
statesJobs[actualState] = scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
if (statesJobs[actualState] == job) {
statesJobs.remove(actualState)
}
}
}
}
}
}
}
/** /**
* Launch handling of states. On [statesManager] [StatesManager.onStartChain], * Launch handling of states. On [statesManager] [StatesManager.onStartChain],
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
@ -60,23 +99,15 @@ class DefaultStatesMachine <T: State>(
* [StatesManager.endChain]. * [StatesManager.endChain].
*/ */
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
val statePerformer: suspend (T) -> Unit = { state: T ->
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it) } launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
} }
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it.second) } launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
} }
statesManager.getActiveStates().forEach { statesManager.getActiveStates().forEach {
launch { statePerformer(it) } launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
} }
} }

View File

@ -0,0 +1,57 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.common.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
/**
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
* must be able to perform update of chain in internal [StatesManager]
*/
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
/**
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
* [StatesManager.update] function
*/
suspend fun updateChain(currentState: T, newState: T)
}
open class DefaultUpdatableStatesMachine<T : State>(
statesManager: StatesManager<T>,
handlers: List<CheckableHandlerHolder<in T, T>>,
) : DefaultStatesMachine<T>(
statesManager,
handlers
), UpdatableStatesMachine<T> {
protected val jobsStates = mutableMapOf<Job, T>()
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
statesJobs[actualState] ?.cancel()
val job = previousState.mapOnPresented {
statesJobs.remove(it)
} ?: scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
statesJobs.remove(
jobsStates[job] ?: return@withLock
)
}
}
}
}
jobsStates.remove(job)
statesJobs[actualState] = job
jobsStates[job] = actualState
}
}
override suspend fun updateChain(currentState: T, newState: T) {
statesManager.update(currentState, newState)
}
}

View File

@ -7,6 +7,14 @@ import kotlin.reflect.KClass
class FSMBuilder<T : State>( class FSMBuilder<T : State>(
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
val fsmBuilder: (states: List<CheckableHandlerHolder<T, T>>, defaultHandler: StatesHandler<T, T>?) -> StatesMachine<T> = { states, defaultHandler ->
StatesMachine(
statesManager,
states.let { list ->
defaultHandler ?.let { list + it.holder { true } } ?: list
}
)
},
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null } var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
) { ) {
private var states = mutableListOf<CheckableHandlerHolder<T, T>>() private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
@ -42,12 +50,7 @@ class FSMBuilder<T : State>(
add(filter, handler) add(filter, handler)
} }
fun build() = StatesMachine( fun build() = fsmBuilder(states.toList(), defaultStateHandler)
statesManager,
states.toList().let { list ->
defaultStateHandler ?.let { list + it.holder { true } } ?: list
}
)
} }
fun <T : State> buildFSM( fun <T : State> buildFSM(