diff --git a/fsm/common/build.gradle b/fsm/common/build.gradle new file mode 100644 index 00000000000..854f51c5dfb --- /dev/null +++ b/fsm/common/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":micro_utils.coroutines") + } + } + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/State.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/State.kt new file mode 100644 index 00000000000..818c15f0a35 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/State.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.fsm.common + +interface State { + val context: Any +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StateHandlerHolder.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StateHandlerHolder.kt new file mode 100644 index 00000000000..5b06f317c16 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StateHandlerHolder.kt @@ -0,0 +1,15 @@ +package dev.inmo.micro_utils.fsm.common + +import kotlin.reflect.KClass + +class StateHandlerHolder( + private val inputKlass: KClass, + private val strict: Boolean = false, + private val delegateTo: StatesHandler +) : StatesHandler { + fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state)) + + override suspend fun StatesMachine.handleState(state: State): State? { + return delegateTo.run { handleState(state as I) } + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesHandler.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesHandler.kt new file mode 100644 index 00000000000..b152b38dea6 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesHandler.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.fsm.common + +fun interface StatesHandler { + suspend fun StatesMachine.handleState(state: I): State? +} 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 new file mode 100644 index 00000000000..5c2ea9592d9 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt @@ -0,0 +1,46 @@ +package dev.inmo.micro_utils.fsm.common + +import dev.inmo.micro_utils.coroutines.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.asFlow + +private suspend fun StatesMachine.launchStateHandling( + state: State, + handlers: List> +): State? { + return handlers.firstOrNull { it.checkHandleable(state) } ?.run { + handleState(state) + } +} + +class StatesMachine ( + private val statesManager: StatesManager, + private val handlers: List> +) : StatesHandler { + override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers) + + fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { + val statePerformer: suspend (State) -> Unit = { state: State -> + val newState = launchStateHandling(state, handlers) + 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) } + } + } + + suspend fun startChain(state: State) { + statesManager.startChain(state) + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesManager.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesManager.kt new file mode 100644 index 00000000000..ac52d84b6e8 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesManager.kt @@ -0,0 +1,92 @@ +package dev.inmo.micro_utils.fsm.common + +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: State, new: State) + + /** + * 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: State) + + /** + * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just + * ignored + */ + suspend fun endChain(state: State) + + 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: State, new: State, currentNew: State) -> 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: State, new: State) = 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: State) = mapMutex.withLock { + if (!contextsToStates.containsKey(state.context)) { + contextsToStates[state.context] = state + _onStartChain.emit(state) + } + } + + private suspend fun endChainWithoutLock(state: State) { + if (contextsToStates[state.context] == state) { + contextsToStates.remove(state.context) + _onEndChain.emit(state) + } + } + + override suspend fun endChain(state: State) { + mapMutex.withLock { + endChainWithoutLock(state) + } + } + + override suspend fun getActiveStates(): List = contextsToStates.values.toList() + +} 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 new file mode 100644 index 00000000000..f0694f360ef --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt @@ -0,0 +1,35 @@ +package dev.inmo.micro_utils.fsm.common.dsl + +import dev.inmo.micro_utils.fsm.common.* +import kotlin.reflect.KClass + +class FSMBuilder( + var statesManager: StatesManager = InMemoryStatesManager() +) { + private var states = mutableListOf>() + + fun add(kClass: KClass, handler: StatesHandler) { + states.add(StateHandlerHolder(kClass, false, handler)) + } + + fun addStrict(kClass: KClass, handler: StatesHandler) { + states.add(StateHandlerHolder(kClass, true, handler)) + } + + fun build() = StatesMachine( + statesManager, + states.toList() + ) +} + +inline fun FSMBuilder.onStateOrSubstate(handler: StatesHandler) { + add(I::class, handler) +} + +inline fun FSMBuilder.strictlyOn(handler: StatesHandler) { + addStrict(I::class, handler) +} + +fun buildFSM( + block: FSMBuilder.() -> Unit +): StatesMachine = FSMBuilder().apply(block).build() diff --git a/fsm/common/src/jvmTest/kotlin/PlayableMain.kt b/fsm/common/src/jvmTest/kotlin/PlayableMain.kt new file mode 100644 index 00000000000..a317c2a5f17 --- /dev/null +++ b/fsm/common/src/jvmTest/kotlin/PlayableMain.kt @@ -0,0 +1,53 @@ +import dev.inmo.micro_utils.fsm.common.* +import dev.inmo.micro_utils.fsm.common.dsl.buildFSM +import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn +import kotlinx.coroutines.* + +sealed interface TrafficLightState : State { + 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 { +// @Test + 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 = buildFSM { + strictlyOn { + delay(1000L) + YellowCommon(it.context).also(::println) + } + strictlyOn { + delay(1000L) + RedCommon(it.context).also(::println) + } + strictlyOn { + delay(1000L) + GreenCommon(it.context).also(::println) + } + this.statesManager = statesManager + } + + initialStates.forEach { machine.startChain(it) } + + val scope = CoroutineScope(Dispatchers.Default) + machine.start(scope).join() + + } + } +} diff --git a/fsm/common/src/main/AndroidManifest.xml b/fsm/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..2a1eb7f83d7 --- /dev/null +++ b/fsm/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/fsm/repos/build.gradle b/fsm/repos/build.gradle new file mode 100644 index 00000000000..d20bb3f38a4 --- /dev/null +++ b/fsm/repos/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":micro_utils.fsm.common") + api project(":micro_utils.repos.common") + } + } + } +} diff --git a/fsm/repos/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/repos/KeyValueBasedStatesManager.kt b/fsm/repos/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/repos/KeyValueBasedStatesManager.kt new file mode 100644 index 00000000000..e53b8cee51d --- /dev/null +++ b/fsm/repos/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/repos/KeyValueBasedStatesManager.kt @@ -0,0 +1,84 @@ +package dev.inmo.micro_utils.fsm.repos + +import dev.inmo.micro_utils.fsm.common.State +import dev.inmo.micro_utils.fsm.common.StatesManager +import dev.inmo.micro_utils.repos.* +import dev.inmo.micro_utils.repos.mappers.withMapper +import dev.inmo.micro_utils.repos.pagination.getAll +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.reflect.KClass + +class KeyValueBasedStatesManager( + private val keyValueRepo: KeyValueRepo, + private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true } +) : StatesManager { + private val _onChainStateUpdated = MutableSharedFlow>(0) + override val onChainStateUpdated: Flow> = _onChainStateUpdated.asSharedFlow() + private val _onEndChain = MutableSharedFlow(0) + override val onEndChain: Flow = _onEndChain.asSharedFlow() + + override val onStartChain: Flow = keyValueRepo.onNewValue.map { it.second } + + private val mutex = Mutex() + + override suspend fun update(old: State, new: State) { + mutex.withLock { + when { + keyValueRepo.get(old.context) != old -> return@withLock + old.context == new.context || !keyValueRepo.contains(new.context) -> { + keyValueRepo.set(old.context, new) + _onChainStateUpdated.emit(old to new) + } + else -> { + val stateOnNewOneContext = keyValueRepo.get(new.context)!! + if (onContextsConflictResolver(old, new, stateOnNewOneContext)) { + endChainWithoutLock(stateOnNewOneContext) + keyValueRepo.unset(old.context) + keyValueRepo.set(new.context, new) + _onChainStateUpdated.emit(old to new) + } + } + } + + } + } + + override suspend fun startChain(state: State) { + if (!keyValueRepo.contains(state.context)) { + keyValueRepo.set(state.context, state) + } + } + + private suspend fun endChainWithoutLock(state: State) { + if (keyValueRepo.get(state.context) == state) { + keyValueRepo.unset(state.context) + _onEndChain.emit(state) + } + } + + override suspend fun endChain(state: State) { + mutex.withLock { endChainWithoutLock(state) } + } + + override suspend fun getActiveStates(): List { + return keyValueRepo.getAll { keys(it) }.map { it.second } + } + +} + +inline fun createStatesManager( + targetKeyValueRepo: KeyValueRepo, + noinline contextToOutTransformer: suspend Any.() -> TargetContextType, + noinline stateToOutTransformer: suspend State.() -> TargetStateType, + noinline outToContextTransformer: suspend TargetContextType.() -> Any, + noinline outToStateTransformer: suspend TargetStateType.() -> State, +) = KeyValueBasedStatesManager( + targetKeyValueRepo.withMapper( + contextToOutTransformer, + stateToOutTransformer, + outToContextTransformer, + outToStateTransformer + ) +) diff --git a/fsm/repos/src/main/AndroidManifest.xml b/fsm/repos/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..adc2fba3383 --- /dev/null +++ b/fsm/repos/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/settings.gradle b/settings.gradle index 1cdf43a6d64..805e1437532 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,9 @@ String[] includes = [ ":serialization:encapsulator", ":serialization:typed_serializer", + ":fsm:common", + ":fsm:repos", + ":dokka" ]