Compare commits

...

97 Commits

Author SHA1 Message Date
d83d30af06 actualize kslog, changelog and gradle.properties 2025-05-16 14:44:39 +06:00
284e763f0d update publish gradle scripts 2025-05-16 14:31:22 +06:00
3bfa172533 update github_release.gradle to allow it to publish with env instead of secret.gradle 2025-05-16 12:39:24 +06:00
b5b1fd6d5f rewrite publishing scripts with using of publication scripts generator 2025-05-16 00:53:12 +06:00
05e0d9b7d2 add clatch for uploading of publication 2025-05-15 22:18:43 +06:00
1ae1f8dee2 Revert "trying to add new plugin for publishing"
This reverts commit 1bf479a0b7.
2025-05-15 21:17:56 +06:00
1bf479a0b7 trying to add new plugin for publishing 2025-05-14 20:47:09 +06:00
9d04d49628 update dependencies and try to use new publishing 2025-05-13 23:41:27 +06:00
3de324519b start 0.25.6 2025-05-13 22:49:03 +06:00
4be90d0ea5 Merge pull request #567 from InsanusMokrassar/0.25.5
0.25.5
2025-04-14 10:24:01 +06:00
041f35ed6c add templates declaration in README.md 2025-04-14 10:09:33 +06:00
9c463d0338 add kdocs to koin common module 2025-04-14 10:03:26 +06:00
5ec0a46089 add KDocs for actor and safeActor 2025-04-14 09:53:24 +06:00
979d0ee4ca new model of templating 2025-04-14 09:43:07 +06:00
448686b399 Merge pull request #572 from 000Sanya/0.25.5
fix test running for wasm/js node
2025-04-14 08:54:53 +06:00
nullsanya
0ff149a3d3 fix test running for wasm/js node 2025-04-14 12:44:00 +10:00
50a90d6e96 update dependencies 2025-04-11 22:45:54 +06:00
5f93706d91 Small performance optimization of MutableMap.applyDiff 2025-04-11 22:40:55 +06:00
39415550f5 Merge pull request #568 from 000Sanya/0.25.5
fix test running for wasm/js browser
2025-04-11 22:40:47 +06:00
nullsanya
4586ad65b5 fix test running for wasm/js browser 2025-04-05 12:45:51 +10:00
8c5678e26f pre-fix of tests for wasmjs 2025-04-04 21:46:36 +06:00
5dff373d5f refactor: rename templates and optimize wasmJsMain blocks 2025-04-04 20:29:40 +06:00
e17868a085 Merge pull request #566 from 000Sanya/master
implement support of wasm/js for browser
2025-04-04 20:24:44 +06:00
97bb6d0936 start 0.25.5 2025-04-04 20:06:44 +06:00
nullsanya
54576d8dec implement support of wasm/js for browser 2025-04-03 21:25:26 +10:00
282bb24c71 Merge pull request #563 from InsanusMokrassar/0.25.4
0.25.4
2025-04-02 23:48:54 +06:00
b1a96b6ecb fix transactions dsl 2025-04-02 16:52:08 +06:00
66dac2086c update koin 2025-04-02 16:09:40 +06:00
e269d0d206 fix in transactions 2025-04-02 16:05:33 +06:00
5d95c3eb9c update dependencies 2025-03-31 11:30:56 +06:00
26650e9b6c add SmartKeyRWLocker.withWriteLocks extension with vararg keys 2025-03-27 20:54:41 +06:00
7339dd8354 start 0.25.4 2025-03-27 20:53:29 +06:00
8ae983971a Merge pull request #561 from InsanusMokrassar/0.25.3
0.25.3
2025-03-25 17:55:58 +06:00
d46cc3b09c remove redundant commentaries 2025-03-25 17:45:53 +06:00
dfd23f8d60 update kotlin 2025-03-25 17:42:32 +06:00
28eb1a11e6 add more tests for smart key rw locker tests 2025-03-25 17:27:34 +06:00
04f82a03bf add docs to SmartKeyRWLocker and SmartRWLocker 2025-03-24 14:21:36 +06:00
aac545074b fill changelog 2025-03-24 14:02:12 +06:00
87a3a925ee add multilocking for key-based lockers 2025-03-24 13:36:04 +06:00
5447bf9691 add one more test for SmartKeyRWLocker 2025-03-24 09:17:38 +06:00
761070b9b7 potentially first version of SmartKeyRWLocker 2025-03-23 11:16:45 +06:00
4c9e435df8 temporal realization of SmartKeyRWLocker 2025-03-22 16:02:08 +06:00
4b7d65e8b4 small logic fix in transactions dsl 2025-03-20 18:04:19 +06:00
0515b49b98 add transactions tests 2025-03-20 18:02:44 +06:00
edb97215ef add TransactionsDSL 2025-03-20 17:28:28 +06:00
5577a24548 fix of docs for HEXAColor 2025-03-19 18:10:20 +06:00
cfaa2a8927 add alsoWithUnlockingOnSuccess and alsoWithUnlockingOnSuccessAsync 2025-03-15 20:43:17 +06:00
78494b6036 start 0.25.3 2025-03-15 20:33:55 +06:00
f9ea7eca61 Merge pull request #557 from InsanusMokrassar/0.25.2
0.25.2
2025-03-14 16:40:12 +06:00
d69fee1732 add @OverrideRequireManualInvalidation to all invalidates supposed to be called in init block 2025-03-14 16:07:07 +06:00
178518db5e alsoInvalidateSync, alsoInvalidateSyncLogging, alsoDoInvalidate -> alsoInvalidateAsync 2025-03-14 15:01:22 +06:00
6fb20fb973 get back reflect in jvmMain of koin module 2025-03-14 14:16:46 +06:00
831bf44e34 alsoInvalidate, alsoDoInvalidate, singleSuspend, factorySuspend 2025-03-14 14:13:58 +06:00
a4c6c367e3 update exposed 2025-03-12 08:53:05 +06:00
bc98e59709 start 0.25.2 2025-03-12 08:41:51 +06:00
ef287bc331 Merge pull request #556 from InsanusMokrassar/0.25.1
0.25.1
2025-03-08 12:26:41 +06:00
3437f4c712 update changelog 2025-03-08 08:59:25 +06:00
618f2dcd79 update paged component 2025-03-08 07:59:27 +06:00
6df8ad3095 add calling of initial loadNext 2025-03-07 18:23:40 +06:00
eda6221288 fix of InfinityPagedComponent 2025-03-07 18:05:20 +06:00
a9859f6a0d moretests to god of tests 2025-03-05 19:48:16 +06:00
0db88bac25 fixes 2025-03-05 19:40:46 +06:00
daa3d9c0dd progress on test adaptation 2025-03-05 19:20:09 +06:00
b343b33594 buildable variant of map-like sorted tree 2025-03-05 16:23:39 +06:00
46e435a448 start 0.25.1 2025-03-05 14:14:57 +06:00
7fe62b4ffa Merge pull request #553 from InsanusMokrassar/0.25.0
0.25.0
2025-03-04 17:10:48 +06:00
9c94348a15 Merge branch 'master' into 0.25.0 2025-03-04 09:49:26 +06:00
bac256e93e Merge pull request #555 from InsanusMokrassar/0.24.9
0.24.9
2025-03-04 09:47:54 +06:00
49f59aa129 fill changelog 2025-03-04 09:47:38 +06:00
800dab5be0 rework of PagedComponent 2025-03-04 09:33:37 +06:00
b9977527b2 rework InfinityPagedComponent 2025-03-04 09:23:33 +06:00
c216dba69d hotfix in InfinityPagedComponent and PagedComponent 2025-03-03 21:16:34 +06:00
d4148d52e3 start hotfix 0.24.9 2025-03-03 21:15:36 +06:00
2006a8cdd0 Merge branch 'master' into 0.25.0 2025-03-03 19:41:13 +06:00
feb52ecbd1 fix of HandleSafelyCoroutineContextTest 2025-03-03 19:40:45 +06:00
42909c3b7a Merge branch 'master' into 0.25.0 2025-03-03 17:38:01 +06:00
706a787163 replace SortedBinaryTreeNodeTests in jvm-only 2025-03-03 17:37:22 +06:00
1bc14bded6 Merge branch 'master' into 0.25.0 2025-03-03 16:24:25 +06:00
f00cb81db1 Merge pull request #551 from InsanusMokrassar/0.24.8
0.24.8
2025-03-03 17:23:00 +07:00
2fbd14956d small update in SortedBinaryTreeNodeTests 2025-03-03 15:49:46 +06:00
24657b43be update InfinityPagedComponents 2025-03-03 15:17:54 +06:00
659d3b6fa5 update dependencies and fill changelog 2025-03-03 14:53:49 +06:00
339483c8a3 disable hard test of binary trees for js 2025-03-03 14:42:05 +06:00
51ec46bbd7 update timeout for deepInsertOnWorks 2025-03-03 13:40:33 +06:00
8a059cc26d fix of build 2025-03-03 12:21:04 +06:00
ddb8e1efb4 fix build 2025-03-03 12:15:36 +06:00
3bf2ed5168 fix of InfinityPagedComponent 2025-03-03 08:57:40 +06:00
e70d34d91a add tests for paged loading components 2025-03-02 23:06:42 +06:00
7a650f5c2f Disallow overrides in repos 2025-03-02 16:12:23 +06:00
fc6f5ae2ee start 0.25.0 2025-03-02 16:10:25 +06:00
3b7dde3cb1 add KDocs for InfinityPagedComponentContext and PagedComponent 2025-03-01 22:16:48 +06:00
0a5cfaba18 complete InfinityPagedComponentContext 2025-03-01 22:04:41 +06:00
ea527b5e91 start add paged/loading component 2025-03-01 16:49:20 +06:00
85f11439e8 adapt gradle files for new tests 2025-02-26 22:01:39 +06:00
98c7b48625 add SortedBinaryTreeNode 2025-02-26 20:57:17 +06:00
8b007bb3af start 0.24.8 2025-02-26 20:56:56 +06:00
89e2d88d1c Merge pull request #548 from InsanusMokrassar/0.24.7
0.24.7
2025-02-20 08:52:41 +06:00
157 changed files with 4139 additions and 771 deletions

View File

@@ -1,5 +1,90 @@
# Changelog
## 0.25.6
* `Versions`:
* `Kotlin`: `2.1.20` -> `2.1.21`
* `KSLog`: `1.4.1` -> `1.4.2`
* `Compose`: `1.7.3` -> `1.8.0`
* `Okio`: `3.10.2` -> `3.11.0`
## 0.25.5
* `Versions`:
* `Exposed`: `0.60.0` -> `0.61.0`
* `Serialization`: `1.8.0` -> `1.8.1`
* `Coroutines`: `1.10.1` -> `1.10.2`
* `Common`:
* Small performance optimization of `MutableMap.applyDiff`
## 0.25.4
* `Versions`:
* `Ktor`: `3.1.1` -> `3.1.2`
* `Koin`: `4.0.2` -> `4.0.4`
* `Coroutines`:
* Add `SmartKeyRWLocker.withWriteLocks` extension with vararg keys
* `Transactions`:
* Fix order of rollback actions calling
## 0.25.3
* `Coroutines`:
* Add extensions `SmartRWLocker.alsoWithUnlockingOnSuccessAsync` and `SmartRWLocker.alsoWithUnlockingOnSuccess`
* Fix of `SmartRWLocker.lockWrite` issue when it could lock write mutex without unlocking
* Add tool `SmartKeyRWLocker`
* `SmartSemaphore` got new property `maxPermits`
* New extension `SmartSemaphore.waitReleaseAll()`
* `Transactions`:
* Add `TransactionsDSL`
## 0.25.2
* `Versions`:
* `Exposed`: `0.59.0` -> `0.60.0`
* `Repo`:
* `Cache`:
* Add extensions `alsoInvalidate`, `alsoInvalidateAsync`, `alsoInvalidateSync` and `alsoInvalidateSyncLogging`
* `Koin`:
* Add extensions `singleSuspend` and `factorySuspend` for defining of dependencies with suspendable blocks
## 0.25.1
* `Coroutines`:
* Add `SortedMapLikeBinaryTreeNode`
* `Pagination`:
* `Compose`:
* One more rework of `InfinityPagedComponent` and `PagedComponent`
## 0.25.0
* `Repos`:
* `Cache`:
* All cache repos now do not have `open` vals - to avoid collisions in runtime
## 0.24.9
* `Pagination`:
* Make alternative constructor parameter `size` of `PaginationResult` with default value
* Add `Pagination.previousPage` extension
* `Compose`:
* Rework of `InfinityPagedComponentContext`
* Rework of `PagedComponent`
## 0.24.8
* `Versions`:
* `Ktor`: `3.1.0` -> `3.1.1`
* `KSP`: `2.1.10-1.0.30` -> `2.1.10-1.0.31`
* `Common`:
* `Compose`:
* Add component `LoadableComponent`
* `Coroutines`:
* Add `SortedBinaryTreeNode`
* `Pagination`:
* `Compose`:
* Add components `PagedComponent` and `InfinityPagedComponent`
## 0.24.7
* `Versions`:

