From 6f9be2a9f838673f7839de11af4adbd58389a811 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 3 May 2023 23:47:05 +0600 Subject: [PATCH 1/6] start 0.18.1 and add SmartMutex --- CHANGELOG.md | 5 + .../inmo/micro_utils/coroutines/SmartMutex.kt | 136 ++++++++++++++++++ gradle.properties | 4 +- 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4f3d85761..3c74e713a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.18.1 + +* `Coroutines`: + * Add `SmartMutex` + ## 0.18.0 **ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED** diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt new file mode 100644 index 00000000000..c8a962d453c --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt @@ -0,0 +1,136 @@ +package dev.inmo.micro_utils.coroutines + +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the + * [SmartMutex] state. + * + * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need + * [Mutable] [SmartMutex]. Otherwise, [Immutable]. + * + * Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its + * [Mutable] creator + */ +sealed interface SmartMutex { + val lockStateFlow: StateFlow + + /** + * * True - locked + * * False - unlocked + */ + val isLocked: Boolean + get() = lockStateFlow.value + + /** + * Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow] + */ + class Immutable(override val lockStateFlow: StateFlow) : SmartMutex + + /** + * Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create + * [Immutable] variant of [this] instance with [immutable] factory + * + * @param locked Preset state of [isLocked] and its internal [_lockStateFlow] + */ + class Mutable(locked: Boolean = false) : SmartMutex { + private val _lockStateFlow = MutableStateFlow(locked) + override val lockStateFlow: StateFlow = _lockStateFlow.asStateFlow() + + private val internalChangesMutex = Mutex() + + fun immutable() = Immutable(lockStateFlow) + + /** + * Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will + * wait for [isLocked] == false and then try to lock + */ + suspend fun lock() { + do { + waitUnlock() + val shouldContinue = internalChangesMutex.withLock { + if (_lockStateFlow.value) { + true + } else { + _lockStateFlow.value = true + false + } + } + } while (shouldContinue && currentCoroutineContext().isActive) + } + + /** + * Will try to lock this [SmartMutex] immediataly + * + * @return True if lock was successful. False otherwise + */ + suspend fun tryLock(): Boolean { + return if (!_lockStateFlow.value) { + internalChangesMutex.withLock { + if (!_lockStateFlow.value) { + _lockStateFlow.value = true + true + } else { + false + } + } + } else { + false + } + } + + /** + * If [isLocked] == true - will change it to false and return true. If current call will not unlock this + * [SmartMutex] - false + */ + suspend fun unlock(): Boolean { + return if (_lockStateFlow.value) { + internalChangesMutex.withLock { + if (_lockStateFlow.value) { + _lockStateFlow.value = false + true + } else { + false + } + } + } else { + false + } + } + } +} + +/** + * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartMutex.Mutable.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + lock() + try { + return action() + } finally { + unlock() + } +} + +/** + * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. + * + * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact + * that some other parties may lock it again + */ +suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } diff --git a/gradle.properties b/gradle.properties index 5db53e2a94f..9f2f9380401 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.18.0 -android_code_version=191 +version=0.18.1 +android_code_version=192 From 13d0e1b682cf6640792e9d8e78ef9a42d2e573a2 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Thu, 4 May 2023 21:12:26 +0600 Subject: [PATCH 2/6] add MapDiff utils --- CHANGELOG.md | 2 + .../dev/inmo/micro_utils/common/MapDiff.kt | 110 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c74e713a2d..5965bb0e95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.18.1 +* `Common`: + * Add `MapDiff` * `Coroutines`: * Add `SmartMutex` diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt new file mode 100644 index 00000000000..3f0e43adb4c --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt @@ -0,0 +1,110 @@ +package dev.inmo.micro_utils.common + +/** + * Contains diff based on the comparison of objects with the same [K]. + * + * @param removed Contains map with keys removed from parent map + * @param changed Contains map with keys values changed new map in comparison with old one + * @param added Contains map with new keys and values + */ +data class MapDiff @Warning(warning) constructor( + val removed: Map, + val changed: Map>, + val added: Map +) { + fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() + inline fun isNotEmpty() = !isEmpty() + + companion object { + private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" + fun empty() = MapDiff(emptyMap(), emptyMap(), emptyMap()) + } +} + +private inline fun createCompareFun( + strictComparison: Boolean +): (K, V, V) -> Boolean = if (strictComparison) { + { _, first, second -> first === second } +} else { + { _, first, second -> first == second } +} + +/** + * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] + * + * @param compareFun Will be used to determine changed values + */ +fun Map.diff( + other: Map, + compareFun: (K, V, V) -> Boolean +): MapDiff { + val removed: Map = (keys - other.keys).associateWith { + getValue(it) + } + val added: Map = (other.keys - keys).associateWith { + other.getValue(it) + } + val changed = keys.intersect(other.keys).mapNotNull { + val old = getValue(it) + val new = other.getValue(it) + if (compareFun(it, old, new)) { + return@mapNotNull null + } else { + it to (old to new) + } + }.toMap() + + return MapDiff( + removed, + changed, + added + ) +} + +/** + * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] + * + * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard + * `equals` will be used + */ +fun Map.diff( + other: Map, + strictComparison: Boolean = false +): MapDiff = diff( + other, + compareFun = createCompareFun(strictComparison) +) + +/** + * Will apply changes with [other] map into [this] one + * + * @param compareFun Will be used to determine changed values + */ +fun MutableMap.applyDiff( + from: Map, + compareFun: (K, V, V) -> Boolean = { _, first, second -> first == second } +) { + diff(from, compareFun).apply { + removed.keys.forEach { remove(it) } + changed.forEach { (k, oldNew) -> + put(k, oldNew.second) + } + added.forEach { (k, new) -> + put(k, new) + } + } +} + +/** + * Will apply changes with [other] map into [this] one + * + * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard + * `equals` will be used + */ +fun MutableMap.applyDiff( + other: Map, + strictComparison: Boolean = false +) = applyDiff( + other, + compareFun = createCompareFun(strictComparison) +) From 149a1aa278202527b22116ca5435c61ddc34e623 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 5 May 2023 11:33:22 +0600 Subject: [PATCH 3/6] fix in new applyDiff --- .../commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt index 3f0e43adb4c..11371657298 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt @@ -82,7 +82,7 @@ fun Map.diff( */ fun MutableMap.applyDiff( from: Map, - compareFun: (K, V, V) -> Boolean = { _, first, second -> first == second } + compareFun: (K, V, V) -> Boolean ) { diff(from, compareFun).apply { removed.keys.forEach { remove(it) } From c9872a61b640872be6421a82b34a109ad2066d29 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 5 May 2023 11:49:09 +0600 Subject: [PATCH 4/6] now applyDiff will return its receiver --- .../dev/inmo/micro_utils/common/MapDiff.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt index 11371657298..7e3ca009c6c 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt @@ -76,15 +76,17 @@ fun Map.diff( ) /** - * Will apply changes with [other] map into [this] one + * Will apply changes with [from] map into [this] one * * @param compareFun Will be used to determine changed values + * + * @return [MapDiff] applied to [this] [MutableMap] */ fun MutableMap.applyDiff( from: Map, compareFun: (K, V, V) -> Boolean -) { - diff(from, compareFun).apply { +): MapDiff { + return diff(from, compareFun).apply { removed.keys.forEach { remove(it) } changed.forEach { (k, oldNew) -> put(k, oldNew.second) @@ -96,15 +98,17 @@ fun MutableMap.applyDiff( } /** - * Will apply changes with [other] map into [this] one + * Will apply changes with [from] map into [this] one * * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard * `equals` will be used + * + * @return [MapDiff] applied to [this] [MutableMap] */ fun MutableMap.applyDiff( - other: Map, + from: Map, strictComparison: Boolean = false -) = applyDiff( - other, +): MapDiff = applyDiff( + from, compareFun = createCompareFun(strictComparison) ) From 0a8453b4d220476d4001fe4b5b38d396fded369f Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 5 May 2023 11:58:27 +0600 Subject: [PATCH 5/6] add MapDiff#reversed and MutableMap#applyDiff(MapDiff) --- .../dev/inmo/micro_utils/common/MapDiff.kt | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt index 7e3ca009c6c..dc275c347d1 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt @@ -75,6 +75,23 @@ fun Map.diff( compareFun = createCompareFun(strictComparison) ) +/** + * Will apply [mapDiff] to [this] [MutableMap] + */ +fun MutableMap.applyDiff( + mapDiff: MapDiff +) { + mapDiff.apply { + removed.keys.forEach { remove(it) } + changed.forEach { (k, oldNew) -> + put(k, oldNew.second) + } + added.forEach { (k, new) -> + put(k, new) + } + } +} + /** * Will apply changes with [from] map into [this] one * @@ -86,14 +103,8 @@ fun MutableMap.applyDiff( from: Map, compareFun: (K, V, V) -> Boolean ): MapDiff { - return diff(from, compareFun).apply { - removed.keys.forEach { remove(it) } - changed.forEach { (k, oldNew) -> - put(k, oldNew.second) - } - added.forEach { (k, new) -> - put(k, new) - } + return diff(from, compareFun).also { + applyDiff(it) } } @@ -112,3 +123,13 @@ fun MutableMap.applyDiff( from, compareFun = createCompareFun(strictComparison) ) + +/** + * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the + * [MapDiff.changed] values will be reversed too + */ +fun MapDiff.reversed(): MapDiff = MapDiff( + removed = added, + changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, + added = removed +) From 4b26a92b378025fb61758db9a4de8cbb2b67b9d1 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 5 May 2023 12:01:29 +0600 Subject: [PATCH 6/6] improve old diff API --- .../dev/inmo/micro_utils/common/DiffUtils.kt | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt index 167c94a21fd..4f2f2635ac4 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt @@ -200,20 +200,18 @@ inline fun Iterable.calculateStrictDiff( ) = calculateDiff(other, strictComparison = true) /** - * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] - * mutable list + * Applies [diff] to [this] [MutableList] */ fun MutableList.applyDiff( - source: Iterable, - strictComparison: Boolean = false -): Diff = calculateDiff(source, strictComparison).also { - for (i in it.removed.indices.sortedDescending()) { - removeAt(it.removed[i].index) + diff: Diff +) { + for (i in diff.removed.indices.sortedDescending()) { + removeAt(diff.removed[i].index) } - it.added.forEach { (i, t) -> + diff.added.forEach { (i, t) -> add(i, t) } - it.replaced.forEach { (_, new) -> + diff.replaced.forEach { (_, new) -> set(new.index, new.value) } } @@ -222,17 +220,30 @@ fun MutableList.applyDiff( * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] * mutable list */ +fun MutableList.applyDiff( + source: Iterable, + strictComparison: Boolean = false +): Diff = calculateDiff(source, strictComparison).also { + applyDiff(it) +} + +/** + * This method call [calculateDiff] and then apply differences to [this] + * mutable list + */ fun MutableList.applyDiff( source: Iterable, comparisonFun: (T?, T?) -> Boolean ): Diff = calculateDiff(source, comparisonFun).also { - for (i in it.removed.indices.sortedDescending()) { - removeAt(it.removed[i].index) - } - it.added.forEach { (i, t) -> - add(i, t) - } - it.replaced.forEach { (_, new) -> - set(new.index, new.value) - } + applyDiff(it) } + +/** + * Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the + * [Diff.replaced] values will be reversed too + */ +fun Diff.reversed() = Diff( + removed = added, + replaced = replaced.map { it.second to it.first }, + added = removed +)