diff --git a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StateHandlerHolder.kt b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StateHandlerHolder.kt index 269563b..f429ee2 100644 --- a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StateHandlerHolder.kt +++ b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StateHandlerHolder.kt @@ -4,8 +4,8 @@ import kotlin.reflect.KClass class StateHandlerHolder( private val inputKlass: KClass, - private val delegateTo: StatesHandler, - private val strict: Boolean = false + private val strict: Boolean = false, + private val delegateTo: StatesHandler ) : StatesHandler { fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state)) diff --git a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesHolder.kt b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesHolder.kt deleted file mode 100644 index 0bc6466..0000000 --- a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesHolder.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.inmo.tgbotapi.libraries.fsm.core - -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -interface StatesHolder { - val onNewState: Flow - val onStateRemoved: Flow - - /** - * Must always save [state] inside of [this] [StatesHolder] AND send event in [onNewState] - */ - suspend fun saveState(state: T) - - /** - * Must always remove [state] in case it is stored AND send event in [onStateRemoved] if it was removed - */ - suspend fun removeState(state: T) - - /** - * Must returns currently stored states - */ - suspend fun loadStates(): List -} - -class ListBasedStatesHolder( - base: List = emptyList() -) : StatesHolder { - private val data: MutableList = base.toMutableList() - private val _onNewState = MutableSharedFlow(0, onBufferOverflow = BufferOverflow.SUSPEND) - override val onNewState: Flow = _onNewState.asSharedFlow() - private val _onStateRemoved = MutableSharedFlow(0, onBufferOverflow = BufferOverflow.SUSPEND) - override val onStateRemoved: Flow = _onStateRemoved.asSharedFlow() - - override suspend fun saveState(state: T) { - data.add(state) - _onNewState.emit(state) - } - - override suspend fun removeState(state: T) { - if (data.remove(state)) { - _onStateRemoved.emit(state) - } - } - - override suspend fun loadStates(): List = data.toList() - -} diff --git a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesMachine.kt b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesMachine.kt index 8c2c0d5..6ad550f 100644 --- a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesMachine.kt +++ b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesMachine.kt @@ -14,22 +14,31 @@ private suspend fun launchStateHandling( } class StatesMachine( - private val statesHolder: StatesHolder, - private val statesQuotaManager: StatesQuotaManager, + private val statesManager: StatesManager, private val handlers: List> ) : StatesHandler { override suspend fun handleState(state: T): O? { - return statesQuotaManager.doOnQuota(state) { - launchStateHandling(state, handlers) - } + return launchStateHandling(state, handlers) } fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { - val statesFlow = statesHolder.loadStates().asFlow() + statesHolder.onNewState - statesFlow.subscribeSafelyWithoutExceptions(this) { - val newState = handleState(it) - newState ?.also { statesHolder.saveState(newState) } - statesHolder.removeState(it) + val statePerformer: suspend (T) -> Unit = { state: T -> + val newState = handleState(state) + if (newState != null) { + statesManager.update(state, newState) + } else { + statesManager.endChain(state) + } + } + statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { + launch { statePerformer(it) } + } + statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { + launch { statePerformer(it.second) } + } + + statesManager.getActiveStates().forEach { + launch { statePerformer(it) } } } } diff --git a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesManager.kt b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesManager.kt new file mode 100644 index 0000000..59572df --- /dev/null +++ b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesManager.kt @@ -0,0 +1,92 @@ +package dev.inmo.tgbotapi.libraries.fsm.core + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +interface StatesManager { + val onChainStateUpdated: Flow> + val onStartChain: Flow + val onEndChain: Flow + + + /** + * Must set current set using [State.context] + */ + suspend fun update(old: T, new: T) + + /** + * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already + * busy by the other [State] + */ + suspend fun startChain(state: T) + + /** + * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just + * ignored + */ + suspend fun endChain(state: T) + + suspend fun getActiveStates(): List +} + +/** + * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] + * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by + * new state by using [endChain] with that state + */ +class InMemoryStatesManager( + private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true } +) : StatesManager { + private val _onChainStateUpdated = MutableSharedFlow>(0) + override val onChainStateUpdated: Flow> = _onChainStateUpdated.asSharedFlow() + private val _onStartChain = MutableSharedFlow(0) + override val onStartChain: Flow = _onStartChain.asSharedFlow() + private val _onEndChain = MutableSharedFlow(0) + override val onEndChain: Flow = _onEndChain.asSharedFlow() + + private val contextsToStates = mutableMapOf() + private val mapMutex = Mutex() + + override suspend fun update(old: T, new: T) = mapMutex.withLock { + when { + contextsToStates[old.context] != old -> return@withLock + old.context == new.context || !contextsToStates.containsKey(new.context) -> { + contextsToStates[old.context] = new + _onChainStateUpdated.emit(old to new) + } + else -> { + val stateOnNewOneContext = contextsToStates.getValue(new.context) + if (onContextsConflictResolver(old, new, stateOnNewOneContext)) { + endChainWithoutLock(stateOnNewOneContext) + contextsToStates.remove(old.context) + contextsToStates[new.context] = new + _onChainStateUpdated.emit(old to new) + } + } + } + } + + override suspend fun startChain(state: T) = mapMutex.withLock { + if (!contextsToStates.containsKey(state.context)) { + contextsToStates[state.context] = state + _onStartChain.emit(state) + } + } + + private suspend fun endChainWithoutLock(state: T) { + if (contextsToStates[state.context] == state) { + contextsToStates.remove(state.context) + _onEndChain.emit(state) + } + } + + override suspend fun endChain(state: T) { + mapMutex.withLock { + endChainWithoutLock(state) + } + } + + override suspend fun getActiveStates(): List = contextsToStates.values.toList() + +} diff --git a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesQuotaManager.kt b/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesQuotaManager.kt deleted file mode 100644 index 42c8fd1..0000000 --- a/fsm/core/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/StatesQuotaManager.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.inmo.tgbotapi.libraries.fsm.core - -import kotlinx.coroutines.sync.Mutex - -interface StatesQuotaManager { - suspend fun doOnQuota(state: T, block: suspend (T) -> O?): O? - - suspend fun transitQuota(from: State, to: State?) -} - -class InMemoryStatesQuotaManager : StatesQuotaManager { - private val currentContextsAndStates = mutableMapOf() - - private val mutex = Mutex() - - override suspend fun doOnQuota(state: T, block: suspend (T) -> O?): O? { - - } - - override suspend fun transitQuota(state: State) { - TODO("Not yet implemented") - } -} diff --git a/fsm/core/src/commonTest/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/PlayableMain.kt b/fsm/core/src/commonTest/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/PlayableMain.kt deleted file mode 100644 index 17d713f..0000000 --- a/fsm/core/src/commonTest/kotlin/dev/inmo/tgbotapi/libraries/fsm/core/PlayableMain.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.inmo.tgbotapi.libraries.fsm.core - -import kotlin.random.Random - -sealed interface TrafficLightState : ImmediateOrNeverState { - val trafficLightNumber: Int - override val context: Int - get() = trafficLightNumber -} -data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState -data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState -data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState - -suspend fun main() { - val countOfTrafficLights = 10 - val initialStates = (0 until countOfTrafficLights).map { - when (Random.nextInt(3)) { - 0 -> GreenCommon(it) - 1 -> YellowCommon(it) - else -> RedCommon(it) - } - } - - val statesHolder = ListBasedStatesHolder(initialStates) - val machine = StatesMachine( - statesHolder, - - ) -} - diff --git a/fsm/core/src/jvmTest/kotlin/PlayableMain.kt b/fsm/core/src/jvmTest/kotlin/PlayableMain.kt new file mode 100644 index 0000000..70291f4 --- /dev/null +++ b/fsm/core/src/jvmTest/kotlin/PlayableMain.kt @@ -0,0 +1,54 @@ +import dev.inmo.tgbotapi.libraries.fsm.core.* +import kotlinx.coroutines.* +import kotlin.random.Random +import kotlin.test.Test + +sealed interface TrafficLightState : ImmediateOrNeverState { + val trafficLightNumber: Int + override val context: Int + get() = trafficLightNumber +} +data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState +data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState +data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState + +class PlayableMain { + fun test() { + runBlocking { + val countOfTrafficLights = 10 + val initialStates = (0 until countOfTrafficLights).map { + when (0/*Random.nextInt(3)*/) { + 0 -> GreenCommon(it) + 1 -> YellowCommon(it) + else -> RedCommon(it) + } + } + + val statesManager = InMemoryStatesManager() + + val machine = StatesMachine( + statesManager, + listOf( + StateHandlerHolder(GreenCommon::class) { + delay(1000L) + YellowCommon(it.context).also(::println) + }, + StateHandlerHolder(YellowCommon::class) { + delay(1000L) + RedCommon(it.context).also(::println) + }, + StateHandlerHolder(RedCommon::class) { + delay(1000L) + GreenCommon(it.context).also(::println) + } + ) + ) + + initialStates.forEach { statesManager.startChain(it) } + + val scope = CoroutineScope(Dispatchers.Default) + machine.start(scope).join() + + } + } +}