View File

@@ -35,3 +35,51 @@ Most of complex modules are built with next hierarchy:
* `common` part contains routes which are common for clients and servers
* `client` submodule contains clients which are usually using `UnifiedRequester` to make requests using routes from `ktor/common` module and some internal logic of requests
* `server` submodule (in most cases `JVM`-only) contains some extensions for `Route` instances which usually will give opportunity to proxy internet requests from `ktor/client` realization to some proxy object
## Gradle Templates
All templates can be used by applying them in your project's build.gradle files using the `apply from` directive. For example:
```gradle
apply from: "$defaultProject"
```
In the sample has been used `defaultProject.gradle` as a basic template.
The project includes a collection of Gradle templates to simplify project setup and configuration. These templates are located in the `gradle/templates` directory and can be used to quickly set up different types of projects:
### Project Setup Templates
* `defaultProject.gradle` (usage `apply from: "$defaultProject"`) - Basic project configuration
* `defaultProjectWithSerialization.gradle` (usage `apply from: "$defaultProjectWithSerialization"`) - Project configuration with Kotlin Serialization support
* `mppJavaProject.gradle` (usage `apply from: "$mppJavaProject"`) - Multiplatform project with Java support
* `mppAndroidProject.gradle` (usage `apply from: "$mppAndroidProject"`) - Multiplatform project with Android support
### Multiplatform Configuration Templates
* `enableMPPAndroid.gradle` (usage `apply from: "$enableMPPAndroid"`) - Enable Android target in multiplatform project
* `enableMPPJs.gradle` (usage `apply from: "$enableMPPJs"`) - Enable JavaScript target in multiplatform project
* `enableMPPJvm.gradle` (usage `apply from: "$enableMPPJvm"`) - Enable JVM target in multiplatform project
* `enableMPPNativeArm64.gradle` (usage `apply from: "$enableMPPNativeArm64"`) - Enable ARM64 native target
* `enableMPPNativeX64.gradle` (usage `apply from: "$enableMPPNativeX64"`) - Enable x64 native target
* `enableMPPWasmJs.gradle` (usage `apply from: "$enableMPPWasmJs"`) - Enable WebAssembly JavaScript target
### Compose Integration Templates
* `addCompose.gradle` (usage `apply from: "$addCompose"`) - Basic Compose configuration
* `addComposeForAndroid.gradle` (usage `apply from: "$addComposeForAndroid"`) - Compose configuration for Android
* `addComposeForDesktop.gradle` (usage `apply from: "$addComposeForDesktop"`) - Compose configuration for Desktop
* `addComposeForJs.gradle` (usage `apply from: "$addComposeForJs"`) - Compose configuration for JavaScript
### Publishing Templates
* `publish.gradle` (usage `apply from: "$publish"`) - General publishing configuration
* `publish_jvm.gradle` (usage `apply from: "$publish_jvm"`) - JVM-specific publishing configuration
* `publish.kpsb` and `publish_jvm.kpsb` (usage `apply from: "$publish_kpsb"` and `apply from: "$publish_jvm_kpsb"`) - Publishing configuration for Kotlin Multiplatform and JVM
### Combined Project Templates
* `mppJvmJsWasmJsLinuxMingwProject.gradle` (usage `apply from: "$mppJvmJsWasmJsLinuxMingwProject"`) - Multiplatform project for JVM, JS, Wasm, Linux, and MinGW
* `mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project.gradle` (usage `apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"`) - Multiplatform project with additional Android and ARM64 support
* `mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project.gradle` (usage `apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"`) - Multiplatform project with Compose support
* `mppProjectWithSerializationAndCompose.gradle` (usage `apply from: "$mppProjectWithSerializationAndCompose"`) - Multiplatform project with both Serialization and Compose support

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -13,7 +13,7 @@ import kotlin.math.floor
*
* Anyway it is recommended to use
*
* @param hexaUInt rgba [UInt] in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
* @param hexaUInt rgba [UInt] in format `0xRRGGBBAA` where RR - red, GG - green, BB - blue` and AA - alpha
*/
@Serializable
@JvmInline
@@ -21,18 +21,18 @@ value class HEXAColor (
val hexaUInt: UInt
) : Comparable<HEXAColor> {
/**
* @returns [hexaUInt] as a string with format `#FFEEBBAA` where FF - red, EE - green, BB - blue and AA - alpha
* @returns [hexaUInt] as a string with format `#RRGGBBAA` where RR - red, GG - green, BB - blue and AA - alpha
*/
val hexa: String
get() = "#${hexaUInt.toString(16).padStart(8, '0')}"
/**
* @returns [hexaUInt] as a string with format `#FFEEBB` where FF - red, EE - green and BB - blue
* @returns [hexaUInt] as a string with format `#RRGGBB` where RR - red, GG - green and BB - blue
*/
val hex: String
get() = hexa.take(7)
/**
* @returns [hexaUInt] as a string with format `#AAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue
* @returns [hexaUInt] as a string with format `#AARRGGBB` where AA - alpha, RR - red, GG - green and BB - blue
*/
val ahex: String
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
@@ -140,12 +140,12 @@ value class HEXAColor (
}.lowercase().toUInt(16).let(::HEXAColor)
/**
* Creates [HEXAColor] from [uint] presume it is in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
* Creates [HEXAColor] from [uint] presume it is in format `0xRRGGBBAA` where RR - red, GG - green, BB - blue` and AA - alpha
*/
fun fromHexa(uint: UInt) = HEXAColor(uint)
/**
* Creates [HEXAColor] from [uint] presume it is in format `0xAAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue`
* Creates [HEXAColor] from [uint] presume it is in format `0xAARRGGBB` where AA - alpha, RR - red, GG - green and BB - blue`
*/
fun fromAhex(uint: UInt) = HEXAColor(
a = ((uint and 0xff000000u) / 0x1000000u).toInt(),

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {
@@ -29,5 +29,12 @@ kotlin {
api libs.okio
}
}
wasmJsMain {
dependencies {
api libs.kotlinx.browser
api libs.kt.coroutines
}
}
}
}

View File

@@ -6,13 +6,14 @@ plugins {
alias(libs.plugins.kt.jb.compose)
}
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
api libs.kt.coroutines
}
}
}

View File

@@ -0,0 +1,74 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.common.dataOrThrow
import dev.inmo.micro_utils.common.optional
class LoadableComponentContext<T> internal constructor(
presetOptional: Optional<T>,
) {
internal val iterationState: MutableState<Int> = mutableStateOf(0)
internal var dataOptional: Optional<T> = if (presetOptional.dataPresented) presetOptional else Optional.absent()
private set
internal val dataState: MutableState<Optional<T>> = mutableStateOf(dataOptional)
fun reload() {
iterationState.value++
}
}
/**
* Showing data with ability to reload data
*
* [block] will be shown when [loader] will complete loading. If you want to reload data, just call
* [LoadableComponentContext.reload]
*/
@Composable
fun <T> LoadableComponent(
preload: Optional<T>,
loader: suspend LoadableComponentContext<T>.() -> T,
block: @Composable LoadableComponentContext<T>.(T) -> Unit
) {
val context = remember { LoadableComponentContext(preload) }
LaunchedEffect(context.iterationState.value) {
context.dataState.value = loader(context).optional
}
context.dataState.let {
if (it.value.dataPresented) {
context.block(it.value.dataOrThrow(IllegalStateException("Data must be presented, but optional has been changed by some way")))
}
}
}
/**
* Showing data with ability to reload data
*
* [block] will be shown when [loader] will complete loading. If you want to reload data, just call
* [LoadableComponentContext.reload]
*/
@Composable
fun <T> LoadableComponent(
preload: T,
loader: suspend LoadableComponentContext<T>.() -> T,
block: @Composable LoadableComponentContext<T>.(T) -> Unit
) {
LoadableComponent(preload.optional, loader, block)
}
/**
* Showing data with ability to reload data
*
* [block] will be shown when [loader] will complete loading. If you want to reload data, just call
* [LoadableComponentContext.reload]
*/
@Composable
fun <T> LoadableComponent(
loader: suspend LoadableComponentContext<T>.() -> T,
block: @Composable LoadableComponentContext<T>.(T) -> Unit
) {
LoadableComponent(Optional.absent(), loader, block)
}

View File

@@ -0,0 +1,42 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import dev.inmo.micro_utils.common.compose.LoadableComponent
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jetbrains.annotations.TestOnly
import kotlin.test.Test
import kotlin.test.assertTrue
class LoadableComponentTests {
@OptIn(ExperimentalTestApi::class)
@Test
@TestOnly
fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = SpecialMutableStateFlow<Int>(0)
val loadedFlow = SpecialMutableStateFlow<Int>(0)
setContent {
LoadableComponent<Int>({
loadingFlow.filter { it == 1 }.first()
}) {
assert(dataState.value.data == 1)
remember {
loadedFlow.value = 2
}
}
}
waitForIdle()
assertTrue(loadedFlow.value == 0)
loadingFlow.value = 1
waitForIdle()
assertTrue(loadedFlow.value == 2)
}
}

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.common
/**
* Creates simple [Comparator] which will use [compareTo] of [T] for both objects
*/
fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }

View File

