diff --git a/CHANGELOG.md b/CHANGELOG.md index d036e4e6f30..c255949536c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * `Versions`: * `AppCompat`: `1.3.1` -> `1.4.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 diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Optional.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Optional.kt index 3c48c1b4da9..0c3a67521d0 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Optional.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Optional.kt @@ -21,8 +21,10 @@ import kotlinx.serialization.Serializable */ @Serializable data class Optional internal constructor( - internal val data: T?, - internal val dataPresented: Boolean + @Warning("It is unsafe to use this data directly") + val data: T?, + @Warning("It is unsafe to use this data directly") + val dataPresented: Boolean ) { companion object { /** @@ -42,17 +44,31 @@ inline val T.optional /** * Will call [block] when data presented ([Optional.dataPresented] == true) */ -fun Optional.onPresented(block: (T) -> Unit): Optional = apply { +inline fun Optional.onPresented(block: (T) -> Unit): Optional = apply { if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } } +/** + * Will call [block] when data presented ([Optional.dataPresented] == true) + */ +inline fun Optional.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) */ -fun Optional.onAbsent(block: () -> Unit): Optional = apply { +inline fun Optional.onAbsent(block: () -> Unit): Optional = apply { if (!dataPresented) { block() } } +/** + * Will call [block] when data presented ([Optional.dataPresented] == true) + */ +inline fun Optional.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 */ @@ -67,9 +83,10 @@ fun Optional.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 */ -fun Optional.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() +inline fun Optional.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 */ +@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse")) suspend fun Optional.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() diff --git a/fsm/common/build.gradle b/fsm/common/build.gradle index 854f51c5dfb..6b6b040f3cf 100644 --- a/fsm/common/build.gradle +++ b/fsm/common/build.gradle @@ -10,6 +10,7 @@ kotlin { sourceSets { commonMain { dependencies { + api project(":micro_utils.common") api project(":micro_utils.coroutines") } } diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt index d470ff12d5c..4a72a715784 100644 --- a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt @@ -1,8 +1,11 @@ package dev.inmo.micro_utils.fsm.common -import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions -import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.common.Optional +import dev.inmo.micro_utils.common.onPresented +import dev.inmo.micro_utils.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 @@ -42,17 +45,53 @@ interface StatesMachine : StatesHandler { /** * 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 ( - private val statesManager: StatesManager, - private val handlers: List> +open class DefaultStatesMachine ( + protected val statesManager: StatesManager, + protected val handlers: List>, ) : StatesMachine { /** * Will call [launchStateHandling] for state handling */ override suspend fun StatesMachine.handleState(state: T): T? = launchStateHandling(state, handlers) + /** + * This + */ + protected val statesJobs = mutableMapOf() + 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, 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], * [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If @@ -60,23 +99,15 @@ class DefaultStatesMachine ( * [StatesManager.endChain]. */ 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) { - launch { statePerformer(it) } + launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } } statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { - launch { statePerformer(it.second) } + launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) } } statesManager.getActiveStates().forEach { - launch { statePerformer(it) } + launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) } } } diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/UpdatableStatesMachine.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/UpdatableStatesMachine.kt new file mode 100644 index 00000000000..92a47551d41 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/UpdatableStatesMachine.kt @@ -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 : StatesMachine { + /** + * 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( + statesManager: StatesManager, + handlers: List>, + +) : DefaultStatesMachine( + statesManager, + handlers +), UpdatableStatesMachine { + protected val jobsStates = mutableMapOf() + + override suspend fun performStateUpdate(previousState: Optional, 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) + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt index 76786ecac6f..aa244eed616 100644 --- a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt @@ -7,6 +7,14 @@ import kotlin.reflect.KClass class FSMBuilder( var statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), + val fsmBuilder: (states: List>, defaultHandler: StatesHandler?) -> StatesMachine = { states, defaultHandler -> + StatesMachine( + statesManager, + states.let { list -> + defaultHandler ?.let { list + it.holder { true } } ?: list + } + ) + }, var defaultStateHandler: StatesHandler? = StatesHandler { null } ) { private var states = mutableListOf>() @@ -42,12 +50,7 @@ class FSMBuilder( add(filter, handler) } - fun build() = StatesMachine( - statesManager, - states.toList().let { list -> - defaultStateHandler ?.let { list + it.holder { true } } ?: list - } - ) + fun build() = fsmBuilder(states.toList(), defaultStateHandler) } fun buildFSM(