mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-02-16 19:52:03 +00:00
start add fsm
This commit is contained in:
parent
c8a25ce544
commit
11b0d059bf
17
fsm/common/build.gradle
Normal file
17
fsm/common/build.gradle
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
val context: Any
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class StateHandlerHolder<I : State>(
|
||||||
|
private val inputKlass: KClass<I>,
|
||||||
|
private val strict: Boolean = false,
|
||||||
|
private val delegateTo: StatesHandler<I>
|
||||||
|
) : StatesHandler<State> {
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
fun interface StatesHandler<I : State> {
|
||||||
|
suspend fun StatesMachine.handleState(state: I): State?
|
||||||
|
}
|
@ -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 <I : State> StatesMachine.launchStateHandling(
|
||||||
|
state: State,
|
||||||
|
handlers: List<StateHandlerHolder<out I>>
|
||||||
|
): State? {
|
||||||
|
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||||
|
handleState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatesMachine (
|
||||||
|
private val statesManager: StatesManager,
|
||||||
|
private val handlers: List<StateHandlerHolder<*>>
|
||||||
|
) : StatesHandler<State> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Pair<State, State>>
|
||||||
|
val onStartChain: Flow<State>
|
||||||
|
val onEndChain: Flow<State>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<State>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<Pair<State, State>>(0)
|
||||||
|
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
private val _onStartChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
|
||||||
|
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
|
private val contextsToStates = mutableMapOf<Any, State>()
|
||||||
|
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<State> = contextsToStates.values.toList()
|
||||||
|
|
||||||
|
}
|
@ -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<StateHandlerHolder<*>>()
|
||||||
|
|
||||||
|
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||||
|
states.add(StateHandlerHolder(kClass, false, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||||
|
states.add(StateHandlerHolder(kClass, true, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build() = StatesMachine(
|
||||||
|
statesManager,
|
||||||
|
states.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
|
||||||
|
add(I::class, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
|
||||||
|
addStrict(I::class, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildFSM(
|
||||||
|
block: FSMBuilder.() -> Unit
|
||||||
|
): StatesMachine = FSMBuilder().apply(block).build()
|
53
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
53
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
@ -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<GreenCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
YellowCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
strictlyOn<YellowCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
RedCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
strictlyOn<RedCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
GreenCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
this.statesManager = statesManager
|
||||||
|
}
|
||||||
|
|
||||||
|
initialStates.forEach { machine.startChain(it) }
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
machine.start(scope).join()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
fsm/common/src/main/AndroidManifest.xml
Normal file
1
fsm/common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
18
fsm/repos/build.gradle
Normal file
18
fsm/repos/build.gradle
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Any, State>,
|
||||||
|
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||||
|
) : StatesManager {
|
||||||
|
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||||
|
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
|
override val onStartChain: Flow<State> = 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<State> {
|
||||||
|
return keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
|
||||||
|
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
|
||||||
|
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
|
||||||
|
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
|
||||||
|
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
|
||||||
|
noinline outToStateTransformer: suspend TargetStateType.() -> State,
|
||||||
|
) = KeyValueBasedStatesManager(
|
||||||
|
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
|
||||||
|
contextToOutTransformer,
|
||||||
|
stateToOutTransformer,
|
||||||
|
outToContextTransformer,
|
||||||
|
outToStateTransformer
|
||||||
|
)
|
||||||
|
)
|
1
fsm/repos/src/main/AndroidManifest.xml
Normal file
1
fsm/repos/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.fsm.repos"/>
|
@ -28,6 +28,9 @@ String[] includes = [
|
|||||||
":serialization:encapsulator",
|
":serialization:encapsulator",
|
||||||
":serialization:typed_serializer",
|
":serialization:typed_serializer",
|
||||||
|
|
||||||
|
":fsm:common",
|
||||||
|
":fsm:repos",
|
||||||
|
|
||||||
":dokka"
|
":dokka"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user