@@ -82,13 +82,11 @@ fun <K, V> MutableMap<K, V>.applyDiff(
mapDiff: MapDiff<K, V>
) {
mapDiff.apply {
removed.keys.forEach { remove(it) }
keys.removeAll(removed.keys)
changed.forEach { (k, oldNew) ->
put(k, oldNew.second)
}
added.forEach { (k, new) ->
put(k, new)
}
putAll(added)
}
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.common
import org.khronos.webgl.*
fun DataView.toByteArray() = ByteArray(this.byteLength) {
getInt8(it)
}
fun ArrayBuffer.toByteArray() = Int8Array(this).toByteArray()
fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also {
forEachIndexed { i, byte -> it.setInt8(i, byte) }
}
fun ByteArray.toArrayBuffer() = toDataView().buffer

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun copyToClipboard(text: String): Boolean {
return runCatching {
window.navigator.clipboard.writeText(
text
)
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,31 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import kotlinx.coroutines.await
import org.w3c.fetch.Response
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
external class ClipboardItem(data: JsAny?) : JsAny
fun createBlobData(blob: Blob): JsAny = js("""({[blob.type]: blob})""")
inline fun Blob.convertToClipboardItem(): ClipboardItem {
return ClipboardItem(createBlobData(this))
}
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
return runCatching {
val response = window.fetch(imageUrl).await<Response>()
val blob = response.blob().await<Blob>()
val data = arrayOf(
Blob(
arrayOf(blob).toJsArray().unsafeCast(),
BlobPropertyBag("image/png")
).convertToClipboardItem()
).toJsArray()
window.navigator.clipboard.write(data.unsafeCast())
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,63 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
private fun createMutationObserverInit(childList: Boolean, subtree: Boolean): JsAny = js("({childList, subtree})")
fun Node.onRemoved(block: () -> Unit): MutationObserver {
lateinit var observer: MutationObserver
observer = MutationObserver { _, _ ->
fun checkIfRemoved(node: Node): Boolean {
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
}
if (checkIfRemoved(this)) {
observer.disconnect()
block()
}
}
observer.observe(document, createMutationObserverInit(childList = true, subtree = true).unsafeCast())
return observer
}
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
var previousIntersectionRatio = -1f
val observer = IntersectionObserver { entries, observer ->
entries.toArray().forEach {
if (previousIntersectionRatio.toDouble() != it.intersectionRatio.toDouble()) {
previousIntersectionRatio = it.intersectionRatio.toDouble().toFloat()
it.block(previousIntersectionRatio, observer)
}
}
}
observer.observe(this)
return observer
}
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
var previous = -1f
onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio > 0 && previous == 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
var previous = -1f
return onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio == 0f && previous != 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.dom.DOMRect
import org.w3c.dom.Element
val DOMRect.isOnScreenByLeftEdge: Boolean
get() = left >= 0 && left <= window.innerWidth
inline val Element.isOnScreenByLeftEdge
get() = getBoundingClientRect().isOnScreenByLeftEdge
val DOMRect.isOnScreenByRightEdge: Boolean
get() = right >= 0 && right <= window.innerWidth
inline val Element.isOnScreenByRightEdge
get() = getBoundingClientRect().isOnScreenByRightEdge
internal val DOMRect.isOnScreenHorizontally: Boolean
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
val DOMRect.isOnScreenByTopEdge: Boolean
get() = top >= 0 && top <= window.innerHeight
inline val Element.isOnScreenByTopEdge
get() = getBoundingClientRect().isOnScreenByTopEdge
val DOMRect.isOnScreenByBottomEdge: Boolean
get() = bottom >= 0 && bottom <= window.innerHeight
inline val Element.isOnScreenByBottomEdge
get() = getBoundingClientRect().isOnScreenByBottomEdge
internal val DOMRect.isOnScreenVertically: Boolean
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
val DOMRect.isOnScreenFully: Boolean
get() = isOnScreenByLeftEdge && isOnScreenByTopEdge && isOnScreenByRightEdge && isOnScreenByBottomEdge
val Element.isOnScreenFully: Boolean
get() = getBoundingClientRect().isOnScreenFully
val DOMRect.isOnScreen: Boolean
get() = isOnScreenFully || (isOnScreenHorizontally && isOnScreenVertically)
inline val Element.isOnScreen: Boolean
get() = getBoundingClientRect().isOnScreen

View File

@@ -0,0 +1,127 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.DOMRectReadOnly
import org.w3c.dom.Element
external interface IntersectionObserverOptions: JsAny {
/**
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
* visible.
*/
var root: Element?
/**
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
*/
var rootMargin: String?
/**
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
*/
var threshold: JsArray<JsNumber>?
}
private fun createEmptyJsObject(): JsAny = js("{}")
fun IntersectionObserverOptions(
block: IntersectionObserverOptions.() -> Unit = {}
): IntersectionObserverOptions = createEmptyJsObject().unsafeCast<IntersectionObserverOptions>().apply(block)
external interface IntersectionObserverEntry: JsAny {
/**
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
* the documentation for Element.getBoundingClientRect().
*/
val boundingClientRect: DOMRectReadOnly
/**
* Returns the ratio of the intersectionRect to the boundingClientRect.
*/
val intersectionRatio: JsNumber
/**
* Returns a DOMRectReadOnly representing the target's visible area.
*/
val intersectionRect: DOMRectReadOnly
/**
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
* then you know the transition is from intersecting to not-intersecting.
*/
val isIntersecting: Boolean
/**
* Returns a DOMRectReadOnly for the intersection observer's root.
*/
val rootBounds: DOMRectReadOnly
/**
* The Element whose intersection with the root changed.
*/
val target: Element
/**
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
* IntersectionObserver's time origin.
*/
val time: Double
}
typealias IntersectionObserverCallback = (entries: JsArray<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
/**
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
*/
external class IntersectionObserver(callback: IntersectionObserverCallback): JsAny {
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
/**
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
* was passed to the constructor or its value is null, the top-level document's viewport is used.
*/
val root: Element
/**
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
*/
val rootMargin: String
/**
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
* crossed for that target. If no value was passed to the constructor, 0 is used.
*/
val thresholds: JsArray<JsNumber>
/**
* Stops the IntersectionObserver object from observing any target.
*/
fun disconnect()
/**
* Tells the IntersectionObserver a target element to observe.
*/
fun observe(targetElement: Element)
/**
* Returns an array of IntersectionObserverEntry objects for all observed targets.
*/
fun takeRecords(): JsArray<IntersectionObserverEntry>
/**
* Tells the IntersectionObserver to stop observing a particular target element.
*/
fun unobserve(targetElement: Element)
}

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.Element
inline val Element.isOverflowWidth
get() = scrollWidth > clientWidth
inline val Element.isOverflowHeight
get() = scrollHeight > clientHeight
inline val Element.isOverflow
get() = isOverflowHeight || isOverflowWidth

View File

@@ -0,0 +1,58 @@
package dev.inmo.micro_utils.common
import kotlinx.coroutines.await
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent
import org.w3c.files.*
import kotlin.js.Promise
/**
* @suppress
*/
actual typealias MPPFile = File
private fun createJsError(message: String): JsAny = js("Error(message)")
fun MPPFile.readBytesPromise() = Promise { success, failure ->
val reader = FileReader()
reader.onload = {
success((reader.result as ArrayBuffer))
}
reader.onerror = {
val message = (it as ErrorEvent).message
failure(createJsError(message))
}
reader.readAsArrayBuffer(this)
}
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await<ArrayBuffer>().toByteArray()
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = jsNumberToBigInt(size).toLong()
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes
private fun jsNumberToBigInt(number: JsNumber): JsBigInt = js("BigInt(number)")

View File

@@ -0,0 +1,41 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
private fun createEventListener(listener: (Event) -> Unit): JsAny = js("({handleEvent: listener})")
fun Element.onActionOutside(type: String, options: AddEventListenerOptions? = null, callback: (Event) -> Unit): EventListener {
lateinit var observer: MutationObserver
val listener = createEventListener { it: Event ->
val elementsToCheck = mutableListOf(this@onActionOutside)
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
val childrenGettingElement = elementsToCheck.removeFirst()
for (i in 0 until childrenGettingElement.childElementCount) {
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
}
}
if (elementsToCheck.isEmpty()) {
callback(it)
}
}.unsafeCast<EventListener>()
if (options == null) {
document.addEventListener(type, listener)
} else {
document.addEventListener(type, listener, options)
}
observer = onRemoved {
if (options == null) {
document.removeEventListener(type, listener)
} else {
document.removeEventListener(type, listener, options)
}
observer.disconnect()
}
return listener
}
fun Element.onClickOutside(options: AddEventListenerOptions? = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun openLink(link: String, target: String = "_blank", features: String = "") {
window.open(link, target, features) ?.focus()
}

View File

@@ -0,0 +1,56 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.*
external class ResizeObserver(
callback: (JsArray<ResizeObserverEntry>, ResizeObserver) -> Unit
): JsAny {
fun observe(target: Element, options: JsAny = definedExternally)
fun unobserve(target: Element)
fun disconnect()
}
private fun createObserveOptions(jsBox: JsString?): JsAny = js("({box: jsBox})")
external interface ResizeObserverSize: JsAny {
val blockSize: Float
val inlineSize: Float
}
external interface ResizeObserverEntry: JsAny {
val borderBoxSize: JsArray<ResizeObserverSize>
val contentBoxSize: JsArray<ResizeObserverSize>
val devicePixelContentBoxSize: JsArray<ResizeObserverSize>
val contentRect: DOMRectReadOnly
val target: Element
}
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
target,
createObserveOptions(options.box?.name?.toJsString())
)
class ResizeObserverObserveOptions(
val box: Box? = null
) {
sealed interface Box {
val name: String
object Content : Box {
override val name: String
get() = "content-box"
}
object Border : Box {
override val name: String
get() = "border-box"
}
object DevicePixelContent : Box {
override val name: String
get() = "device-pixel-content-box"
}
}
}

View File

@@ -0,0 +1,30 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import kotlinx.dom.createElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.files.get
fun selectFile(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {},
onFile: (MPPFile) -> Unit
) {
(document.createElement("input") {
(this as HTMLInputElement).apply {
type = "file"
onchange = {
runCatching {
files ?.get(0) ?: error("File must not be null")
}.onSuccess {
onFile(it)
}.onFailure {
onFailure(it)
}
}
inputSetup(this)
}
} as HTMLElement).click()
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
fun triggerDownloadFile(filename: String, fileLink: String) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
hiddenElement.href = fileLink
hiddenElement.target = "_blank"
hiddenElement.download = filename
hiddenElement.click()
}

View File

@@ -0,0 +1,48 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.dom.Element
import org.w3c.dom.css.CSSStyleDeclaration
sealed class Visibility
data object Visible : Visibility()
data object Invisible : Visibility()
data object Gone : Visibility()
var CSSStyleDeclaration.visibilityState: Visibility
get() = when {
display == "none" -> Gone
visibility == "hidden" -> Invisible
else -> Visible
}
set(value) {
when (value) {
Visible -> {
if (display == "none") {
display = "initial"
}
visibility = "visible"
}
Invisible -> {
if (display == "none") {
display = "initial"
}
visibility = "hidden"
}
Gone -> {
display = "none"
}
}
}
inline var Element.visibilityState: Visibility
get() = window.getComputedStyle(this).visibilityState
set(value) {
window.getComputedStyle(this).visibilityState = value
}
inline val Element.isVisible: Boolean
get() = visibilityState == Visible
inline val Element.isInvisible: Boolean
get() = visibilityState == Invisible
inline val Element.isGone: Boolean
get() = visibilityState == Gone

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.common
actual fun Float.fixed(signs: Int): Float {
return jsToFixed(toDouble().toJsNumber(), signs.coerceIn(FixedSignsRange)).toString().toFloat()
}
actual fun Double.fixed(signs: Int): Double {
return jsToFixed(toJsNumber(), signs.coerceIn(FixedSignsRange)).toString().toDouble()
}
private fun jsToFixed(number: JsNumber, signs: Int): JsString = js("number.toFixed(signs)")

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -6,7 +6,7 @@ plugins {
alias(libs.plugins.kt.jb.compose)
}
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -4,6 +4,15 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
/**
* Creates an actor using coroutines that processes incoming messages of type [T].
* An actor is a computational entity that processes messages sequentially in response to messages it receives.
*
* @param T The type of messages this actor will process
* @param channelCapacity The capacity of the [Channel] used for message delibery to the actor. Defaults to [Channel.UNLIMITED]
* @param block The processing logic to be applied to each received message
* @return A [Channel] that can be used to send messages to this actor or cancel it
*/
fun <T> CoroutineScope.actor(
channelCapacity: Int = Channel.UNLIMITED,
block: suspend (T) -> Unit
@@ -13,6 +22,16 @@ fun <T> CoroutineScope.actor(
return channel
}
/**
* Creates a safe actor that catches and handles exceptions during message processing.
* This variant wraps the processing logic in a safety mechanism to prevent actor failure due to exceptions.
*
* @param T The type of messages this actor will process
* @param channelCapacity The capacity of the [Channel] used for message processing. Defaults to [Channel.UNLIMITED]
* @param onException Handler for exceptions that occur during message processing. Defaults to [defaultSafelyExceptionHandler]
* @param block The processing logic to be applied to each received message
* @return A [Channel] that can be used to send messages to this actor
*/
inline fun <T> CoroutineScope.safeActor(
channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -71,7 +71,7 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap)
}
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
val job = subscribeLoggingDropExceptions(subscope) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
actor.send(
AsyncSubscriptionCommandClearReceiver(marker)

View File

@@ -0,0 +1,226 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Combining [globalRWLocker] and internal map of [SmartRWLocker] associated by [T] to provide next logic:
*
* * Locker by key, for reading: waiting for [globalRWLocker] unlock write by acquiring read permit in it and then
* taking or creating locker for key [T] and lock its reading too
* * Locker by key, for writing: waiting for [globalRWLocker] unlock write by acquiring read permit in it and then
* taking or creating locker for key [T] and lock its writing
* * [globalRWLocker], for reading: using [SmartRWLocker.acquireRead], will be suspended until its
* [SmartRWLocker.lockWrite] will not be unlocked
* * [globalRWLocker], for writing: using [SmartRWLocker.lockWrite], will be paused by other reading and writing
* operations and will pause other operations until the end of operation (calling of [unlockWrite])
*
* You may see, that lockers by key still will use global locker permits - it is required to prevent [globalRWLocker]
* write locking during all other operations are not done. In fact, all the keys works like a simple [SmartRWLocker] as
* well, as [globalRWLocker], but they are linked with [globalRWLocker] [SmartRWLocker.acquireRead] permissions
*/
class SmartKeyRWLocker<T>(
globalLockerReadPermits: Int = Int.MAX_VALUE,
globalLockerWriteIsLocked: Boolean = false,
private val perKeyReadPermits: Int = Int.MAX_VALUE
) {
private val globalRWLocker: SmartRWLocker = SmartRWLocker(
readPermits = globalLockerReadPermits,
writeIsLocked = globalLockerWriteIsLocked
)
private val lockers = mutableMapOf<T, SmartRWLocker>()
private val lockersMutex = Mutex()
private val lockersWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE)
private val globalWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE)
private fun allocateLockerWithoutLock(key: T) = lockers.getOrPut(key) {
SmartRWLocker(perKeyReadPermits)
}
private suspend fun allocateLocker(key: T) = lockersMutex.withLock {
lockers.getOrPut(key) {
SmartRWLocker(perKeyReadPermits)
}
}
suspend fun writeMutex(key: T): SmartMutex.Immutable = globalRWLocker.withReadAcquire {
allocateLockerWithoutLock(key).writeMutex
}
suspend fun readSemaphore(key: T): SmartSemaphore.Immutable = globalRWLocker.withReadAcquire {
allocateLockerWithoutLock(key).readSemaphore
}
fun writeMutexOrNull(key: T): SmartMutex.Immutable? = lockers[key] ?.writeMutex
fun readSemaphoreOrNull(key: T): SmartSemaphore.Immutable? = lockers[key] ?.readSemaphore
fun writeMutex(): SmartMutex.Immutable = globalRWLocker.writeMutex
fun readSemaphore(): SmartSemaphore.Immutable = globalRWLocker.readSemaphore
suspend fun acquireRead() {
globalWritingLocker.acquire()
try {
lockersWritingLocker.waitReleaseAll()
globalRWLocker.acquireRead()
} catch (e: CancellationException) {
globalWritingLocker.release()
throw e
}
}
suspend fun releaseRead(): Boolean {
globalWritingLocker.release()
return globalRWLocker.releaseRead()
}
suspend fun lockWrite() {
globalRWLocker.lockWrite()
}
suspend fun unlockWrite(): Boolean {
return globalRWLocker.unlockWrite()
}
fun isWriteLocked(): Boolean = globalRWLocker.writeMutex.isLocked == true
suspend fun acquireRead(key: T) {
globalRWLocker.acquireRead()
val locker = allocateLocker(key)
try {
locker.acquireRead()
} catch (e: CancellationException) {
globalRWLocker.releaseRead()
throw e
}
}
suspend fun releaseRead(key: T): Boolean {
val locker = allocateLocker(key)
return locker.releaseRead() && globalRWLocker.releaseRead()
}
suspend fun lockWrite(key: T) {
globalWritingLocker.withAcquire(globalWritingLocker.maxPermits) {
lockersWritingLocker.acquire()
}
try {
globalRWLocker.acquireRead()
try {
val locker = allocateLocker(key)
locker.lockWrite()
} catch (e: CancellationException) {
globalRWLocker.releaseRead()
throw e
}
} catch (e: CancellationException) {
lockersWritingLocker.release()
throw e
}
}
suspend fun unlockWrite(key: T): Boolean {
val locker = allocateLocker(key)
return (locker.unlockWrite() && globalRWLocker.releaseRead()).also {
if (it) {
lockersWritingLocker.release()
}
}
}
fun isWriteLocked(key: T): Boolean = lockers[key] ?.writeMutex ?.isLocked == true
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquire(action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquireRead()
try {
return action()
} finally {
releaseRead()
}
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLock(action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lockWrite()
try {
return action()
} finally {
unlockWrite()
}
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquire(key: T, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquireRead(key)
try {
return action()
} finally {
releaseRead(key)
}
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquires(keys: Iterable<T>, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val acquired = mutableSetOf<T>()
try {
keys.forEach {
acquireRead(it)
acquired.add(it)
}
return action()
} finally {
acquired.forEach {
releaseRead(it)
}
}
}
suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquires(vararg keys: T, action: () -> R): R = withReadAcquires(keys.asIterable(), action)
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLock(key: T, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lockWrite(key)
try {
return action()
} finally {
unlockWrite(key)
}
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLocks(keys: Iterable<T>, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val locked = mutableSetOf<T>()
try {
keys.forEach {
lockWrite(it)
locked.add(it)
}
return action()
} finally {
locked.forEach {
unlockWrite(it)
}
}
}
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLocks(vararg keys: T, action: () -> R): R = withWriteLocks(keys.asIterable(), action)

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@@ -7,9 +8,10 @@ import kotlin.contracts.contract
/**
* Composite mutex which works with next rules:
*
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
* * [acquireRead] require to [writeMutex] to be free. Then it will take one lock from [readSemaphore]
* * [releaseRead] will just free up one permit in [readSemaphore]
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed. If coroutine will be
* cancelled during read semaphore freeing, locking will be cancelled too with [SmartMutex.Mutable.unlock]ing of [writeMutex]
* * [unlockWrite] will just unlock [writeMutex]
*/
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
@@ -39,7 +41,12 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
*/
suspend fun lockWrite() {
_writeMutex.lock()
_readSemaphore.acquire(readPermits)
try {
_readSemaphore.acquire(readPermits)
} catch (e: CancellationException) {
_writeMutex.unlock()
throw e
}
}
/**

View File

@@ -0,0 +1,23 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
suspend inline fun alsoWithUnlockingOnSuccess(
vararg lockers: SmartRWLocker,
block: suspend () -> Unit
): Result<Unit> {
return runCatching {
block()
}.onSuccess {
lockers.forEach { it.unlockWrite() }
}
}
fun alsoWithUnlockingOnSuccessAsync(
scope: CoroutineScope,
vararg lockers: SmartRWLocker,
block: suspend () -> Unit
): Job = scope.launchLoggingDropExceptions {
alsoWithUnlockingOnSuccess(*lockers, block = block)
}

View File

@@ -24,6 +24,7 @@ import kotlin.contracts.contract
* [Mutable] creator
*/
sealed interface SmartSemaphore {
val maxPermits: Int
val permitsStateFlow: StateFlow<Int>
/**
@@ -36,7 +37,7 @@ sealed interface SmartSemaphore {
/**
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
*/
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
class Immutable(override val permitsStateFlow: StateFlow<Int>, override val maxPermits: Int) : SmartSemaphore
/**
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
@@ -44,15 +45,16 @@ sealed interface SmartSemaphore {
*
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
*/
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
override val maxPermits: Int = permits
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false)
fun immutable() = Immutable(permitsStateFlow)
fun immutable() = Immutable(permitsStateFlow, maxPermits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.maxPermits)
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
@@ -126,10 +128,10 @@ sealed interface SmartSemaphore {
*/
suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value < this.permits) {
return if (_freePermitsStateFlow.value < this.maxPermits) {
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < this.permits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
if (_freePermitsStateFlow.value < this.maxPermits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.maxPermits)
true
} else {
false
@@ -166,3 +168,4 @@ suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, acti
* the fact that some other parties may lock it again
*/
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }
suspend fun SmartSemaphore.waitReleaseAll() = permitsStateFlow.first { it == maxPermits }

View File

@@ -0,0 +1,318 @@
package dev.inmo.micro_utils.coroutines.collections
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import kotlinx.coroutines.job
import kotlinx.serialization.Serializable
import kotlin.coroutines.coroutineContext
/**
* Creates simple [Comparator] which will use [compareTo] of [T] for both objects
*/
private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }
@Serializable
class SortedBinaryTreeNode<T>(
val data: T,
internal val comparator: Comparator<T>,
) : Iterable<SortedBinaryTreeNode<T>> {
internal var leftNode: SortedBinaryTreeNode<T>? = null
internal var rightNode: SortedBinaryTreeNode<T>? = null
internal val locker: SmartRWLocker by lazy {
SmartRWLocker()
}
suspend fun getLeftNode() = locker.withReadAcquire {
leftNode
}
suspend fun getRightNode() = locker.withReadAcquire {
rightNode
}
suspend fun getLeft() = getLeftNode() ?.data
suspend fun getRight() = getRightNode() ?.data
override fun equals(other: Any?): Boolean {
return other === this || (other is SortedBinaryTreeNode<*> && other.data == data && other.rightNode == rightNode && other.leftNode == leftNode)
}
override fun hashCode(): Int {
return data.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode()
}
suspend fun size(): Int {
return locker.withReadAcquire {
1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0)
}
}
/**
* This [Iterator] will run from less to greater values of nodes starting the
* [dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode]-receiver. Due to non-suspending
* nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It
* means, that tree can be changed during to iteration process
*/
override fun iterator(): Iterator<SortedBinaryTreeNode<T>> = iterator {
leftNode ?.let {
it.iterator().forEach { yield(it) }
}
yield(this@SortedBinaryTreeNode)
rightNode ?.let {
it.iterator().forEach { yield(it) }
}
}
override fun toString(): String {
return "$data($leftNode;$rightNode)"
}
companion object {
operator fun <T : Comparable<T>> invoke(
data: T,
) = SortedBinaryTreeNode(
data,
data.createComparator()
)
}
}
/**
* Will add subnode in tree if there are no any node with [newData]
*
* * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node,
* will be used [SortedBinaryTreeNode.rightNode]
* * If [newData] is equal to [SortedBinaryTreeNode.data] of currently
* checking node - will be returned currently checking node
* * If [newData] is less than [SortedBinaryTreeNode.data] of currently
* checking node - will be used [SortedBinaryTreeNode.leftNode]
*
* This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or
* [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
*/
private suspend fun <T> SortedBinaryTreeNode<T>.upsertSubNode(
subNode: SortedBinaryTreeNode<T>,
skipLockers: Set<SmartRWLocker> = emptySet()
): SortedBinaryTreeNode<T> {
var currentlyChecking = this
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (coroutineContext.job.isActive) {
if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) {
currentlyChecking.locker.lockWrite()
lockedLockers.add(currentlyChecking.locker)
}
val left = currentlyChecking.leftNode
val right = currentlyChecking.rightNode
val comparingResult = currentlyChecking.comparator.compare(subNode.data, currentlyChecking.data)
val isGreater = comparingResult > 0
when {
comparingResult == 0 -> return currentlyChecking
isGreater && right == null -> {
currentlyChecking.rightNode = subNode
return subNode
}
isGreater && right != null -> {
currentlyChecking = right
}
left == null -> {
currentlyChecking.leftNode = subNode
return subNode
}
else -> {
currentlyChecking = left
}
}
}
} finally {
lockedLockers.forEach {
runCatching { it.unlockWrite() }
}
}
error("Unable to add node")
}
/**
* Will add subnode in tree if there are no any node with [newData]
*
* * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node,
* will be used [SortedBinaryTreeNode.rightNode]
* * If [newData] is equal to [SortedBinaryTreeNode.data] of currently
* checking node - will be returned currently checking node
* * If [newData] is less than [SortedBinaryTreeNode.data] of currently
* checking node - will be used [SortedBinaryTreeNode.leftNode]
*
* This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or
* [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
*/
suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(newData: T): SortedBinaryTreeNode<T> {
return upsertSubNode(
SortedBinaryTreeNode(newData, comparator)
)
}
suspend fun <T> SortedBinaryTreeNode<T>.findParentNode(data: T): SortedBinaryTreeNode<T>? {
var currentParent: SortedBinaryTreeNode<T>? = null
var currentlyChecking: SortedBinaryTreeNode<T>? = this
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (coroutineContext.job.isActive) {
if (currentlyChecking == null) {
return null
}
if (currentlyChecking.locker !in lockedLockers) {
currentlyChecking.locker.acquireRead()
lockedLockers.add(currentlyChecking.locker)
}
val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data)
when {
comparingResult > 0 -> {
currentParent = currentlyChecking
currentlyChecking = currentlyChecking.rightNode
continue
}
comparingResult < 0 -> {
currentParent = currentlyChecking
currentlyChecking = currentlyChecking.leftNode
continue
}
else -> return currentParent
}
}
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
error("Unable to find node")
}
/**
* Will remove (detach) node from tree starting with [this] [SortedBinaryTreeNode]
*
* @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached;
* null otherwise
*/
suspend fun <T> SortedBinaryTreeNode<T>.removeSubNode(data: T): Pair<SortedBinaryTreeNode<T>, SortedBinaryTreeNode<T>>? {
val onFoundToRemoveCallback: suspend SortedBinaryTreeNode<T>.(left: SortedBinaryTreeNode<T>?, right: SortedBinaryTreeNode<T>?) -> Unit = { left, right ->
left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker)) }
right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker)) }
}
while (coroutineContext.job.isActive) {
val foundParentNode = findParentNode(data) ?: return null
foundParentNode.locker.withWriteLock {
val left = foundParentNode.leftNode
val right = foundParentNode.rightNode
when {
left != null && left.comparator.compare(data, left.data) == 0 -> {
foundParentNode.leftNode = null
foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode)
return foundParentNode to left
}
right != null && right.comparator.compare(data, right.data) == 0 -> {
foundParentNode.rightNode = null
foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode)
return foundParentNode to right
}
else -> {
return@withWriteLock // data has been changed, new search required
}
}
}
}
error("Unable to remove node")
}
suspend fun <T> SortedBinaryTreeNode<T>.findNode(data: T): SortedBinaryTreeNode<T>? {
var currentlyChecking: SortedBinaryTreeNode<T>? = this
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (coroutineContext.job.isActive) {
if (currentlyChecking == null) {
return null
}
if (currentlyChecking.locker !in lockedLockers) {
currentlyChecking.locker.acquireRead()
lockedLockers.add(currentlyChecking.locker)
}
val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data)
when {
comparingResult > 0 -> {
currentlyChecking = currentlyChecking.rightNode
continue
}
comparingResult < 0 -> {
currentlyChecking = currentlyChecking.leftNode
continue
}
else -> return currentlyChecking
}
}
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
error("Unable to find node")
}
suspend fun <T> SortedBinaryTreeNode<T>.contains(data: T): Boolean = findNode(data) != null
suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRange(from: T, to: T, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedBinaryTreeNode<T>> {
val results = mutableSetOf<SortedBinaryTreeNode<T>>()
val leftToCheck = mutableSetOf(this)
val lockedLockers = mutableSetOf<SmartRWLocker>()
val fromComparingFun: (SortedBinaryTreeNode<T>) -> Boolean = if (fromInclusiveMode) {
{ it.comparator.compare(from, it.data) <= 0 }
} else {
{ it.comparator.compare(from, it.data) < 0 }
}
val toComparingFun: (SortedBinaryTreeNode<T>) -> Boolean = if (toInclusiveMode) {
{ it.comparator.compare(to, it.data) >= 0 }
} else {
{ it.comparator.compare(to, it.data) > 0 }
}
try {
while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) {
val currentlyChecking = leftToCheck.first()
leftToCheck.remove(currentlyChecking)
if (currentlyChecking in results) {
continue
}
currentlyChecking.locker.acquireRead()
lockedLockers.add(currentlyChecking.locker)
if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) {
results.add(currentlyChecking)
currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
continue
}
when {
currentlyChecking.comparator.compare(to, currentlyChecking.data) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
currentlyChecking.comparator.compare(from, currentlyChecking.data) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
}
}
return results.toSet()
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
error("Unable to find nodes range")
}
suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRange(from: T, to: T): Set<SortedBinaryTreeNode<T>> = findNodesInRange(
from = from,
to = to,
fromInclusiveMode = true,
toInclusiveMode = true
)
suspend fun <T> SortedBinaryTreeNode<T>.findNodesInRangeExcluding(from: T, to: T): Set<SortedBinaryTreeNode<T>> = findNodesInRange(
from = from,
to = to,
fromInclusiveMode = false,
toInclusiveMode = false
)
suspend fun <T : Comparable<T>> SortedBinaryTreeNode<T>.findNodesInRange(range: ClosedRange<T>): Set<SortedBinaryTreeNode<T>> = findNodesInRange(
from = range.start,
to = range.endInclusive,
)

