diff --git a/.github/workflows/dokka_push.yml b/.github/workflows/dokka_push.yml index 2fefc23dbf6..517ca2ae083 100644 --- a/.github/workflows/dokka_push.yml +++ b/.github/workflows/dokka_push.yml @@ -11,9 +11,9 @@ jobs: - uses: actions/setup-java@v1 with: java-version: 1.8 - - name: Fix android 31.0.0 dx + - name: Fix android 32.0.0 dx continue-on-error: true - run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar + run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar - name: Build run: ./gradlew dokkaHtml - name: Publish KDocs diff --git a/.github/workflows/packages_push.yml b/.github/workflows/packages_push.yml index 0991d752c1a..8a161775c4a 100644 --- a/.github/workflows/packages_push.yml +++ b/.github/workflows/packages_push.yml @@ -9,9 +9,9 @@ jobs: - uses: actions/setup-java@v1 with: java-version: 1.8 - - name: Fix android 31.0.0 dx + - name: Fix android 32.0.0 dx continue-on-error: true - run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar + run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar - name: Rewrite version run: | branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" diff --git a/CHANGELOG.md b/CHANGELOG.md index 12af78d0f75..c255949536c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.8.8 + +* `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 * `Ktor`: 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..deadfb62324 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/UpdatableStatesMachine.kt @@ -0,0 +1,58 @@ +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 { + if (previousState.dataOrNull() != actualState) { + 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..0668693b956 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,12 @@ import kotlin.reflect.KClass class FSMBuilder( var statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), + val fsmBuilder: (statesManager: StatesManager, states: List>) -> StatesMachine = { statesManager, states -> + StatesMachine( + statesManager, + states + ) + }, var defaultStateHandler: StatesHandler? = StatesHandler { null } ) { private var states = mutableListOf>() @@ -42,7 +48,7 @@ class FSMBuilder( add(filter, handler) } - fun build() = StatesMachine( + fun build() = fsmBuilder( statesManager, states.toList().let { list -> defaultStateHandler ?.let { list + it.holder { true } } ?: list diff --git a/gradle.properties b/gradle.properties index d8a06549686..df127369aba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,12 +24,12 @@ uuidVersion=0.3.1 core_ktx_version=1.7.0 androidx_recycler_version=1.2.1 -appcompat_version=1.3.1 +appcompat_version=1.4.0 android_minSdkVersion=19 -android_compileSdkVersion=31 -android_buildToolsVersion=31.0.0 -dexcount_version=3.0.0 +android_compileSdkVersion=32 +android_buildToolsVersion=32.0.0 +dexcount_version=3.0.1 junit_version=4.12 test_ext_junit_version=1.1.2 espresso_core=3.3.0 @@ -45,5 +45,5 @@ dokka_version=1.5.31 # Project data group=dev.inmo -version=0.8.7 -android_code_version=87 +version=0.8.8 +android_code_version=88