Compare commits

...

125 Commits

Author SHA1 Message Date
9822ff321b Merge pull request #607 from InsanusMokrassar/0.26.4
0.26.4
2025-09-02 01:33:50 +06:00
b485d485ef MPPFilePathSeparator 2025-09-01 22:40:26 +06:00
0b3d445109 start 0.26.4 2025-09-01 22:34:32 +06:00
d7e48940bc Merge pull request #606 from InsanusMokrassar/0.26.3
0.26.3
2025-08-20 19:17:26 +06:00
1049eb0fe7 update dependencies 2025-08-20 19:15:24 +06:00
c871ef5635 start 0.26.3 2025-08-20 18:39:59 +06:00
7edfcb20c4 Merge pull request #593 from InsanusMokrassar/0.26.2
0.26.2
2025-07-31 15:22:34 +06:00
7a1438a2c0 update dependencies 2025-07-31 15:11:42 +06:00
2af8cba8cd rename SpecialMutableStateFlow to MutableRedeliverStateFlow 2025-07-29 17:53:47 +06:00
27d74c0a62 start 0.26.2 2025-07-29 17:46:31 +06:00
f86d1bfe06 Merge pull request #591 from InsanusMokrassar/0.26.1
0.26.1
2025-07-21 23:05:46 +06:00
7cc5972ff7 update dependencies 2025-07-21 22:58:00 +06:00
3bbf978b00 fixes in actorAsync 2025-07-21 22:02:30 +06:00
ed36467600 add opportunity to pass logger in subscribe async 2025-07-21 21:53:03 +06:00
dd0de327fc start 0.26.1 2025-07-21 21:37:50 +06:00
dccd3ce8fd Merge pull request #589 from InsanusMokrassar/0.26.0
0.26.0
2025-07-11 16:46:21 +06:00
fa45e7b696 fix of kspCommonMainKotlinMetadata call 2025-07-11 16:39:08 +06:00
57f009e8aa try to fix build 2025-07-11 14:40:53 +06:00
04b633a5ea fill changelog 2025-07-10 23:44:11 +06:00
20d42b05bb fixed issue with ksp (with https://github.com/google/ksp/issues/2491) 2025-07-10 19:22:08 +06:00
91ba50f1ff start updates 2025-07-10 18:21:57 +06:00
f4476c99f9 start 0.26.0 2025-07-10 17:59:06 +06:00
50f3f586ab fixes 2025-06-18 15:00:02 +06:00
36a2d7ec8e small improvement of InfinityPagedComponent and add opportunity to set publishing type 2025-06-18 14:31:01 +06:00
4890b5833e Merge pull request #587 from InsanusMokrassar/0.25.8
0.25.8
2025-06-17 15:47:54 +06:00
e20ab89688 improvements in InfinityPagedComponent 2025-06-17 15:45:05 +06:00
e557ba8184 start 0.25.8 2025-06-17 15:31:03 +06:00
8540e21d5a Merge pull request #584 from InsanusMokrassar/0.25.7
0.25.7
2025-06-10 23:38:07 +06:00
76c04a8506 update dependencies 2025-06-10 23:29:26 +06:00
128632770e start 0.25.7 2025-06-10 23:19:46 +06:00
31e0800e81 Update build.gradle 2025-06-08 20:41:05 +06:00
00ca96eec8 add nmcp 2025-06-08 18:51:53 +06:00
077ef2c639 fix of repositories order in build.gradle 2025-06-02 19:44:16 +06:00
e3ea7be0e7 one more improvement 2025-06-02 11:47:38 +06:00
05fd1c2b14 one more improvement 2025-06-02 11:45:30 +06:00
affcffe270 small improvement in uploadSonatypePublicationmain.kts 2025-06-02 11:44:09 +06:00
62930231e4 first version of uploadSonatypePublicationmain.kts 2025-06-02 11:42:45 +06:00
ad651631ec experiments time 2025-06-02 08:34:09 +06:00
cf1c8f13db add publish_all_script 2025-06-01 23:16:50 +06:00
9acc69b897 Revert "add keepa-live for uploading script parts"
This reverts commit 2ed8443e28.
2025-05-18 19:15:58 +06:00
9bc7cbdb50 add allowing of connection and keep-alive headers 2025-05-18 18:35:31 +06:00
2ed8443e28 add keepa-live for uploading script parts 2025-05-17 15:59:53 +06:00
94f598c2b4 Merge pull request #578 from InsanusMokrassar/0.25.6
0.25.6
2025-05-16 14:45:34 +06:00
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
ddb8e1efb4 fix build 2025-03-03 12:15:36 +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
177 changed files with 3358 additions and 1005 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.vscode
.kotlin .kotlin
out/* out/*
*.iml *.iml
@@ -9,6 +10,7 @@ settings.xml
.gradle/ .gradle/
build/ build/
out/ out/
bin/
secret.gradle secret.gradle
local.properties local.properties

View File

@@ -1,5 +1,136 @@
# Changelog # Changelog
## 0.26.4
* `Common`:
* Add expect/actual `MPPFilePathSeparator`
* Fix `FileName` realization to take care about system file path separator
## 0.26.3
* `Versions`:
* `Kotlin`: `2.2.0` -> `2.2.10`
* `KSP`: `2.2.0-2.0.2` -> `2.2.10-2.0.2`
* `Android CoreKTX`: `1.16.0` -> `1.17.0`
* `Android Fragment`: `1.8.8` -> `1.8.9`
## 0.26.2
* `Versions`:
* `Ktor`: `3.2.2` -> `3.2.3`
* `Okio`: `3.15.0` -> `3.16.0`
* `Coroutines`:
* Rename `SpecialMutableStateFlow` to `MutableRedeliverStateFlow`
## 0.26.1
* `Versions`:
* `Compose`: `1.8.1` -> `1.8.2`
* `Ktor`: `3.2.1` -> `3.2.2`
* `Coroutines`:
* Add opportunity to pass logger in subscribe async
## 0.26.0
**WARNING!!! SINCE THIS VERSION IF YOU WANT TO USE SOME OF KSP MODULES, SET `ksp.useKSP2=false` IN YOUR `gradle.properties`** (see [gh issue 2491](https://github.com/google/ksp/issues/2491))
* `Versions`:
* `Kotlin`: `2.1.21` -> `2.2.0`
* `Serialization`: `1.8.1` -> `1.9.0`
* `KSLog`: `1.4.2` -> `1.5.0`
* `Ktor`: `3.1.3` -> `3.2.1`
* `Koin`: `4.0.4` -> `4.1.0`
* `Okio`: `3.12.0` -> `3.15.0`
* `KSP`: `2.1.20-1.0.31` -> `2.2.0-2.0.2`
* `kotlin-poet`: `1.18.1` -> `2.2.0`
## 0.25.8
* `Pagination`:
* `Compose`:
* New function `rememberInfinityPagedComponentContext` to create `InfinityPagedComponentContext`
* New variants of `InfinityPagedComponent` component
## 0.25.7
* `Versions`:
* `Compose`: `1.8.0` -> `1.8.1`
* `Xerial SQLite`: `3.49.1.0` -> `3.50.1.0`
* `Okio`: `3.11.0` -> `3.12.0`
* `Android AppCompat`: `1.7.0` -> `1.7.1`
* `Android Fragment`: `1.8.6` -> `1.8.8`
## 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 ## 0.24.8
* `Versions`: * `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 * `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 * `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 * `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

@@ -19,15 +19,30 @@ buildscript {
plugins { plugins {
alias(libs.plugins.versions) alias(libs.plugins.versions)
alias(libs.plugins.nmcp.aggregation)
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
nmcpAggregation {
centralPortal {
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')
validationTimeout = Duration.ofHours(4)
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
}
publishAllProjectsProbablyBreakingProjectIsolation()
}
} }
allprojects { allprojects {
repositories { repositories {
mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
} }
} }

View File

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

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library" 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 * 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 @Serializable
@JvmInline @JvmInline
@@ -21,18 +21,18 @@ value class HEXAColor (
val hexaUInt: UInt val hexaUInt: UInt
) : Comparable<HEXAColor> { ) : 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 val hexa: String
get() = "#${hexaUInt.toString(16).padStart(8, '0')}" 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 val hex: String
get() = hexa.take(7) 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 val ahex: String
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}" get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
@@ -140,12 +140,12 @@ value class HEXAColor (
}.lowercase().toUInt(16).let(::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) 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( fun fromAhex(uint: UInt) = HEXAColor(
a = ((uint and 0xff000000u) / 0x1000000u).toInt(), a = ((uint and 0xff000000u) / 0x1000000u).toInt(),

View File

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

View File

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

View File

@@ -2,11 +2,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import dev.inmo.micro_utils.common.compose.LoadableComponent import dev.inmo.micro_utils.common.compose.LoadableComponent
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -16,8 +14,8 @@ class LoadableComponentTests {
@Test @Test
@TestOnly @TestOnly
fun testSimpleLoad() = runComposeUiTest { fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = SpecialMutableStateFlow<Int>(0) val loadingFlow = MutableRedeliverStateFlow<Int>(0)
val loadedFlow = SpecialMutableStateFlow<Int>(0) val loadedFlow = MutableRedeliverStateFlow<Int>(0)
setContent { setContent {
LoadableComponent<Int>({ LoadableComponent<Int>({
loadingFlow.filter { it == 1 }.first() loadingFlow.filter { it == 1 }.first()

View File

@@ -7,7 +7,7 @@ import kotlin.jvm.JvmInline
@JvmInline @JvmInline
value class FileName(val string: String) { value class FileName(val string: String) {
val name: String val name: String
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } get() = withoutSlashAtTheEnd.takeLastWhile { it != MPPFilePathSeparator }
val extension: String val extension: String
get() = name.takeLastWhile { it != '.' } get() = name.takeLastWhile { it != '.' }
val nameWithoutExtension: String val nameWithoutExtension: String
@@ -18,7 +18,7 @@ value class FileName(val string: String) {
} ?: filename } ?: filename
} }
val withoutSlashAtTheEnd: String val withoutSlashAtTheEnd: String
get() = string.dropLastWhile { it == '/' } get() = string.dropLastWhile { it == MPPFilePathSeparator }
override fun toString(): String = string override fun toString(): String = string
} }
@@ -26,6 +26,7 @@ value class FileName(val string: String) {
expect class MPPFile expect class MPPFile
expect val MPPFile.filename: FileName expect val MPPFile.filename: FileName
expect val MPPFilePathSeparator: Char
expect val MPPFile.filesize: Long expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator

View File

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

View File

@@ -35,6 +35,10 @@ private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().awa
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/** /**
* @suppress * @suppress
*/ */

View File

@@ -14,6 +14,10 @@ actual typealias MPPFile = File
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = File.separatorChar
/** /**
* @suppress * @suppress
*/ */

View File

@@ -11,6 +11,10 @@ actual typealias MPPFile = Path
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(toString()) get() = FileName(toString())
actual val MPPFilePathSeparator: Char = Path.DIRECTORY_SEPARATOR.first()
/** /**
* @suppress * @suppress
*/ */

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,62 @@
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)
actual val MPPFilePathSeparator: Char
get() = '/'
/**
* @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" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

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

View File

@@ -3,7 +3,7 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@@ -16,7 +16,7 @@ import org.jetbrains.compose.web.css.StyleSheet
* to add `Style(stylesheet)` on every compose function call * to add `Style(stylesheet)` on every compose function call
*/ */
object StyleSheetsAggregator { object StyleSheetsAggregator {
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet()) private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow() val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable @Composable

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.* import androidx.compose.ui.test.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import kotlin.test.Test import kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test @Test
@TestOnly @TestOnly
fun simpleTest() = runComposeUiTest { fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0) val flowState = MutableRedeliverStateFlow(0)
setContent { setContent {
Button({ flowState.value++ }) { Text("Click") } Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString()) Text(flowState.collectAsState().value.toString())

View File

@@ -1,2 +0,0 @@
actual val AllowDeepInsertOnWorksTest: Boolean
get() = true

View File

@@ -4,6 +4,15 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow 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( fun <T> CoroutineScope.actor(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
block: suspend (T) -> Unit block: suspend (T) -> Unit
@@ -13,6 +22,16 @@ fun <T> CoroutineScope.actor(
return channel 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( inline fun <T> CoroutineScope.safeActor(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
@@ -7,13 +8,15 @@ import kotlinx.coroutines.flow.consumeAsFlow
fun <T> CoroutineScope.actorAsync( fun <T> CoroutineScope.actorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
markerFactory: suspend (T) -> Any? = { null }, markerFactory: suspend (T) -> Any? = { null },
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
): Channel<T> { ): Channel<T> {
val channel = Channel<T>(channelCapacity) val channel = Channel<T>(channelCapacity)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block) channel.consumeAsFlow().subscribeAsync(this, markerFactory, logger, block)
return channel return channel
} }
@Deprecated("Use standard actosAsync instead", ReplaceWith("actorAsync(channelCapacity, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.actorAsync"))
inline fun <T> CoroutineScope.safeActorAsync( inline fun <T> CoroutineScope.safeActorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -8,6 +9,7 @@ import kotlinx.coroutines.sync.withLock
private class SubscribeAsyncReceiver<T>( private class SubscribeAsyncReceiver<T>(
val scope: CoroutineScope, val scope: CoroutineScope,
val logger: KSLog,
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
) { ) {
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED) private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
@@ -15,7 +17,7 @@ private class SubscribeAsyncReceiver<T>(
get() = dataChannel get() = dataChannel
init { init {
scope.launchLoggingDropExceptions { scope.launchLoggingDropExceptions(logger = logger) {
for (data in dataChannel) { for (data in dataChannel) {
output(data) output(data)
} }
@@ -33,13 +35,16 @@ private data class AsyncSubscriptionCommandData<T, M>(
val scope: CoroutineScope, val scope: CoroutineScope,
val markerFactory: suspend (T) -> M, val markerFactory: suspend (T) -> M,
val block: suspend (T) -> Unit, val block: suspend (T) -> Unit,
val logger: KSLog,
val onEmpty: suspend (M) -> Unit val onEmpty: suspend (M) -> Unit
) : AsyncSubscriptionCommand<T, M> { ) : AsyncSubscriptionCommand<T, M> {
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
val marker = markerFactory(data) val marker = markerFactory(data)
markersMap.getOrPut(marker) { markersMap.getOrPut(marker) {
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) { SubscribeAsyncReceiver(scope.LinkedSupervisorScope(), logger) {
safelyWithoutExceptions { block(it) } runCatchingLogging(logger = logger) {
block(it)
}
if (isEmpty()) { if (isEmpty()) {
onEmpty(marker) onEmpty(marker)
} }
@@ -63,6 +68,7 @@ private data class AsyncSubscriptionCommandClearReceiver<T, M>(
fun <T, M> Flow<T>.subscribeAsync( fun <T, M> Flow<T>.subscribeAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
): Job { ): Job {
val subscope = scope.LinkedSupervisorScope() val subscope = scope.LinkedSupervisorScope()
@@ -71,8 +77,14 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap) it.invoke(markersMap)
} }
val job = subscribeSafelyWithoutExceptions(subscope) { data -> val job = subscribeLoggingDropExceptions(subscope, logger = logger) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> val dataCommand = AsyncSubscriptionCommandData(
data = data,
scope = subscope,
markerFactory = markerFactory,
block = block,
logger = logger
) { marker ->
actor.send( actor.send(
AsyncSubscriptionCommandClearReceiver(marker) AsyncSubscriptionCommandClearReceiver(marker)
) )
@@ -85,17 +97,20 @@ fun <T, M> Flow<T>.subscribeAsync(
return job return job
} }
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyAsync( fun <T, M> Flow<T>.subscribeSafelyAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) { ) = subscribeAsync(scope, markerFactory, logger) {
safely(onException) { safely(onException) {
block(it) block(it)
} }
} }
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync( fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
@@ -107,11 +122,22 @@ fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
} }
} }
fun <T, M> Flow<T>.subscribeLoggingDropExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory, logger) {
block(it)
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, logger, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync( fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) { ) = subscribeAsync(scope, markerFactory, logger) {
safelyWithoutExceptions({ /* do nothing */}) { safelyWithoutExceptions({ /* do nothing */}) {
block(it) block(it)
} }

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/** /**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to * Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber * each active subscriber
*/ */
open class SpecialMutableStateFlow<T>( open class MutableRedeliverStateFlow<T>(
initialValue: T initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)
@@ -68,3 +65,6 @@ open class SpecialMutableStateFlow<T>(
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector) override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
} }
@Deprecated("Renamed to MutableRedeliverStateFlow", ReplaceWith("MutableRedeliverStateFlow<T>"))
typealias SpecialMutableStateFlow<T> = MutableRedeliverStateFlow<T>

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,7 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow] * @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/ */
class Mutable(locked: Boolean = false) : SmartMutex { class Mutable(locked: Boolean = false) : SmartMutex {
private val _lockStateFlow = SpecialMutableStateFlow<Boolean>(locked) private val _lockStateFlow = MutableRedeliverStateFlow<Boolean>(locked)
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex() private val internalChangesMutex = Mutex()

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@@ -7,9 +8,10 @@ import kotlin.contracts.contract
/** /**
* Composite mutex which works with next rules: * 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] * * [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] * * [unlockWrite] will just unlock [writeMutex]
*/ */
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) { 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() { suspend fun lockWrite() {
_writeMutex.lock() _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

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

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines.collections package dev.inmo.micro_utils.coroutines.collections
import dev.inmo.micro_utils.coroutines.SmartRWLocker import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.waitReadRelease
import dev.inmo.micro_utils.coroutines.withReadAcquire import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock import dev.inmo.micro_utils.coroutines.withWriteLock
import kotlinx.coroutines.job import kotlinx.coroutines.job
@@ -93,7 +92,7 @@ class SortedBinaryTreeNode<T>(
* This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or * 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 * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
*/ */
private suspend fun <T> SortedBinaryTreeNode<T>.addSubNode( private suspend fun <T> SortedBinaryTreeNode<T>.upsertSubNode(
subNode: SortedBinaryTreeNode<T>, subNode: SortedBinaryTreeNode<T>,
skipLockers: Set<SmartRWLocker> = emptySet() skipLockers: Set<SmartRWLocker> = emptySet()
): SortedBinaryTreeNode<T> { ): SortedBinaryTreeNode<T> {
@@ -149,7 +148,7 @@ private suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(
* [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
*/ */
suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(newData: T): SortedBinaryTreeNode<T> { suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(newData: T): SortedBinaryTreeNode<T> {
return addSubNode( return upsertSubNode(
SortedBinaryTreeNode(newData, comparator) SortedBinaryTreeNode(newData, comparator)
) )
} }
@@ -198,8 +197,8 @@ suspend fun <T> SortedBinaryTreeNode<T>.findParentNode(data: T): SortedBinaryTre
*/ */
suspend fun <T> SortedBinaryTreeNode<T>.removeSubNode(data: T): Pair<SortedBinaryTreeNode<T>, SortedBinaryTreeNode<T>>? { 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 -> val onFoundToRemoveCallback: suspend SortedBinaryTreeNode<T>.(left: SortedBinaryTreeNode<T>?, right: SortedBinaryTreeNode<T>?) -> Unit = { left, right ->
left ?.also { leftNode -> addSubNode(leftNode, setOf(locker)) } left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker)) }
right ?.also { rightNode -> addSubNode(rightNode, setOf(locker)) } right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker)) }
} }
while (coroutineContext.job.isActive) { while (coroutineContext.job.isActive) {
val foundParentNode = findParentNode(data) ?: return null val foundParentNode = findParentNode(data) ?: return null

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 kotlinx.coroutines.test.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartRWLockerTests { class SmartRWLockerTests {
@Test @Test
@@ -148,4 +151,17 @@ class SmartRWLockerTests {
assertEquals(false, locker.writeMutex.isLocked) 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

@@ -1,33 +1,31 @@
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.subscribe import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SpecialMutableStateFlowTests { class SpecialMutableStateFlowTests {
@Test @Test
fun simpleTest() = runTest { fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
specialMutableStateFlow.value = 1 mutableRedeliverStateFlow.value = 1
specialMutableStateFlow.first { it == 1 } mutableRedeliverStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value) assertEquals(1, mutableRedeliverStateFlow.value)
} }
@Test @Test
fun specialTest() = runTest { fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
lateinit var subscriberJob: Job lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) { subscriberJob = mutableRedeliverStateFlow.subscribe(this) {
when (it) { when (it) {
1 -> specialMutableStateFlow.value = 2 1 -> mutableRedeliverStateFlow.value = 2
2 -> subscriberJob.cancel() 2 -> subscriberJob.cancel()
} }
} }
specialMutableStateFlow.value = 1 mutableRedeliverStateFlow.value = 1
subscriberJob.join() subscriberJob.join()
assertEquals(2, specialMutableStateFlow.value) assertEquals(2, mutableRedeliverStateFlow.value)
} }
} }

View File

@@ -1,2 +0,0 @@
actual val AllowDeepInsertOnWorksTest: Boolean
get() = false

View File

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

View File

@@ -1,2 +0,0 @@
actual val AllowDeepInsertOnWorksTest: Boolean
get() = true

View File

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

View File

@@ -1,3 +1,5 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode
import dev.inmo.micro_utils.coroutines.collections.addSubNode import dev.inmo.micro_utils.coroutines.collections.addSubNode
import dev.inmo.micro_utils.coroutines.collections.findNode import dev.inmo.micro_utils.coroutines.collections.findNode
@@ -10,8 +12,6 @@ import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
expect val AllowDeepInsertOnWorksTest: Boolean
class SortedBinaryTreeNodeTests { class SortedBinaryTreeNodeTests {
@Test @Test
fun insertOnZeroLevelWorks() = runTest { fun insertOnZeroLevelWorks() = runTest {
@@ -46,7 +46,6 @@ class SortedBinaryTreeNodeTests {
} }
@Test @Test
fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) { fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) {
if (AllowDeepInsertOnWorksTest == false) return@runTest
val zeroNode = SortedBinaryTreeNode(0) val zeroNode = SortedBinaryTreeNode(0)
val rangeRadius = 500 val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>() val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()
@@ -124,7 +123,6 @@ class SortedBinaryTreeNodeTests {
} }
@Test @Test
fun deepInsertOnWorks() = runTest(timeout = 240.seconds) { fun deepInsertOnWorks() = runTest(timeout = 240.seconds) {
if (AllowDeepInsertOnWorksTest == false) return@runTest
val zeroNode = SortedBinaryTreeNode(0) val zeroNode = SortedBinaryTreeNode(0)
val rangeRadius = 500 val rangeRadius = 500
val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>() val nodes = mutableMapOf<Int, SortedBinaryTreeNode<Int>>()

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

@@ -1,2 +0,0 @@
actual val AllowDeepInsertOnWorksTest: Boolean
get() = true

View File

@@ -1,2 +0,0 @@
actual val AllowDeepInsertOnWorksTest: Boolean
get() = true

View File

@@ -1,2 +0,0 @@
actual val AllowDeepInsertOnWorksTest: Boolean
get() = true

View File

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

View File

@@ -30,3 +30,13 @@ allprojects {
} }
} }
} }
tasks.register("getPublishableModules") {
doLast {
rootProject.subprojects.each { project ->
if (project.plugins.hasPlugin('maven-publish')) {
println(":${project.name}")
}
}
}
}

View File

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

View File

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

View File

@@ -11,12 +11,20 @@ private String getCurrentVersionChangelog() {
return changelogDataOS.toString().trim() 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" apply plugin: "com.github.breadmoirai.github-release"
def githubReleaseToken = project.hasProperty(githubTokenVariableName) ? project.property(githubTokenVariableName).toString() : githubTokenVariableFromEnv
githubRelease { githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}" token githubReleaseToken
owner = "InsanusMokrassar" owner = "InsanusMokrassar"
repo = "MicroUtils" repo = "MicroUtils"

View File

@@ -8,6 +8,9 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
# https://github.com/google/ksp/issues/2491
ksp.useKSP2=false
# JS NPM # JS NPM
crypto_js_version=4.1.1 crypto_js_version=4.1.1
@@ -15,5 +18,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.24.8 version=0.26.4
android_code_version=288 android_code_version=303

View File

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

View File

@@ -1,46 +1,49 @@
[versions] [versions]
kt = "2.1.10" kt = "2.2.10"
kt-serialization = "1.8.0" kt-serialization = "1.9.0"
kt-coroutines = "1.10.1" kt-coroutines = "1.10.2"
kslog = "1.4.1" kotlinx-browser = "0.3"
jb-compose = "1.7.3" kslog = "1.5.0"
jb-exposed = "0.59.0"
jb-compose = "1.8.2"
jb-exposed = "0.61.0"
jb-dokka = "2.0.0" jb-dokka = "2.0.0"
sqlite = "3.49.1.0" sqlite = "3.50.1.0"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "3.1.1" ktor = "3.2.3"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "4.0.2" koin = "4.1.0"
okio = "3.10.2" okio = "3.16.0"
ksp = "2.1.10-1.0.31" ksp = "2.2.10-2.0.2"
kotlin-poet = "1.18.1" kotlin-poet = "2.2.0"
versions = "0.51.0" versions = "0.52.0"
nmcp = "1.1.0"
android-gradle = "8.7.+" android-gradle = "8.10.+"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.15.0" android-coreKtx = "1.17.0"
android-recyclerView = "1.4.0" android-recyclerView = "1.4.0"
android-appCompat = "1.7.0" android-appCompat = "1.7.1"
android-fragment = "1.8.6" android-fragment = "1.8.9"
android-espresso = "3.6.1" android-espresso = "3.7.0"
android-test = "1.2.1" android-test = "1.3.0"
android-props-minSdk = "21" android-props-minSdk = "21"
android-props-compileSdk = "35" android-props-compileSdk = "36"
android-props-buildTools = "35.0.0" android-props-buildTools = "36.0.0"
[libraries] [libraries]
@@ -55,6 +58,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-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" } 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-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
@@ -120,3 +124,4 @@ jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" } kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
nmcp-aggregation = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }

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" apply from: "$defaultProject"
project.group = "$group" apply from: "$enableMPPJvm"
apply from: "$enableMPPJs"
apply from: "$enableMPPWasmJs"
apply from: "$enableMPPAndroid"
apply from: "$publish" 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 = "240000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "240000"
}
}
}
}
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" apply from: "$defaultProject"
project.group = "$group" apply from: "$enableMPPJvm"
apply from: "$publish" 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 = "240000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "240000"
}
}
}
}
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 = "240000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "240000"
}
}
}
}
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" apply from: "$defaultProjectWithSerialization"
project.group = "$group" 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" apply from: "$publish"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) {
browser {
testTask {
useMocha {
timeout = "240000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "240000"
}
}
}
}
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,7 +1,3 @@
if (ext.getProperties()["do_publish"] == false) {
return
}
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
task javadocsJar(type: Jar) { task javadocsJar(type: Jar) {
@@ -23,72 +19,64 @@ publishing {
} }
developers { developers {
developer {
id = "InsanusMokrassar"
name = "Aleksei Ovsiannikov"
email = "ovsyannikov.alexey95@gmail.com"
}
developer { developer {
id = "InsanusMokrassar" id = "000Sanya"
name = "Aleksei Ovsiannikov" name = "Syrov Aleksandr"
email = "ovsyannikov.alexey95@gmail.com" email = "000sanya.000sanya@gmail.com"
} }
developer {
id = "000Sanya"
name = "Syrov Aleksandr"
email = "000sanya.000sanya@gmail.com"
}
} }
licenses { licenses {
license {
license { name = "Apache Software License 2.0"
name = "Apache Software License 2.0" url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
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)) { repositories {
maven { if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
name = "GithubPackages" maven {
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils") 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') credentials {
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD') 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 { if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
name = "InmoNexus" maven {
url = uri("https://nexus.inmo.dev/repository/maven-releases/") 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') credentials {
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD') 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 { if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
name = "sonatype" maven {
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 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') credentials {
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') 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")) { if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing' 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":false}}

View File

@@ -1,5 +1,6 @@
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
task javadocJar(type: Jar) { task javadocJar(type: Jar) {
from javadoc from javadoc
archiveClassifier = 'javadoc' archiveClassifier = 'javadoc'
@@ -55,42 +56,39 @@ publishing {
} }
} }
repositories { }
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) { }
maven { repositories {
name = "GithubPackages" if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils") maven {
name = "GithubPackages"
credentials { url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
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') 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" if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
url = uri("https://nexus.inmo.dev/repository/maven-releases/") maven {
name = "InmoNexus"
credentials { url = uri("https://nexus.inmo.dev/repository/maven-releases/")
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') 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" if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") maven {
name = "sonatype"
credentials { url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
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') 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 +97,13 @@ publishing {
if (project.hasProperty("signing.gnupg.keyName")) { if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing' apply plugin: 'signing'
signing { signing {
useGpgCmd() useGpgCmd()
sign publishing.publications sign publishing.publications
} }
task signAll { task signAll {
tasks.withType(Sign).forEach { tasks.withType(Sign).forEach {
dependsOn(it) 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":false},"type":"JVM"}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

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

View File

@@ -10,6 +10,7 @@ repositories {
dependencies { dependencies {
api project(":micro_utils.koin") api project(":micro_utils.koin")
api project(":micro_utils.ksp.generator")
api libs.kotlin.poet api libs.kotlin.poet
api libs.ksp api libs.ksp
} }

View File

@@ -26,6 +26,7 @@ import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_ksp.generator.safeClassName
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin import org.koin.core.Koin
@@ -237,15 +238,7 @@ class Processor(
""".trimIndent() """.trimIndent()
) )
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach { ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = runCatching { val type = safeClassName { it.type }
it.type.asTypeName()
}.getOrElse { e ->
if (e is KSTypeNotPresentException) {
e.ksType.toClassName()
} else {
throw e
}
}
val targetType = runCatching { val targetType = runCatching {
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type)) type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
}.getOrElse { e -> }.getOrElse { e ->

View File

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

View File

@@ -5,7 +5,6 @@ package dev.inmo.micro_utils.koin.generator.test
import kotlin.Any import kotlin.Any
import kotlin.Boolean import kotlin.Boolean
import kotlin.Deprecated
import kotlin.String import kotlin.String
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.definition.Definition import org.koin.core.definition.Definition
@@ -30,95 +29,59 @@ public val Koin.sampleInfo: Test<String>
/** /**
* @return Definition by key "sampleInfo" with [parameters] * @return Definition by key "sampleInfo" with [parameters]
*/ */
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
get(named("sampleInfo"), parameters)
/** /**
* @return Definition by key "sampleInfo" with [parameters] * @return Definition by key "sampleInfo" with [parameters]
*/ */
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
get(named("sampleInfo"), parameters)
/** /**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo" * Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/ */
@Deprecated( public fun Module.singleSampleInfo(createdAtStart: Boolean = false, definition: Definition<Test<String>>): KoinDefinition<Test<String>> = single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
"This definition is old style and should not be used anymore. Use singleSampleInfo instead",
ReplaceWith("singleSampleInfo"),
)
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/
public fun Module.singleSampleInfo(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/** /**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo" * Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/ */
@Deprecated( public fun Module.factorySampleInfo(definition: Definition<Test<String>>): KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
"This definition is old style and should not be used anymore. Use factorySampleInfo instead",
ReplaceWith("factorySampleInfo"),
)
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/
public fun Module.factorySampleInfo(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
/** /**
* @return Definition by key "test" with [parameters] * @return Definition by key "test" with [parameters]
*/ */
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
= get(named("test"), parameters)
/** /**
* @return Definition by key "test" with [parameters] * @return Definition by key "test" with [parameters]
*/ */
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
= get(named("test"), parameters)
/** /**
* Will register [definition] with [org.koin.core.module.Module.single] and key "test" * Will register [definition] with [org.koin.core.module.Module.single] and key "test"
*/ */
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart = createdAtStart, definition = definition)
definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart =
createdAtStart, definition = definition)
/** /**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test" * Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
*/ */
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>): public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("test"), definition = definition)
KoinDefinition<T> = factory(named("test"), definition = definition)
/** /**
* @return Definition by key "testNullable" with [parameters] * @return Definition by key "testNullable" with [parameters]
*/ */
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? = public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
null): T? = getOrNull(named("testNullable"), parameters)
/** /**
* @return Definition by key "testNullable" with [parameters] * @return Definition by key "testNullable" with [parameters]
*/ */
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? = public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
null): T? = getOrNull(named("testNullable"), parameters)
/** /**
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable" * Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
*/ */
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false, public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"), createdAtStart = createdAtStart, definition = definition)
noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"),
createdAtStart = createdAtStart, definition = definition)
/** /**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable" * Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
*/ */
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>): public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("testNullable"), definition = definition)
KoinDefinition<T> = factory(named("testNullable"), definition = definition)

View File

@@ -4,8 +4,14 @@ import org.koin.core.definition.Definition
import org.koin.core.module.Module 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 * Declares a factory with a random qualifier in the Koin module.
* not care about how :) * 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( inline fun <reified T : Any> Module.factoryWithRandomQualifier(
noinline definition: Definition<T> noinline definition: Definition<T>

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