View File

@@ -0,0 +1,401 @@
package dev.inmo.micro_utils.coroutines.collections
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import kotlinx.coroutines.job
import kotlinx.serialization.Serializable
import kotlin.coroutines.coroutineContext
/**
* Creates simple [Comparator] which will use [compareTo] of [T] for both objects
*/
private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }
@Serializable
class SortedMapLikeBinaryTreeNode<K, V>(
val key: K,
val value: V,
internal val comparator: Comparator<K>,
) : Iterable<SortedMapLikeBinaryTreeNode<K, V>> {
internal var leftNode: SortedMapLikeBinaryTreeNode<K, V>? = null
internal var rightNode: SortedMapLikeBinaryTreeNode<K, V>? = null
internal val locker: SmartRWLocker by lazy {
SmartRWLocker()
}
suspend fun getLeftNode() = locker.withReadAcquire {
leftNode
}
suspend fun getRightNode() = locker.withReadAcquire {
rightNode
}
suspend fun getLeftKey() = getLeftNode() ?.key
suspend fun getLeftValue() = getLeftNode() ?.value
suspend fun getRightKey() = getRightNode() ?.value
suspend fun getRightValue() = getRightNode() ?.value
override fun equals(other: Any?): Boolean {
return other === this || (other is SortedMapLikeBinaryTreeNode<*, *> && other.key == key && other.rightNode == rightNode && other.leftNode == leftNode)
}
override fun hashCode(): Int {
return key.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode()
}
suspend fun size(): Int {
return locker.withReadAcquire {
1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0)
}
}
/**
* This [Iterator] will run from less to greater values of nodes starting the
* [dev.inmo.micro_utils.coroutines.collections.SortedMapLikeBinaryTreeNode]-receiver. Due to non-suspending
* nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It
* means, that tree can be changed during to iteration process
*/
override fun iterator(): Iterator<SortedMapLikeBinaryTreeNode<K, V>> = iterator {
leftNode ?.let {
it.iterator().forEach { yield(it) }
}
yield(this@SortedMapLikeBinaryTreeNode)
rightNode ?.let {
it.iterator().forEach { yield(it) }
}
}
override fun toString(): String {
return "$key($leftNode;$rightNode)"
}
companion object {
operator fun <K : Comparable<K>, V> invoke(
key: K,
value: V
) = SortedMapLikeBinaryTreeNode(
key,
value,
key.createComparator()
)
}
}
/**
* Will add subnode in tree if there are no any node with [newData]
*
* * If [newData] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node,
* will be used [SortedMapLikeBinaryTreeNode.rightNode]
* * If [newData] is equal to [SortedMapLikeBinaryTreeNode.key] of currently
* checking node - will be returned currently checking node
* * If [newData] is less than [SortedMapLikeBinaryTreeNode.key] of currently
* checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode]
*
* This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or
* [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [newData] will be found
*
* @param replaceMode Will replace only value if node already exists
*/
private suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode(
subNode: SortedMapLikeBinaryTreeNode<K, V>,
skipLockers: Set<SmartRWLocker> = emptySet(),
replaceMode: Boolean
): SortedMapLikeBinaryTreeNode<K, V> {
var currentlyChecking = this
var latestParent: SortedMapLikeBinaryTreeNode<K, V>? = null
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (coroutineContext.job.isActive) {
if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) {
currentlyChecking.locker.lockWrite()
lockedLockers.add(currentlyChecking.locker)
}
val left = currentlyChecking.leftNode
val right = currentlyChecking.rightNode
val comparingResult = currentlyChecking.comparator.compare(subNode.key, currentlyChecking.key)
val isGreater = comparingResult > 0
when {
comparingResult == 0 -> {
val resultNode = if (replaceMode) {
subNode
} else {
val newNode = SortedMapLikeBinaryTreeNode(
subNode.key,
subNode.value,
currentlyChecking.comparator,
)
newNode.leftNode = currentlyChecking.leftNode
newNode.rightNode = currentlyChecking.rightNode
newNode
}
latestParent ?.let {
when {
it.leftNode === currentlyChecking -> it.leftNode = resultNode
it.rightNode === currentlyChecking -> it.rightNode = resultNode
}
}
return resultNode
}
isGreater && right == null -> {
currentlyChecking.rightNode = subNode
return subNode
}
isGreater && right != null -> {
latestParent = currentlyChecking
currentlyChecking = right
}
left == null -> {
currentlyChecking.leftNode = subNode
return subNode
}
else -> {
latestParent = currentlyChecking
currentlyChecking = left
}
}
}
} finally {
lockedLockers.forEach {
runCatching { it.unlockWrite() }
}
}
error("Unable to add node")
}
/**
* Will add subnode in tree if there are no any node with [key]
*
* * If [key] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node,
* will be used [SortedMapLikeBinaryTreeNode.rightNode]
* * If [key] is equal to [SortedMapLikeBinaryTreeNode.key] of currently
* checking node - will be returned currently checking node
* * If [key] is less than [SortedMapLikeBinaryTreeNode.key] of currently
* checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode]
*
* This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or
* [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [key] will be found
*/
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode(
key: K,
value: V
): SortedMapLikeBinaryTreeNode<K, V> {
return upsertSubNode(
SortedMapLikeBinaryTreeNode(key, value, comparator),
replaceMode = false
)
}
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findParentNode(data: K): SortedMapLikeBinaryTreeNode<K, V>? {
var currentParent: SortedMapLikeBinaryTreeNode<K, V>? = null
var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (coroutineContext.job.isActive) {
if (currentlyChecking == null) {
return null
}
if (currentlyChecking.locker !in lockedLockers) {
currentlyChecking.locker.acquireRead()
lockedLockers.add(currentlyChecking.locker)
}
val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.key)
when {
comparingResult > 0 -> {
currentParent = currentlyChecking
currentlyChecking = currentlyChecking.rightNode
continue
}
comparingResult < 0 -> {
currentParent = currentlyChecking
currentlyChecking = currentlyChecking.leftNode
continue
}
else -> return currentParent
}
}
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
error("Unable to find node")
}
/**
* Will remove (detach) node from tree starting with [this] [SortedMapLikeBinaryTreeNode]
*
* @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached;
* null otherwise
*/
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.removeSubNode(data: K): Pair<SortedMapLikeBinaryTreeNode<K, V>, SortedMapLikeBinaryTreeNode<K, V>>? {
val onFoundToRemoveCallback: suspend SortedMapLikeBinaryTreeNode<K, V>.(left: SortedMapLikeBinaryTreeNode<K, V>?, right: SortedMapLikeBinaryTreeNode<K, V>?) -> Unit = { left, right ->
left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker), replaceMode = true) }
right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker), replaceMode = true) }
}
while (coroutineContext.job.isActive) {
val foundParentNode = findParentNode(data) ?: return null
foundParentNode.locker.withWriteLock {
val left = foundParentNode.leftNode
val right = foundParentNode.rightNode
when {
left != null && left.comparator.compare(data, left.key) == 0 -> {
foundParentNode.leftNode = null
foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode)
return foundParentNode to left
}
right != null && right.comparator.compare(data, right.key) == 0 -> {
foundParentNode.rightNode = null
foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode)
return foundParentNode to right
}
else -> {
return@withWriteLock // data has been changed, new search required
}
}
}
}
error("Unable to remove node")
}
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNode(key: K): SortedMapLikeBinaryTreeNode<K, V>? {
var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (coroutineContext.job.isActive) {
if (currentlyChecking == null) {
return null
}
if (currentlyChecking.locker !in lockedLockers) {
currentlyChecking.locker.acquireRead()
lockedLockers.add(currentlyChecking.locker)
}
val comparingResult = currentlyChecking.comparator.compare(key, currentlyChecking.key)
when {
comparingResult > 0 -> {
currentlyChecking = currentlyChecking.rightNode
continue
}
comparingResult < 0 -> {
currentlyChecking = currentlyChecking.leftNode
continue
}
else -> return currentlyChecking
}
}
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
error("Unable to find node")
}
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.contains(data: K): Boolean = findNode(data) != null
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedMapLikeBinaryTreeNode<K, V>> {
val results = mutableSetOf<SortedMapLikeBinaryTreeNode<K, V>>()
val leftToCheck = mutableSetOf(this)
val lockedLockers = mutableSetOf<SmartRWLocker>()
val fromComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (fromInclusiveMode) {
{ it.comparator.compare(from, it.key) <= 0 }
} else {
{ it.comparator.compare(from, it.key) < 0 }
}
val toComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (toInclusiveMode) {
{ it.comparator.compare(to, it.key) >= 0 }
} else {
{ it.comparator.compare(to, it.key) > 0 }
}
try {
while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) {
val currentlyChecking = leftToCheck.first()
leftToCheck.remove(currentlyChecking)
if (currentlyChecking in results) {
continue
}
currentlyChecking.locker.acquireRead()
lockedLockers.add(currentlyChecking.locker)
if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) {
results.add(currentlyChecking)
currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
continue
}
when {
currentlyChecking.comparator.compare(to, currentlyChecking.key) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
currentlyChecking.comparator.compare(from, currentlyChecking.key) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
}
}
return results.toSet()
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
error("Unable to find nodes range")
}
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.deepEquals(other: SortedMapLikeBinaryTreeNode<K, V>): Boolean {
val leftToCheck = mutableSetOf(this)
val othersToCheck = mutableSetOf(other)
val lockedLockers = mutableSetOf<SmartRWLocker>()
try {
while (leftToCheck.isNotEmpty() && othersToCheck.isNotEmpty()) {
val thisToCheck = leftToCheck.first()
leftToCheck.remove(thisToCheck)
val otherToCheck = othersToCheck.first()
othersToCheck.remove(otherToCheck)
if (thisToCheck.locker !in lockedLockers) {
thisToCheck.locker.acquireRead()
lockedLockers.add(thisToCheck.locker)
}
if (otherToCheck.locker !in lockedLockers) {
otherToCheck.locker.acquireRead()
lockedLockers.add(otherToCheck.locker)
}
if (thisToCheck.key != otherToCheck.key || thisToCheck.value != otherToCheck.value) {
return false
}
if ((thisToCheck.leftNode == null).xor(otherToCheck.leftNode == null)) {
return false
}
if ((thisToCheck.rightNode == null).xor(otherToCheck.rightNode == null)) {
return false
}
thisToCheck.leftNode?.let { leftToCheck.add(it) }
thisToCheck.rightNode?.let { leftToCheck.add(it) }
otherToCheck.leftNode?.let { othersToCheck.add(it) }
otherToCheck.rightNode?.let { othersToCheck.add(it) }
}
} finally {
lockedLockers.forEach {
runCatching { it.releaseRead() }
}
}
return leftToCheck.isEmpty() && othersToCheck.isEmpty()
}
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
from = from,
to = to,
fromInclusiveMode = true,
toInclusiveMode = true
)
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRangeExcluding(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
from = from,
to = to,
fromInclusiveMode = false,
toInclusiveMode = false
)
suspend fun <K : Comparable<K>, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(range: ClosedRange<K>): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
from = range.start,
to = range.endInclusive,
)

View File

@@ -0,0 +1,12 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration
suspend fun <T> realWithTimeout(time: Duration, block: suspend () -> T): T {
return withContext(Dispatchers.Default.limitedParallelism(1)) {
withTimeout(time) {
block()
}
}
}

View File

@@ -0,0 +1,196 @@
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests {
@Test
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite()
assertTrue { locker.isWriteLocked() }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.unlockWrite()
assertFalse { locker.isWriteLocked() }
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
}
@Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits)
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits)
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
}
@Test
fun readLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.acquireRead()
}
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.acquireRead()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
assertTrue { locker.releaseRead() }
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits)
}
@Test
fun writeLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.lockWrite()
}
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() }
assertFalse(locker.isWriteLocked())
}
@Test
fun readsBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKeys = (0 until 100).map { "test$it" }
for (i in testKeys.indices) {
val it = testKeys[i]
locker.acquireRead(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() }
}
val readPermitsBeforeLock = locker.readSemaphore().freePermits
realWithTimeout(1.seconds) { locker.acquireRead() }
locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
}
@Test
fun writesBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKeys = (0 until 100).map { "test$it" }
for (i in testKeys.indices) {
val it = testKeys[i]
locker.lockWrite(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true }
}
next.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
val readPermitsBeforeLock = locker.readSemaphore().freePermits
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } }
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(it)
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
}
}

View File

@@ -6,7 +6,10 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartRWLockerTests {
@Test
@@ -148,4 +151,17 @@ class SmartRWLockerTests {
assertEquals(false, locker.writeMutex.isLocked)
}
}
@Test
fun exceptionOnLockingWillNotLockLocker() = runTest {
val locker = SmartRWLocker()
locker.acquireRead()
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite()
}
}
assertFalse { locker.writeMutex.isLocked }
}
}

View File

@@ -7,7 +7,9 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
val objectToSynchronize = Object()
synchronized(objectToSynchronize) {
launch(start = CoroutineStart.UNDISPATCHED) {
result = safelyWithResult(block)
result = runCatching {
block()
}
}.invokeOnCompletion {
synchronized(objectToSynchronize) {
objectToSynchronize.notifyAll()

View File

@@ -1,25 +1,20 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class HandleSafelyCoroutineContextTest {
@Test
fun testHandleSafelyCoroutineContext() {
val scope = CoroutineScope(Dispatchers.Default)
fun testHandleSafelyCoroutineContext() = runTest {
val scope = this
var contextHandlerHappen = false
var localHandlerHappen = false
var defaultHandlerHappen = false
defaultSafelyExceptionHandler = {
defaultHandlerHappen = true
throw it
}
val contextHandler: ExceptionHandler<Unit> = {
contextHandlerHappen = true
}
val checkJob = scope.launch {
safelyWithContextExceptionHandler(contextHandler) {
safely(
runCatchingLogging ({
contextHandlerHappen = true
}) {
runCatchingLogging (
{
localHandlerHappen = true
}
@@ -29,10 +24,8 @@ class HandleSafelyCoroutineContextTest {
println(coroutineContext)
error("That must happen too:)")
}
}
launchSynchronously { checkJob.join() }
}.join()
assert(contextHandlerHappen)
assert(localHandlerHappen)
assert(defaultHandlerHappen)
}
}

View File

@@ -0,0 +1,176 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode
import dev.inmo.micro_utils.coroutines.collections.addSubNode
import dev.inmo.micro_utils.coroutines.collections.findNode
import dev.inmo.micro_utils.coroutines.collections.findNodesInRange
import dev.inmo.micro_utils.coroutines.collections.findParentNode
import dev.inmo.micro_utils.coroutines.collections.removeSubNode
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SortedBinaryTreeNodeTests {
@Test
fun insertOnZeroLevelWorks() = runTest {
val zeroNode = SortedBinaryTreeNode(0)
zeroNode.addSubNode(1)
zeroNode.addSubNode(-1)
assertEquals(0, zeroNode.data)
assertEquals(1, zeroNode.getRightNode() ?.data)
assertEquals(-1, zeroNode.getLeftNode() ?.data)
}
@Test
fun searchOnZeroLevelWorks() = runTest {
val zeroNode = SortedBinaryTreeNode(0)
val oneNode = zeroNode.addSubNode(1)
val minusOneNode = zeroNode.addSubNode(-1)
val assertingNodesToSearchQuery = mapOf(
setOf(oneNode) to (1 .. 1),
setOf(zeroNode, oneNode) to (0 .. 1),
setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1),
setOf(minusOneNode, zeroNode) to (-1 .. 0),
setOf(minusOneNode) to (-1 .. -1),
setOf(zeroNode) to (0 .. 0),
)
assertingNodesToSearchQuery.forEach {
val foundData = zeroNode.findNodesInRange(it.value)
assertTrue(foundData.containsAll(it.key))
assertTrue(it.key.containsAll(foundData))
}
}
@Test
fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) {
val zeroNode = SortedBinaryTreeNode(0)
val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
for (i in -rangeRadius .. rangeRadius) {
nodes[i] = zeroNode.addSubNode(i)
}
for (i in -rangeRadius .. rangeRadius) {
val expectedNode = nodes.getValue(i)
val foundNode = zeroNode.findNode(i)
assertTrue(expectedNode === foundNode)
if (expectedNode === zeroNode) continue
val parentNode = zeroNode.findParentNode(i)
assertTrue(
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
)
}
val sourceTreeSize = zeroNode.size()
assertTrue(sourceTreeSize == nodes.size)
assertTrue(sourceTreeSize == (rangeRadius * 2 + 1))
for (i in -rangeRadius .. rangeRadius) {
val expectedNode = nodes.getValue(i)
val parentNode = zeroNode.findParentNode(i)
if (parentNode == null && i == zeroNode.data && expectedNode === zeroNode) continue
assertTrue(parentNode != null, "It is expected, that parent node of ${expectedNode.data} will not be null")
assertTrue(
parentNode.getLeftNode() === expectedNode || parentNode.getRightNode() === expectedNode,
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
)
val removeResult = zeroNode.removeSubNode(i)
assertTrue(removeResult ?.first === parentNode)
assertTrue(removeResult.second === expectedNode)
nodes[i] = zeroNode.addSubNode(i)
assertTrue(nodes[i] != null)
assertTrue(nodes[i] != expectedNode)
assertTrue(nodes[i] ?.data == i)
}
assertTrue(sourceTreeSize == zeroNode.size())
for (i in -rangeRadius .. rangeRadius) {
val expectedNode = nodes.getValue(i)
val foundNode = zeroNode.findNode(i)
assertTrue(expectedNode === foundNode)
if (expectedNode === zeroNode) continue
val parentNode = zeroNode.findParentNode(i)
assertTrue(
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
)
}
var previousData = -rangeRadius - 1
for (node in zeroNode) {
assertTrue(nodes[node.data] === node)
assertTrue(previousData == node.data - 1)
previousData = node.data
}
assertTrue(sourceTreeSize == zeroNode.size())
}
@Test
fun deepInsertOnWorks() = runTest(timeout = 240.seconds) {
val zeroNode = SortedBinaryTreeNode(0)
val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
for (i in -rangeRadius .. rangeRadius) {
nodes[i] = zeroNode.addSubNode(i)
}
for (i in -rangeRadius .. rangeRadius) {
val expectedNode = nodes.getValue(i)
val foundNode = zeroNode.findNode(i)
assertTrue(expectedNode === foundNode)
if (expectedNode === zeroNode) continue
val parentNode = zeroNode.findParentNode(i)
assertTrue(
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
"It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}"
)
}
val sourceTreeSize = zeroNode.size()
var previousData = -rangeRadius - 1
for (node in zeroNode) {
assertTrue(nodes[node.data] === node)
assertTrue(previousData == node.data - 1)
previousData = node.data
}
assertTrue(sourceTreeSize == zeroNode.size())
}
@Test
fun deepInsertIteratorWorking() = runTest {
val zeroNode = SortedBinaryTreeNode(0)
val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
for (i in -rangeRadius .. rangeRadius) {
nodes[i] = zeroNode.addSubNode(i)
}
var previousData = -rangeRadius - 1
for (node in zeroNode) {
assertTrue(nodes[node.data] === node)
assertTrue(previousData == node.data - 1)
previousData = node.data
}
assertTrue(previousData == rangeRadius)
}
}

View File

@@ -0,0 +1,118 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.coroutines.collections.*
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SortedMapLikeBinaryTreeNodeTests {
@Test
fun insertOnZeroLevelWorks() = runTest {
val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
zeroNode.upsertSubNode(1, 1)
zeroNode.upsertSubNode(-1, -1)
assertEquals(0, zeroNode.key)
assertEquals(1, zeroNode.getRightNode() ?.key)
assertEquals(-1, zeroNode.getLeftNode() ?.key)
assertEquals(0, zeroNode.findNode(0) ?.value)
assertEquals(1, zeroNode.findNode(1) ?.value)
assertEquals(-1, zeroNode.findNode(-1) ?.value)
}
@Test
fun searchOnZeroLevelWorks() = runTest {
val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
val oneNode = zeroNode.upsertSubNode(1, 1)
val minusOneNode = zeroNode.upsertSubNode(-1, -1)
val assertingNodesToSearchQuery = mapOf(
setOf(oneNode) to (1 .. 1),
setOf(zeroNode, oneNode) to (0 .. 1),
setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1),
setOf(minusOneNode, zeroNode) to (-1 .. 0),
setOf(minusOneNode) to (-1 .. -1),
setOf(zeroNode) to (0 .. 0),
)
assertingNodesToSearchQuery.forEach {
val foundData = zeroNode.findNodesInRange(it.value)
assertTrue(foundData.containsAll(it.key))
assertTrue(it.key.containsAll(foundData))
}
}
@Test
fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) {
var zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>()
for (i in -rangeRadius .. rangeRadius) {
nodes[i] = zeroNode.upsertSubNode(i, i)
if (i == zeroNode.key) {
zeroNode = nodes.getValue(i)
}
}
for (i in -rangeRadius .. rangeRadius) {
val expectedNode = nodes.getValue(i)
val foundNode = zeroNode.findNode(i)
assertEquals(expectedNode, foundNode)
if (expectedNode === zeroNode) continue
val parentNode = zeroNode.findParentNode(i)
assertTrue(
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
"It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}"
)
assertTrue(
foundNode != null && expectedNode.deepEquals(foundNode)
)
zeroNode.upsertSubNode(i, -i)
val foundModifiedNode = zeroNode.findNode(i)
assertEquals(foundNode ?.value, foundModifiedNode ?.value ?.times(-1))
}
}
@Test
fun deepInsertOnWorks() = runTest(timeout = 240.seconds) {
val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>()
for (i in -rangeRadius .. rangeRadius) {
if (zeroNode.key != i) {
nodes[i] = zeroNode.upsertSubNode(i, i)
}
}
nodes[zeroNode.key] = zeroNode
for (i in -rangeRadius .. rangeRadius) {
val expectedNode = nodes.getValue(i)
val foundNode = zeroNode.findNode(i)
assertTrue(expectedNode === foundNode)
if (expectedNode === zeroNode) continue
val parentNode = zeroNode.findParentNode(i)
assertTrue(
parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
"It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}"
)
}
val sourceTreeSize = zeroNode.size()
var previousData = -rangeRadius - 1
for (node in zeroNode) {
assertTrue(nodes[node.key] === node)
assertTrue(previousData == node.key - 1)
previousData = node.key
}
assertTrue(sourceTreeSize == zeroNode.size())
}
}

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -11,12 +11,20 @@ private String getCurrentVersionChangelog() {
return changelogDataOS.toString().trim()
}
if (new File(projectDir, "secret.gradle").exists()) {
apply from: './secret.gradle'
def githubTokenVariableName = "GITHUB_RELEASE_TOKEN"
def githubTokenVariableFromEnv = System.getenv(githubTokenVariableName)
def secretFile = new File(projectDir, "secret.gradle")
if (secretFile.exists() || project.hasProperty(githubTokenVariableName) || (githubTokenVariableFromEnv != "" && githubTokenVariableFromEnv != null)) {
if (secretFile.exists()) {
apply from: './secret.gradle'
}
apply plugin: "com.github.breadmoirai.github-release"
def githubReleaseToken = project.hasProperty(githubTokenVariableName) ? project.property(githubTokenVariableName).toString() : githubTokenVariableFromEnv
githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}"
token githubReleaseToken
owner = "InsanusMokrassar"
repo = "MicroUtils"

View File

@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.24.7
android_code_version=287
version=0.25.6
android_code_version=296

View File

@@ -0,0 +1,7 @@
config.set({
client: {
mocha: {
timeout: 240000
}
}
});

View File

@@ -1,13 +1,15 @@
[versions]
kt = "2.1.10"
kt-serialization = "1.8.0"
kt-coroutines = "1.10.1"
kt = "2.1.21"
kt-serialization = "1.8.1"
kt-coroutines = "1.10.2"
kslog = "1.4.1"
kotlinx-browser = "0.3"
jb-compose = "1.7.3"
jb-exposed = "0.59.0"
kslog = "1.4.2"
jb-compose = "1.8.0"
jb-exposed = "0.61.0"
jb-dokka = "2.0.0"
sqlite = "3.49.1.0"
@@ -15,23 +17,23 @@ sqlite = "3.49.1.0"
korlibs = "5.4.0"
uuid = "0.8.4"
ktor = "3.1.0"
ktor = "3.1.3"
gh-release = "2.5.2"
koin = "4.0.2"
koin = "4.0.4"
okio = "3.10.2"
okio = "3.11.0"
ksp = "2.1.10-1.0.30"
ksp = "2.1.20-1.0.31"
kotlin-poet = "1.18.1"
versions = "0.51.0"
android-gradle = "8.7.+"
android-gradle = "8.9.+"
dexcount = "4.0.0"
android-coreKtx = "1.15.0"
android-coreKtx = "1.16.0"
android-recyclerView = "1.4.0"
android-appCompat = "1.7.0"
android-fragment = "1.8.6"
@@ -39,8 +41,8 @@ android-espresso = "3.6.1"
android-test = "1.2.1"
android-props-minSdk = "21"
android-props-compileSdk = "35"
android-props-buildTools = "35.0.0"
android-props-compileSdk = "36"
android-props-buildTools = "36.0.0"
[libraries]
@@ -55,6 +57,7 @@ kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-and
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
kt-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kt-coroutines" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser-wasm-js", version.ref = "kotlinx-browser" }
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

View File

@@ -0,0 +1,9 @@
kotlin {
sourceSets {
commonMain {
dependencies {
implementation compose.runtime
}
}
}
}

View File

@@ -0,0 +1,9 @@
kotlin {
sourceSets {
androidUnitTest {
dependencies {
implementation compose.uiTest
}
}
}
}

View File

@@ -0,0 +1,15 @@
kotlin {
sourceSets {
jvmMain {
dependencies {
implementation compose.desktop.currentOs
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation compose.uiTest
}
}
}
}

View File

@@ -0,0 +1,9 @@
kotlin {
sourceSets {
jsMain {
dependencies {
implementation compose.web.core
}
}
}
}

View File

@@ -0,0 +1,24 @@
project.version = "$version"
project.group = "$group"
kotlin {
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,25 @@
project.version = "$version"
project.group = "$group"
kotlin {
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api libs.kt.serialization
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,31 @@
kotlin {
androidTarget {
publishAllLibraryVariants()
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
androidUnitTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
androidInstrumentedTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettings"

View File

@@ -0,0 +1,30 @@
kotlin {
js (IR) {
browser {
testTask {
useMocha {
timeout = "240000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "240000"
}
}
}
}
sourceSets {
jsMain {
dependencies {
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
}
}

View File

@@ -0,0 +1,21 @@
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
jvmMain {
dependencies {
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
}
}

View File

@@ -0,0 +1,8 @@
kotlin {
linuxArm64()
sourceSets {
nativeMain.dependsOn commonMain
linuxArm64Main.dependsOn nativeMain
}
}

View File

@@ -0,0 +1,10 @@
kotlin {
linuxX64()
mingwX64()
sourceSets {
nativeMain.dependsOn commonMain
linuxX64Main.dependsOn nativeMain
mingwX64Main.dependsOn nativeMain
}
}

View File

@@ -0,0 +1,27 @@
kotlin {
wasmJs {
browser {
testTask {
useKarma {
useChromeHeadless()
useConfigDirectory(rootProject.relativeProjectPath("gradle/karma.config.d"))
}
}
}
nodejs {
testTask {
timeout = Duration.ofSeconds(240)
nodeJsArgs.add("--unhandled-rejections=warn")
nodeJsArgs.add("--trace-warnings")
}
}
}
sourceSets {
wasmJsTest {
dependencies {
implementation kotlin('test-wasm-js')
}
}
}
}

View File

@@ -1,37 +1,6 @@
project.version = "$version"
project.group = "$group"
apply from: "$defaultProject"
apply from: "$enableMPPJvm"
apply from: "$enableMPPJs"
apply from: "$enableMPPWasmJs"
apply from: "$enableMPPAndroid"
apply from: "$publish"
kotlin {
androidTarget {
publishAllLibraryVariants()
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
}
}
apply from: "$defaultAndroidSettings"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -1,108 +0,0 @@
project.version = "$version"
project.group = "$group"
apply from: "$publish"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) {
browser {
testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
}
androidTarget {
publishAllLibraryVariants()
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
linuxX64()
mingwX64()
linuxArm64()
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
implementation compose.runtime
api libs.kt.serialization
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
androidUnitTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
implementation compose.uiTest
}
}
androidInstrumentedTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
jvmMain {
dependencies {
implementation compose.desktop.currentOs
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation compose.uiTest
}
}
jsMain {
dependencies {
implementation compose.web.core
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
nativeMain.dependsOn commonMain
linuxX64Main.dependsOn nativeMain
mingwX64Main.dependsOn nativeMain
linuxArm64Main.dependsOn nativeMain
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettings"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,12 @@
apply from: "$defaultProjectWithSerialization"
apply from: "$enableMPPJvm"
apply from: "$enableMPPJs"
apply from: "$enableMPPWasmJs"
apply from: "$enableMPPAndroid"
apply from: "$enableMPPNativeX64"
apply from: "$enableMPPNativeArm64"
apply from: "$addCompose"
apply from: "$addComposeForAndroid"
apply from: "$addComposeForDesktop"
apply from: "$addComposeForJs"
apply from: "$publish"

View File

@@ -1,40 +1,3 @@
project.version = "$version"
project.group = "$group"
apply from: "$defaultProject"
apply from: "$enableMPPJvm"
apply from: "$publish"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -1,88 +0,0 @@
project.version = "$version"
project.group = "$group"
apply from: "$publish"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) {
browser {
testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
}
androidTarget {
publishAllLibraryVariants()
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
linuxX64()
mingwX64()
linuxArm64()
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api libs.kt.serialization
}
}
commonTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
androidUnitTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
nativeMain.dependsOn commonMain
linuxX64Main.dependsOn nativeMain
mingwX64Main.dependsOn nativeMain
linuxArm64Main.dependsOn nativeMain
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettings"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -1,70 +0,0 @@
project.version = "$version"
project.group = "$group"
apply from: "$publish"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) {
browser {
testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
}
linuxX64()
mingwX64()
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api libs.kt.serialization
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
nativeMain.dependsOn commonMain
linuxX64Main.dependsOn nativeMain
mingwX64Main.dependsOn nativeMain
androidMain.dependsOn jvmMain
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,8 @@
apply from: "$defaultProjectWithSerialization"
apply from: "$enableMPPJvm"
apply from: "$enableMPPJs"
apply from: "$enableMPPWasmJs"
apply from: "$enableMPPAndroid"
apply from: "$enableMPPNativeX64"
apply from: "$enableMPPNativeArm64"
apply from: "$publish"

View File

@@ -0,0 +1,6 @@
apply from: "$defaultProjectWithSerialization"
apply from: "$enableMPPJvm"
apply from: "$enableMPPJs"
apply from: "$enableMPPWasmJs"
apply from: "$enableMPPNativeX64"
apply from: "$publish"

View File

@@ -1,98 +1,10 @@
project.version = "$version"
project.group = "$group"
apply from: "$defaultProjectWithSerialization"
apply from: "$enableMPPJvm"
apply from: "$enableMPPJs"
apply from: "$enableMPPWasmJs"
apply from: "$enableMPPAndroid"
apply from: "$addCompose"
apply from: "$addComposeForAndroid"
apply from: "$addComposeForDesktop"
apply from: "$addComposeForJs"
apply from: "$publish"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) {
browser {
testTask {
useMocha {
timeout = "60000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "60000"
}
}
}
}
androidTarget {
publishAllLibraryVariants()
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
implementation compose.runtime
api libs.kt.serialization
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
jvmMain {
dependencies {
implementation compose.desktop.currentOs
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation compose.uiTest
}
}
jsMain {
dependencies {
implementation compose.web.core
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
androidUnitTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
implementation compose.uiTest
}
}
}
}
apply from: "$defaultAndroidSettings"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
//compose {
// if (composePluginKotlinVersion != null && !composePluginKotlinVersion.isEmpty()) {
// kotlinCompilerPlugin.set(composePluginKotlinVersion)
// }
//}

View File

@@ -1,8 +1,51 @@
if (ext.getProperties()["do_publish"] == false) {
return
}
import java.nio.charset.StandardCharsets
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
apply plugin: 'maven-publish'
// This script work based on https://ossrh-staging-api.central.sonatype.com/swagger-ui/#/default/manual_upload_repository
// and getting available open repos and just uploading them
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
def taskName = "uploadSonatypePublication"
if (rootProject.tasks.names.contains(taskName) == false) {
rootProject.tasks.register(taskName) {
doLast {
def username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
def password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
def bearer = Base64.getEncoder().encodeToString("$username:$password".getBytes(StandardCharsets.UTF_8))
def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
.uri(URI.create("https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?state=open"))
.GET()
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $bearer")
.build()
def response = client.send(request, HttpResponse.BodyHandlers.ofString())
def keys = new ArrayList<String>()
response.body().findAll("\"key\"[\\s]*:[\\s]*\"[^\"]+\"").forEach {
def key = it.find("[^\"]+\"\$").find("[^\"]+")
keys.add(key)
}
keys.forEach {
println("Start uploading $it")
def uploadRequest = HttpRequest.newBuilder()
.uri(URI.create("https://ossrh-staging-api.central.sonatype.com/manual/upload/repository/$it?publishing_type=user_managed"))
.POST(HttpRequest.BodyPublishers.ofString(""))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $bearer")
.build()
def uploadResponse = client.send(uploadRequest, HttpResponse.BodyHandlers.ofString())
if (uploadResponse.statusCode() != 200) {
throw IllegalStateException("Faced error of uploading for repo with key $it. Response: $uploadResponse")
}
}
}
}
}
}
task javadocsJar(type: Jar) {
archiveClassifier = 'javadoc'
@@ -23,72 +66,64 @@ publishing {
}
developers {
developer {
id = "InsanusMokrassar"
name = "Aleksei Ovsiannikov"
email = "ovsyannikov.alexey95@gmail.com"
}
developer {
id = "InsanusMokrassar"
name = "Aleksei Ovsiannikov"
email = "ovsyannikov.alexey95@gmail.com"
}
developer {
id = "000Sanya"
name = "Syrov Aleksandr"
email = "000sanya.000sanya@gmail.com"
}
developer {
id = "000Sanya"
name = "Syrov Aleksandr"
email = "000sanya.000sanya@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
}
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
}
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
}
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
}
if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing'

View File

@@ -1 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"},"includeCentralSonatypeUploadingScript":true}}

View File

@@ -1,4 +1,52 @@
import java.nio.charset.StandardCharsets
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
apply plugin: 'maven-publish'
// This script work based on https://ossrh-staging-api.central.sonatype.com/swagger-ui/#/default/manual_upload_repository
// and getting available open repos and just uploading them
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
def taskName = "uploadSonatypePublication"
if (rootProject.tasks.names.contains(taskName) == false) {
rootProject.tasks.register(taskName) {
doLast {
def username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
def password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
def bearer = Base64.getEncoder().encodeToString("$username:$password".getBytes(StandardCharsets.UTF_8))
def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
.uri(URI.create("https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?state=open"))
.GET()
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $bearer")
.build()
def response = client.send(request, HttpResponse.BodyHandlers.ofString())
def keys = new ArrayList<String>()
response.body().findAll("\"key\"[\\s]*:[\\s]*\"[^\"]+\"").forEach {
def key = it.find("[^\"]+\"\$").find("[^\"]+")
keys.add(key)
}
keys.forEach {
println("Start uploading $it")
def uploadRequest = HttpRequest.newBuilder()
.uri(URI.create("https://ossrh-staging-api.central.sonatype.com/manual/upload/repository/$it?publishing_type=user_managed"))
.POST(HttpRequest.BodyPublishers.ofString(""))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $bearer")
.build()
def uploadResponse = client.send(uploadRequest, HttpResponse.BodyHandlers.ofString())
if (uploadResponse.statusCode() != 200) {
throw IllegalStateException("Faced error of uploading for repo with key $it. Response: $uploadResponse")
}
}
}
}
}
}
task javadocJar(type: Jar) {
from javadoc
@@ -55,42 +103,39 @@ publishing {
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
}
}
}
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
@@ -99,13 +144,13 @@ publishing {
if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing'
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)

View File

@@ -1 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"},"includeCentralSonatypeUploadingScript":true},"type":"JVM"}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {
@@ -17,11 +17,13 @@ kotlin {
jvmMain {
dependencies {
api libs.kt.reflect
api project(":micro_utils.coroutines")
}
}
androidMain {
dependencies {
api libs.kt.reflect
api project(":micro_utils.coroutines")
}
}
}

View File

@@ -5,7 +5,7 @@ plugins {
id "com.google.devtools.ksp"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {

View File

@@ -4,8 +4,14 @@ import org.koin.core.definition.Definition
import org.koin.core.module.Module
/**
* Will be useful in case you need to declare some singles with one type several types, but need to separate them and do
* not care about how :)
* Declares a factory with a random qualifier in the Koin module.
* This is useful when you need to declare multiple factory definitions of the same type
* but want them to be uniquely identifiable without manually specifying qualifiers.
* Unlike singles, factories create a new instance each time they are requested.
*
* @param T The type of instance to be created by the factory
* @param definition The definition function that creates new instances
* @return A Koin definition for the factory with a random qualifier
*/
inline fun <reified T : Any> Module.factoryWithRandomQualifier(
noinline definition: Definition<T>

View File

@@ -4,6 +4,16 @@ import org.koin.core.definition.Definition
import org.koin.core.module.Module
import org.koin.core.qualifier.StringQualifier
/**
* Declares a factory with a string qualifier in the Koin module.
* This is a convenience function that wraps the string qualifier in a [StringQualifier].
* Unlike singles, factories create a new instance each time they are requested.
*
* @param T The type of instance to be created by the factory
* @param qualifier The string value to be used as a qualifier
* @param definition The definition function that creates new instances
* @return A Koin definition for the factory with the specified string qualifier
*/
inline fun <reified T : Any> Module.factory(
qualifier: String,
noinline definition: Definition<T>

View File

@@ -3,6 +3,21 @@ package dev.inmo.micro_utils.koin
import org.koin.core.Koin
import org.koin.core.scope.Scope
/**
* Retrieves all instances of type [T] from the current [Scope] and returns them as a distinct list.
* This function is useful when you want to avoid duplicate instances of the same type.
*
* @param T The type of instances to retrieve
* @return A list of distinct instances of type [T]
*/
inline fun <reified T : Any> Scope.getAllDistinct() = getAll<T>().distinct()
/**
* Retrieves all instances of type [T] from the [Koin] container and returns them as a distinct list.
* This function is useful when you want to avoid duplicate instances of the same type.
*
* @param T The type of instances to retrieve
* @return A list of distinct instances of type [T]
*/
inline fun <reified T : Any> Koin.getAllDistinct() = getAll<T>().distinct()

View File

@@ -3,6 +3,23 @@ package dev.inmo.micro_utils.koin
import org.koin.core.Koin
import org.koin.core.scope.Scope
/**
* Retrieves the first available instance of type [T] from the current scope.
* This is useful when you need any instance of a type and don't care which one.
*
* @param T The type of instance to retrieve
* @return The first available instance of type [T]
* @throws NoSuchElementException if no instances of type [T] are available
*/
inline fun <reified T : Any> Scope.getAny() = getAll<T>().first()
/**
* Retrieves the first available instance of type [T] from the Koin container.
* This is useful when you need any instance of a type and don't care which one.
*
* @param T The type of instance to retrieve
* @return The first available instance of type [T]
* @throws NoSuchElementException if no instances of type [T] are available
*/
inline fun <reified T : Any> Koin.getAny() = getAll<T>().first()

View File

@@ -7,33 +7,81 @@ import org.koin.core.instance.InstanceFactory
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.scope.Scope
/**
* Retrieves an instance of type [T] from the Koin container using a [BeanDefinition].
*
* @param T The type of instance to retrieve
* @param definition The bean definition to use for instance retrieval
* @param parameters Optional parameters to pass to the instance constructor
* @return An instance of type [T]
*/
fun <T> Koin.get(definition: BeanDefinition<T>, parameters: ParametersDefinition? = null): T = get(
definition.primaryType,
definition.qualifier,
parameters
)
/**
* Retrieves an instance of type [T] from the Koin container using an [InstanceFactory].
*
* @param T The type of instance to retrieve
* @param definition The instance factory to use for instance retrieval
* @param parameters Optional parameters to pass to the instance constructor
* @return An instance of type [T]
*/
fun <T> Koin.get(definition: InstanceFactory<T>, parameters: ParametersDefinition? = null): T = get(
definition.beanDefinition,
parameters
)
/**
* Retrieves an instance of type [T] from the Koin container using a [KoinDefinition].
*
* @param T The type of instance to retrieve
* @param definition The Koin definition to use for instance retrieval
* @param parameters Optional parameters to pass to the instance constructor
* @return An instance of type [T]
*/
fun <T> Koin.get(definition: KoinDefinition<T>, parameters: ParametersDefinition? = null): T = get(
definition.factory,
parameters
)
/**
* Retrieves an instance of type [T] from the current scope using a [BeanDefinition].
*
* @param T The type of instance to retrieve
* @param definition The bean definition to use for instance retrieval
* @param parameters Optional parameters to pass to the instance constructor
* @return An instance of type [T]
*/
fun <T> Scope.get(definition: BeanDefinition<T>, parameters: ParametersDefinition? = null): T = get(
definition.primaryType,
definition.qualifier,
parameters
)
/**
* Retrieves an instance of type [T] from the current scope using an [InstanceFactory].
*
* @param T The type of instance to retrieve
* @param definition The instance factory to use for instance retrieval
* @param parameters Optional parameters to pass to the instance constructor
* @return An instance of type [T]
*/
fun <T> Scope.get(definition: InstanceFactory<T>, parameters: ParametersDefinition? = null): T = get(
definition.beanDefinition,
parameters
)
/**
* Retrieves an instance of type [T] from the current scope using a [KoinDefinition].
*
* @param T The type of instance to retrieve
* @param definition The Koin definition to use for instance retrieval
* @param parameters Optional parameters to pass to the instance constructor
* @return An instance of type [T]
*/
fun <T> Scope.get(definition: KoinDefinition<T>, parameters: ParametersDefinition? = null): T = get(
definition.factory,
parameters

View File

@@ -3,4 +3,10 @@ package dev.inmo.micro_utils.koin
import com.benasher44.uuid.uuid4
import org.koin.core.qualifier.StringQualifier
/**
* Creates a [StringQualifier] with a random string value.
*
* @param randomFun A function that generates a random string. By default, it uses UUID4 string representation.
* @return A [StringQualifier] with a randomly generated string value
*/
fun RandomQualifier(randomFun: () -> String = { uuid4().toString() }) = StringQualifier(randomFun())

View File

@@ -4,8 +4,14 @@ import org.koin.core.definition.Definition
import org.koin.core.module.Module
/**
* Will be useful in case you need to declare some singles with one type several types, but need to separate them and do
* not care about how :)
* Declares a single instance with a random qualifier in the Koin module.
* This is useful when you need to declare multiple instances of the same type
* but want them to be uniquely identifiable without manually specifying qualifiers.
*
* @param T The type of instance to be created
* @param createdAtStart Whether the instance should be created when the Koin module starts (default: false)
* @param definition The definition function that creates the instance
* @return A Koin definition for the single instance with a random qualifier
*/
inline fun <reified T : Any> Module.singleWithRandomQualifier(
createdAtStart: Boolean = false,

View File

@@ -4,6 +4,16 @@ import org.koin.core.definition.Definition
import org.koin.core.module.Module
import org.koin.core.qualifier.StringQualifier
/**
* Declares a single instance with a string qualifier in the Koin module.
* This is a convenience function that wraps the string qualifier in a [StringQualifier].
*
* @param T The type of instance to be created
* @param qualifier The string value to be used as a qualifier
* @param createdAtStart Whether the instance should be created when the Koin module starts (default: false)
* @param definition The definition function that creates the instance
* @return A Koin definition for the single instance with the specified string qualifier
*/
inline fun <reified T : Any> Module.single(
qualifier: String,
createdAtStart: Boolean = false,

View File

@@ -0,0 +1,32 @@
package dev.inmo.micro_utils.koin
import dev.inmo.micro_utils.coroutines.doSynchronously
import kotlinx.coroutines.CoroutineScope
import org.koin.core.module.Module
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.Qualifier
import org.koin.core.qualifier.StringQualifier
import org.koin.core.scope.Scope
import kotlin.reflect.KClass
inline fun <reified T : Any> Module.factorySuspend(
qualifier: Qualifier? = null,
coroutineScope: CoroutineScope? = null,
noinline definition: suspend Scope.(ParametersHolder) -> T
) = factory(
qualifier,
if (coroutineScope == null) {
{
doSynchronously {
definition(it)
}
}
} else {
{
coroutineScope.doSynchronously {
definition(it)
}
}
}
)

View File

@@ -0,0 +1,32 @@
package dev.inmo.micro_utils.koin
import dev.inmo.micro_utils.coroutines.doSynchronously
import kotlinx.coroutines.CoroutineScope
import org.koin.core.module.Module
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.StringQualifier
import org.koin.core.scope.Scope
inline fun <reified T : Any> Module.singleSuspend(
qualifier: StringQualifier,
createdAtStart: Boolean = false,
coroutineScope: CoroutineScope? = null,
noinline definition: suspend Scope.(ParametersHolder) -> T
) = single(
qualifier,
createdAtStart,
if (coroutineScope == null) {
{
doSynchronously {
definition(it)
}
}
} else {
{
coroutineScope.doSynchronously {
definition(it)
}
}
}
)

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -7,7 +7,7 @@ plugins {
ext.do_publish = false
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -7,7 +7,7 @@ plugins {
ext.do_publish = false
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -7,7 +7,7 @@ plugins {
ext.do_publish = false
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -0,0 +1,63 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import io.ktor.client.content.*
import kotlinx.coroutines.*
import org.w3c.xhr.*
import org.w3c.xhr.XMLHttpRequest.Companion.DONE
suspend fun tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: ProgressListener
): TemporalFileId {
val formData = FormData()
val answer = CompletableDeferred<TemporalFileId>(currentCoroutineContext().job)
val subscope = CoroutineScope(currentCoroutineContext().LinkedSupervisorJob())
formData.append(
"data",
file
)
val request = XMLHttpRequest()
request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = {
subscope.launchLoggingDropExceptions { onUpload.onProgress(it.loaded.toString().toLong(), it.total.toString().toLong()) }
}
request.onload = {
if (request.status == 200.toShort()) {
answer.complete(TemporalFileId(request.responseText))
} else {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
}
request.onerror = {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
request.open("POST", fullTempUploadDraftPath, true)
request.send(formData)
answer.invokeOnCompletion {
runCatching {
if (request.readyState != DONE) {
request.abort()
}
}
}
return answer.await().also {
subscope.cancel()
}
}
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: ProgressListener
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)

View File

@@ -0,0 +1,97 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import io.ktor.client.HttpClient
import io.ktor.client.content.*
import io.ktor.http.Headers
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.job
import kotlinx.io.readByteArray
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.StringFormat
import kotlinx.serialization.encodeToString
import org.khronos.webgl.toInt8Array
import org.w3c.files.Blob
import org.w3c.xhr.FormData
import org.w3c.xhr.TEXT
import org.w3c.xhr.XMLHttpRequest
import org.w3c.xhr.XMLHttpRequestResponseType
/**
* Will execute submitting of multipart data request
*
* @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass
* [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value
* in case you wish to pass other source of multipart binary data than regular file
* @suppress
*/
actual suspend fun <T> HttpClient.uniUpload(
url: String,
data: Map<String, Any>,
resultDeserializer: DeserializationStrategy<T>,
headers: Headers,
stringFormat: StringFormat,
onUpload: ProgressListener
): T? {
val formData = FormData()
val answer = CompletableDeferred<T?>(currentCoroutineContext().job)
val subscope = CoroutineScope(currentCoroutineContext().LinkedSupervisorJob())
data.forEach { (k, v) ->
when (v) {
is MPPFile -> formData.append(
k,
v
)
is UniUploadFileInfo -> formData.append(
k,
Blob(arrayOf(v.inputAllocator().readByteArray().toInt8Array()).toJsArray().unsafeCast()),
v.fileName.name
)
else -> formData.append(
k,
stringFormat.encodeToString(v)
)
}
}
val request = XMLHttpRequest()
headers.forEach { s, strings ->
request.setRequestHeader(s, strings.joinToString())
}
request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = {
subscope.launchLoggingDropExceptions { onUpload.onProgress(it.loaded.toString().toLong(), it.total.toString().toLong()) }
}
request.onload = {
if (request.status == 200.toShort()) {
answer.complete(
stringFormat.decodeFromString(resultDeserializer, request.responseText)
)
} else {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
}
request.onerror = {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
request.open("POST", url, true)
request.send(formData)
answer.invokeOnCompletion {
runCatching {
if (request.readyState != XMLHttpRequest.DONE) {
request.abort()
}
}
}
return answer.await().also {
subscope.cancel()
}
}

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.*
import io.ktor.utils.io.core.ByteReadPacket
import io.ktor.utils.io.core.Input
actual fun MPPFile.input(): Input = ByteReadPacket(readBytes())

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

Some files were not shown because too many files have changed in this diff Show More