Compare commits

...

330 Commits

Author SHA1 Message Date
4f270d9047 generate docs for a lot of API (test try) 2026-02-24 18:18:10 +06:00
3df90b1993 update sqlite (wow^^) 2026-02-24 15:40:05 +06:00
8e8915d84c update dependencies 2026-02-24 15:16:45 +06:00
50369e0904 migrate onto 0.29.0 2026-02-24 15:11:43 +06:00
3ae2c84c08 fill changelog 2026-02-15 18:38:07 +06:00
b2e6ab51cb update runCatchingLogging to rethrow CanellationException 2026-02-15 18:35:43 +06:00
d6496f02e6 start 0.28.1 2026-02-15 18:21:57 +06:00
38e98327b4 fill changelog for 0.28.0 2026-02-04 21:26:32 +06:00
c72a6fda5d Merge pull request #638 from InsanusMokrassar/0.28.0
0.28.0
2026-01-27 23:37:56 +06:00
23d58d42ee make it buildable 2026-01-27 16:19:43 +06:00
d972cebf47 start update dependencies 2026-01-24 19:41:35 +06:00
3558a5135b start 0.28.0 2026-01-24 16:12:54 +06:00
82460d2ce3 Merge pull request #633 from InsanusMokrassar/0.27.0
0.27.0
2026-01-15 12:53:41 +06:00
4ce2b7d3b2 update compose versions 2026-01-15 12:07:53 +06:00
2c10bb3c88 update dependencies
io.ktor:ktor-client-core 3.3.2 -> 3.3.3

com.squareup.okio:okio 3.16.2 -> 3.16.4

com.google.devtools.ksp:symbol-processing-api 2.3.2 -> 2.3.4
2026-01-15 11:56:03 +06:00
01bed4a6c9 start 0.27.0 2026-01-15 11:34:35 +06:00
52c4867468 Merge pull request #624 from InsanusMokrassar/0.26.8
0.26.8
2025-11-12 22:47:29 +06:00
92b3fd25e7 fill changelog 2025-11-12 22:25:33 +06:00
e0e1da5082 add suspendPoint 2025-11-12 22:20:17 +06:00
80953f5d09 update ktor and android script config 2025-11-12 18:18:01 +06:00
2849db57f2 start 0.26.8 2025-11-12 18:01:11 +06:00
0170b92272 Merge pull request #622 from InsanusMokrassar/0.26.7
0.26.7
2025-11-05 15:41:10 +06:00
7bcb81400b fill changelog 2025-11-05 15:14:21 +06:00
078aedfb68 update dependencies 2025-11-05 13:42:27 +06:00
cb56bf9793 Revert "add getCurrentLocale and compose translation"
This reverts commit fce47897d5.
2025-11-05 12:29:33 +06:00
b152986b4e improve SmartKeyRWLockerTests and fix SmartSemaphore 2025-11-05 12:27:24 +06:00
fce47897d5 add getCurrentLocale and compose translation 2025-10-31 19:34:09 +06:00
0b701a3e99 start 0.26.7 2025-10-31 11:21:06 +06:00
9dad353957 Merge pull request #619 from InsanusMokrassar/0.26.6
0.26.6
2025-10-19 16:24:37 +06:00
89e16b7bdb update dependencies
io.ktor:ktor-* 3.3.0 -> 3.3.1
com.squareup.okio:okio 3.16.0 -> 3.16.2
org.jetbrains.dokka:dokka-gradle-plugin 2.0.0 -> 2.1.0
2025-10-19 16:23:23 +06:00
c2965da341 start 0.26.6 2025-10-19 15:24:41 +06:00
ffb072dc5f Merge pull request #613 from InsanusMokrassar/0.26.5
0.26.5
2025-10-04 14:39:30 +06:00
a247dbcb02 rollback sqlite update 2025-10-04 14:20:18 +06:00
1dd71175f4 update dependencies 2025-09-30 23:29:04 +06:00
bbe62c0e7b start 0.26.5 2025-09-30 23:06:42 +06:00
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
2fbd14956d small update in SortedBinaryTreeNodeTests 2025-03-03 15:49:46 +06:00
24657b43be update InfinityPagedComponents 2025-03-03 15:17:54 +06:00
659d3b6fa5 update dependencies and fill changelog 2025-03-03 14:53:49 +06:00
339483c8a3 disable hard test of binary trees for js 2025-03-03 14:42:05 +06:00
51ec46bbd7 update timeout for deepInsertOnWorks 2025-03-03 13:40:33 +06:00
8a059cc26d fix of build 2025-03-03 12:21:04 +06:00
ddb8e1efb4 fix build 2025-03-03 12:15:36 +06:00
3bf2ed5168 fix of InfinityPagedComponent 2025-03-03 08:57:40 +06:00
e70d34d91a add tests for paged loading components 2025-03-02 23:06:42 +06:00
7a650f5c2f Disallow overrides in repos 2025-03-02 16:12:23 +06:00
fc6f5ae2ee start 0.25.0 2025-03-02 16:10:25 +06:00
3b7dde3cb1 add KDocs for InfinityPagedComponentContext and PagedComponent 2025-03-01 22:16:48 +06:00
0a5cfaba18 complete InfinityPagedComponentContext 2025-03-01 22:04:41 +06:00
ea527b5e91 start add paged/loading component 2025-03-01 16:49:20 +06:00
85f11439e8 adapt gradle files for new tests 2025-02-26 22:01:39 +06:00
98c7b48625 add SortedBinaryTreeNode 2025-02-26 20:57:17 +06:00
8b007bb3af start 0.24.8 2025-02-26 20:56:56 +06:00
89e2d88d1c Merge pull request #548 from InsanusMokrassar/0.24.7
0.24.7
2025-02-20 08:52:41 +06:00
260399e965 AbstractExposedKeyValuesRepo#set fix 2025-02-19 23:30:54 +06:00
de72843b8e Retry 2025-02-19 23:29:49 +06:00
b589142d9f update dependencies 2025-02-19 14:18:08 +06:00
f479c85869 remove redundant K2 enabling (enabled by default since kotlin 2.0 2025-02-19 14:13:53 +06:00
26992c039a more fixes to god of fixes 2025-02-19 14:13:02 +06:00
ef50e1a24f try K2? 2025-02-19 12:28:47 +06:00
2f201670d2 fix? 2025-02-18 16:35:16 +06:00
90c80573a0 potential fix for DirectFullKeyValuesCacheRepo 2025-02-18 15:36:15 +06:00
059519cdca fixes in FullKeyValueCacheRepo 2025-02-18 15:25:49 +06:00
55647e2e2f start 0.24.7 2025-02-18 15:10:08 +06:00
1802be68ef Merge pull request #544 from InsanusMokrassar/0.24.6
0.24.6
2025-02-15 10:05:05 +06:00
aa79496a36 upfill changelog 2025-02-15 10:04:12 +06:00
4bfa4c32aa fix of #541 2025-02-14 13:16:14 +06:00
d8ca29eab1 start deprecation of safely functions 2025-02-14 12:37:40 +06:00
f7a9f2e13d fixes and forcing uppercase for new files 2025-02-13 12:13:15 +06:00
af286d3d53 add disabling of publishing for ksp test modules 2025-02-13 11:04:25 +06:00
9a20e8155e update dependnecies 2025-02-13 10:33:40 +06:00
b81af7e1c8 fill changelog 2025-02-13 09:09:01 +06:00
74563bbac9 small hotfix 2025-02-12 21:20:14 +06:00
07ad79f0b3 add including of annotations from source function on variation generation 2025-02-12 21:19:40 +06:00
5c28255e06 fixes for generics 2025-02-12 14:32:14 +06:00
99783f281d generate sample functions 2025-02-12 12:37:08 +06:00
a5008c3d15 first version of parameters variations 2025-02-12 12:20:22 +06:00
f807f2beeb start add variations generator 2025-02-11 13:50:58 +06:00
db34b25ef2 start 0.24.6 2025-02-10 13:35:52 +06:00
96daf11fd4 Merge pull request #538 from InsanusMokrassar/0.24.5
0.24.5
2025-01-29 09:08:08 +06:00
f7e98dfd2d update kotlin 2025-01-29 08:56:37 +06:00
61277e92bd update dependencies 2025-01-27 09:35:54 +06:00
32ef9f399f start 0.24.5 2025-01-27 09:21:39 +06:00
54e6ca5dc3 Merge pull request #535 from InsanusMokrassar/0.24.4
0.24.4
2025-01-19 09:11:54 +06:00
a8e226786d improve selectByIds and fill changelog 2025-01-17 19:46:29 +06:00
ce717a4c9f improvements in FSM 2025-01-17 18:34:54 +06:00
fd41bf0ae7 start 0.24.4 2025-01-17 14:41:09 +06:00
b2b68bf29f Merge pull request #533 from InsanusMokrassar/0.24.3
0.24.3
2025-01-15 13:36:29 +06:00
b87c29c354 replace tests of GenerateSealed*Workaround to be jvm-only 2025-01-15 13:25:58 +06:00
24977822c9 fixes in GenerateSealedTypesWorkaround 2025-01-15 13:22:07 +06:00
647daa8627 start 0.24.3 2025-01-15 13:21:37 +06:00
a372efacb1 Merge pull request #532 from InsanusMokrassar/0.24.2
0.24.2
2025-01-14 10:13:53 +06:00
f40d33db2a update exposed 2025-01-14 10:09:16 +06:00
a5eb0fbd24 add opportunity to generate types lists for sealed subtypes 2025-01-14 09:57:52 +06:00
307c8030af start 0.24.2 2025-01-14 09:57:52 +06:00
a9016465fa update dependencies 2025-01-14 09:57:32 +06:00
358b70eb5f start 0.24.1 2025-01-14 09:57:32 +06:00
6fcbb80a71 Merge pull request #530 from InsanusMokrassar/renovate/migrate-config
Migrate renovate config
2025-01-10 08:45:48 +06:00
renovate[bot]
643f6c420b Migrate config renovate.json 2025-01-10 02:41:30 +00:00
ac68b0b941 Merge pull request #519 from InsanusMokrassar/0.24.0
0.24.0
2024-12-21 09:15:11 +06:00
d62f67bd88 update dokka 2024-12-21 09:13:10 +06:00
8718c5e310 update dependencies and add kdocs for percentage 2024-12-21 09:02:31 +06:00
e8273ab80c Progress -> Percentage 2024-12-08 22:34:28 +06:00
2718605987 start 0.24.0 2024-12-08 22:27:05 +06:00
d99538d80b Merge pull request #517 from InsanusMokrassar/0.23.2
0.23.2
2024-12-06 13:47:33 +06:00
ce7a1e4e21 update dependencies 2024-12-06 13:17:09 +06:00
921734763d update workflows 2024-12-04 10:19:41 +06:00
18c608f569 small improvement in AccumulatorFlow 2024-12-04 10:16:19 +06:00
b915f6ece2 start 0.23.2 2024-12-04 10:12:39 +06:00
24347b422c Merge pull request #513 from InsanusMokrassar/0.23.1
0.23.1
2024-11-25 15:31:41 +06:00
e5f4ae647f update dependencies 2024-11-25 15:27:55 +06:00
72202b8a21 add StyleSheetsAggregator 2024-11-25 15:11:30 +06:00
dbc14d41de start 0.23.1 2024-11-15 21:39:17 +06:00
1dca5ea00d Merge pull request #499 from InsanusMokrassar/0.23.0
0.23.0
2024-10-30 15:07:58 +06:00
f03d1d788c fill changelog and update ktor 2024-10-30 15:06:19 +06:00
fcac6f9fa8 update ksp 2024-10-29 17:29:46 +06:00
ca0cd433c9 Merge branch 'master' into 0.23.0 2024-10-29 17:23:05 +06:00
39589fdbd0 Merge pull request #507 from InsanusMokrassar/0.22.9
0.22.9
2024-10-29 13:42:30 +06:00
605fc3cff9 start add direct caches 2024-10-29 13:14:41 +06:00
12cd6f48f8 start 0.22.9 2024-10-29 12:00:58 +06:00
1f57478d10 Update build.yml 2024-10-27 13:53:10 +06:00
7ef4c5d282 Merge branch 'master' into 0.23.0 2024-10-27 13:42:14 +06:00
f09d92be32 Merge pull request #506 from InsanusMokrassar/0.22.8
0.22.8
2024-10-24 16:32:18 +06:00
6245b36bdb add pads and breakAsPairs extensions 2024-10-24 15:30:10 +06:00
54cc353bcc start 0.22.8 2024-10-24 15:26:02 +06:00
b7abba099c Merge pull request #503 from InsanusMokrassar/0.22.7
hotfix due to deprecated annotation
2024-10-19 15:40:14 +06:00
c5dbd10335 hotfix due to deprecated annotation 2024-10-19 15:39:00 +06:00
a44e3e953d Merge pull request #502 from InsanusMokrassar/0.22.7
0.22.7
2024-10-19 15:26:37 +06:00
ee2521cb01 update dependencies 2024-10-19 15:21:00 +06:00
4625dfb857 replace GenerateSealedWorkaround 2024-10-19 13:44:37 +06:00
b9a2653066 start 0.22.7 2024-10-19 13:41:25 +06:00
fcaa327660 Merge pull request #501 from InsanusMokrassar/0.22.6
0.22.6
2024-10-17 22:40:53 +06:00
496117d517 improvements in ksp parts 2024-10-17 21:04:41 +06:00
8ce7d37b72 start 0.22.6 2024-10-17 20:52:45 +06:00
25391609b9 Update libs.versions.toml 2024-10-12 08:37:44 +06:00
f0b7b9c5e5 migrate to 0.23.0 2024-10-11 23:38:53 +06:00
301cdaa2c2 fix of build after migration (wat) 2024-10-11 23:32:11 +06:00
fce7ec8912 start migration onto ktor 3.0 2024-10-10 21:53:26 +06:00
24bd403549 start 0.22.6 2024-10-10 21:38:49 +06:00
46c89c48a9 Merge pull request #492 from InsanusMokrassar/0.22.5
0.22.5
2024-10-06 12:38:13 +06:00
bad9a53fdb update dependencies 2024-10-06 12:28:48 +06:00
0bce7bd60a fix of #488 2024-10-06 12:25:42 +06:00
2f70a1cfb4 solution of #489 2024-10-01 20:30:45 +06:00
bfb6e738ee add debouncedBy extension 2024-09-26 23:50:57 +06:00
c7ad9aae07 start 0.22.5 2024-09-26 23:17:00 +06:00
fecd719239 Merge pull request #491 from InsanusMokrassar/0.22.4
0.22.4
2024-09-25 22:53:53 +06:00
18d6ac31b5 update exposed 2024-09-25 22:09:50 +06:00
d8dbebfc7e start 0.22.4 2024-09-25 21:03:18 +06:00
16463d0eb9 Merge pull request #487 from InsanusMokrassar/0.22.3
0.22.3
2024-09-20 13:27:16 +06:00
837cac644d add extensions in JS 2024-09-20 13:14:50 +06:00
e83e0a8535 update dependencies 2024-09-20 12:44:10 +06:00
2e309c31a6 start 0.22.3 2024-09-20 12:18:17 +06:00
625db02651 Merge pull request #479 from InsanusMokrassar/0.22.2
0.22.2
2024-08-30 20:57:10 +06:00
e3144c08c1 update dependencies 2024-08-30 20:48:34 +06:00
40e25970ad start 0.22.2 2024-08-30 20:46:07 +06:00
4ccb8a8d11 Merge pull request #477 from InsanusMokrassar/0.22.1
0.22.1
2024-08-29 15:02:34 +06:00
26d1c3d9ef update dependencies 2024-08-29 14:49:29 +06:00
cc8fe7b001 start 0.22.1 2024-08-29 14:14:20 +06:00
4601eab864 update gradle scripts to improve performance of configuration 2024-08-27 18:57:59 +06:00
1ee9c88ffd Merge pull request #459 from InsanusMokrassar/0.22.0
0.22.0
2024-08-11 16:51:30 +06:00
7feac213f6 update android code version 2024-08-11 16:51:09 +06:00
c75a9c0f61 update dependencies 2024-08-11 16:44:08 +06:00
afd0c9784f Update libs.versions.toml 2024-08-11 12:41:30 +06:00
f1ddbea06e update android version 2024-08-09 23:06:50 +06:00
42bf3cd4e3 update kotlin-poet 2024-08-09 22:21:53 +06:00
27f5549f56 improve tests of repos 2024-08-09 21:58:09 +06:00
b0569f8421 add tests for cruds 2024-08-09 19:22:32 +06:00
8d86f29325 Revert "small change in FullKeyValuesCacheRepoTests and FullKeyValueCacheRepoTests"
This reverts commit 3db8d2291e73812f9eebcec1c0ad088ad0ca854a.
2024-08-07 16:50:13 +06:00
aeca32498a Revert "Update build.yml"
This reverts commit ab0b3a8f0d2c2a38eab5c9910f8ae5543a487c97.
2024-08-07 16:50:13 +06:00
e8232664f3 update delay on creating tests in cache 2024-08-07 16:50:13 +06:00
efa869f91a Update build.yml 2024-08-07 16:50:13 +06:00
dbff3d7cf1 small change in FullKeyValuesCacheRepoTests and FullKeyValueCacheRepoTests 2024-08-07 16:50:13 +06:00
9e70f73684 upgrade kotlin 2024-08-07 16:50:13 +06:00
9f2f0de0c4 add check of emptiness in map reslozations of KV/KVs repos 2024-08-07 16:50:13 +06:00
bf293a8f8f fix of tests 2024-08-07 16:50:13 +06:00
5fbc1a132f improvements in SmartRWLocker and breaking of FullKeyValueCacheRepo tests 2024-08-07 16:50:13 +06:00
36093b9741 fix of full caches initial actualization and realize MapKeyValuesRepo methods getAll 2024-08-07 16:50:13 +06:00
8713fb04c5 small change in MapWriteKeyValuesRepo.set and remove duration of FullKeyValuesCacheRepoTests.creatingWorksProperly 2024-08-07 16:50:13 +06:00
ea82a59f31 update gradle wrapper version 2024-08-07 16:50:13 +06:00
18f67cd4f2 small improvement of test data in Full Cache tests 2024-08-07 16:50:13 +06:00
120e7228c7 add FullKeyValuesCacheRepoTests and small rewrite of MapWriteKeyValuesRepo.set(Map<Key, List<Value>>) function 2024-08-07 16:50:13 +06:00
892fa90c37 start add cache repos tests 2024-08-07 16:50:13 +06:00
491dac5bf0 update exposed 2024-08-07 16:50:13 +06:00
2ab06fbafd change version 2024-08-07 16:50:11 +06:00
e68735d061 Revert "migrations onto new libraries"
This reverts commit 647cd7d7b4.
2024-08-07 16:46:49 +06:00
9ba4d98c30 migrations onto new libraries 2024-08-07 16:46:49 +06:00
19bbfd4916 downgrade kotlin and fix build 2024-08-07 16:46:49 +06:00
d94cd7ea94 some fixes 2024-08-07 16:46:49 +06:00
0dd8f41eb0 update dependencies 2024-08-07 16:46:49 +06:00
f1ab5ab51f update kotlinx serialization 2024-08-07 16:46:49 +06:00
a8056f3120 update dependencies 2024-08-07 16:46:49 +06:00
09c0cdebb5 update dependencies 2024-08-07 16:46:49 +06:00
629d7c7a82 temporal project in upgrade 2024-08-07 16:46:49 +06:00
a0dd1aec3d trying to update to new compose 2024-08-07 16:46:49 +06:00
17d6377902 Merge pull request #466 from InsanusMokrassar/0.21.6
0.21.6
2024-07-26 19:16:13 +06:00
ad401105a1 fixes in ksp sealed generator 2024-07-26 17:17:11 +06:00
4913e99c2e start 0.21.6 2024-07-26 16:40:30 +06:00
16b97029cf Merge pull request #465 from InsanusMokrassar/0.21.5
0.21.5
2024-07-25 02:48:27 +06:00
481130c9bb fill changelog 2024-07-25 02:30:10 +06:00
698ed6718d improve sealed workaround generation 2024-07-25 02:27:33 +06:00
d164813bb4 Revert "start moving to kotlinx-io"
This reverts commit 52157ee0e7.
2024-07-25 01:20:32 +06:00
52157ee0e7 start moving to kotlinx-io 2024-07-24 00:34:49 +06:00
877b62fe5d start 0.21.5 2024-07-24 00:06:18 +06:00
d823a02971 Merge pull request #464 from InsanusMokrassar/0.21.4
add additional targets with natives support
2024-07-16 17:58:02 +06:00
e950056e3b add additional targets with natives support 2024-07-16 17:18:26 +06:00
70014ba233 Merge pull request #463 from InsanusMokrassar/0.21.4
0.21.4
2024-07-16 14:23:41 +06:00
4425f24a20 add support of native platforms in common compose and coroutines compose 2024-07-16 14:16:08 +06:00
410964a44b start 0.21.4 2024-07-16 14:02:36 +06:00
30389e8536 Merge pull request #462 from InsanusMokrassar/0.21.3
0.21.3
2024-07-15 16:33:28 +06:00
398 changed files with 15745 additions and 4161 deletions

View File

@@ -17,8 +17,9 @@ jobs:
mv gradle.properties.tmp gradle.properties
- name: Build
run: ./gradlew build
- name: Publish
- name: Publish to InmoNexus
continue-on-error: true
run: ./gradlew publishAllPublicationsToGiteaRepository
run: ./gradlew publishAllPublicationsToInmoNexusRepository
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INMONEXUS_USER: ${{ secrets.INMONEXUS_USER }}
INMONEXUS_PASSWORD: ${{ secrets.INMONEXUS_PASSWORD }}

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
.idea
.vscode
.kotlin
out/*
*.iml
target
@@ -8,6 +10,7 @@ settings.xml
.gradle/
build/
out/
bin/
secret.gradle
local.properties
@@ -17,5 +20,6 @@ publishing.sh
local.*
local/
**/*.local.*
.kotlin/

View File

@@ -1,5 +1,471 @@
# Changelog
## 0.29.0
* `Versions`:
* `Kotlin`: `2.3.0` -> `2.3.10`
* `KSLog`: `1.5.2` -> `1.6.0`
* `KSP`: `2.3.4` -> `2.3.6`
* `Compose`: `1.10.0` -> `1.10.1`
* `SQLite`: `3.50.1.0` -> `3.51.2.0`
* `Coroutines`:
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions
## 0.28.0
**THIS VERSION CONTAINS BREAKING CHANGES DUE TO EXPOSED 1.0.0 UPDATE**
* `Versions`:
* `Kotlin`: `2.2.21` -> `2.3.0`
* `Serialization`: `1.9.0` -> `1.10.0`
* `Exposed`: `0.61.0` -> `1.0.0` (**MAJOR VERSION UPDATE**)
* `Ktor`: `3.3.3` -> `3.4.0`
* `NMCP`: `1.2.0` -> `1.2.1`
* `Repos`:
* `Exposed`:
* All Exposed-based repositories have been updated to support Exposed 1.0.0 API changes
* Import paths have been migrated to new `org.jetbrains.exposed.v1.*` package structure
* `Pagination`:
* `Exposed`:
* Updated to use new Exposed 1.0.0 import paths
## 0.27.0
* `Versions`:
* `Ktor`: `3.3.2` -> `3.3.3`
* `Okio`: `3.16.2` -> `3.16.4`
* `KSP`: `2.3.2` -> `2.3.4`
* `Compose`: `1.9.3` -> `1.10.0`
* `Compose Material3`: `1.9.0` -> `1.10.0-alpha05`
## 0.26.8
* `Versions`:
* `KSLog`: `1.5.1` -> `1.5.2`
* `Compose`: `1.9.2` -> `1.9.3`
* `Ktor`: `3.3.1` -> `3.3.2`
* `Coroutines`:
* Add simple suspend function `suspendPoint` which will ensure that current coroutine is active to let it be
destroyable even in case it have non-suspendable nature
## 0.26.7
* `Versions`:
* `Kotlin`: `2.2.20` -> `2.2.21`
* `Compose`: `1.8.2` -> `1.9.2`
* `KSP`: `2.2.20-2.0.3` -> `2.3.1`
* `Coroutines`:
* Fix `SmartSemaphore.waitRelease` to wait for the exact number of permits
* Improve `SmartKeyRWLocker` tests
* `KSP`:
* `Sealed`/`ClassCasts`/`Variations`:
* Add workaround for `NoSuchElementException` to improve processors stability on new `KSP`
* `Koin`:
* `Generator`:
* Handle missing annotation values safely (`NoSuchElementException` workaround)
* `Android`:
* `Pickers`:
* Add dependency `androidx.compose.material:material-icons-extended`
## 0.26.6
* `Versions`:
* `Ktor`: `3.3.0` -> `3.3.1`
* `Okio`: `3.16.0` -> `3.16.2`
## 0.26.5
* `Versions`:
* `Kotlin`: `2.2.10` -> `2.2.20`
* `KSLog`: `1.5.0` -> `1.5.1`
* `Ktor`: `3.2.3` -> `3.3.0`
* `KotlinX Browser`: `0.3` -> `0.5.0`
* `Koin`: `4.1.0` -> `4.1.1`
## 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
* `Versions`:
* `Ktor`: `3.1.0` -> `3.1.1`
* `KSP`: `2.1.10-1.0.30` -> `2.1.10-1.0.31`
* `Common`:
* `Compose`:
* Add component `LoadableComponent`
* `Coroutines`:
* Add `SortedBinaryTreeNode`
* `Pagination`:
* `Compose`:
* Add components `PagedComponent` and `InfinityPagedComponent`
## 0.24.7
* `Versions`:
* `SQLite`: `3.49.0.0` -> `3.49.1.0`
* `Common`:
* Add `retryOnFailure` utility for simple retries code writing
* `Repos`:
* `Cache`:
* Fix of `FullKeyValueCacheRepo` fields usage
* `Exposed`:
* `AbstractExposedKeyValuesRepo` will produce `onValueRemoved` event on `set` if some data has been removed
## 0.24.6
* `Versions`:
* `KSLog`: `1.4.0` -> `1.4.1`
* `Exposed`: `0.58.0` -> `0.59.0`
* `SQLite`: `3.48.0.0` -> `3.49.0.0`
* `AndroidFragment`: `1.8.5` -> `1.8.6`
* `Coroutines`:
* Safely functions has been replaced with `Logging` variations (resolve of [#541](https://github.com/InsanusMokrassar/MicroUtils/issues/541))
* `KSP`:
* `Variations`:
* Module has been created
## 0.24.5
* `Versions`:
* `Kotlin`: `2.1.0` -> `2.1.10`
* `SQLite`: `3.47.2.0` -> `3.48.0.0`
* `Koin`: `4.0.1` -> `4.0.2`
* `Android RecyclerView`: `1.3.2` -> `1.4.0`
## 0.24.4
* `Repos`:
* `Exposed`:
* Improve `CommonExposedRepo.selectByIds`
* `FSM`:
* Fixes and improvements
## 0.24.3
* `Ksp`:
* `Sealed`:
* Fixes in processing of `GenerateSealedTypesWorkaround` annotations
## 0.24.2
* `Versions`:
* `Exposed`: `0.57.0` -> `0.58.0`
* `Ksp`:
* `Sealed`:
* Add annotation `GenerateSealedTypesWorkaround` which allow to generate `subtypes` lists
## 0.24.1
* `Versions`:
* `Serialization`: `1.7.3` -> `1.8.0`
* `SQLite`: `3.47.1.0` -> `3.47.2.0`
* `Koin`: `4.0.0` -> `3.10.2`
* `OKio`: `3.9.1` -> `3.10.2`
## 0.24.0
* `Versions`:
* `Coroutines`: `1.9.0` -> `1.10.1`
* `KSLog`: `1.3.6` -> `1.4.0`
* `Compose`: `1.7.1` -> `1.7.3`
* `Ktor`: `3.0.2` -> `3.0.3`
* `Common`:
* Rename `Progress` to more common `Percentage`. `Progress` now is typealias
* Fix of `Progress.compareTo` extension
## 0.23.2
* `Versions`:
* `Kotlin`: `2.0.21` -> `2.1.0`
* `Exposed`: `0.56.0` -> `0.57.0`
* `Xerial SQLite`: `3.47.0.0` -> `3.47.1.0`
* `Ktor`: `3.0.1` -> `3.0.2`
* `Coroutines`:
* Small refactor in `AccumulatorFlow` to use `runCatching` instead of `runCatchingSafely`
## 0.23.1
* `Versions`:
* `Compose`: `1.7.0` -> `1.7.1`
* `Exposed`: `0.55.0` -> `0.56.0`
* `Xerial SQLite`: `3.46.1.3` -> `3.47.0.0`
* `Android CoreKTX`: `1.13.1` -> `1.15.0`
* `Android Fragment`: `1.8.4` -> `1.8.5`
* `Coroutines`:
* `Compose`:
* Add `StyleSheetsAggregator`
## 0.23.0
**THIS UPDATE MAY CONTAINS SOME BREAKING CHANGES (INCLUDING BREAKING CHANGES IN BYTECODE LAYER) RELATED TO UPDATE OF
KTOR DEPENDENCY**
**THIS UPDATE CONTAINS CHANGES ACCORDING TO MIGRATION [GUIDE FROM KTOR](https://ktor.io/docs/migrating-3.html)**
* `Versions`:
* `Ktor`: `2.3.12` -> `3.0.1`
* `Ktor`:
* `Common`:
* Extension `Input.downloadToTempFile` has changed its receiver to `Source`. Its API can be broken
* `Client`:
* Extension `HttpClient.tempUpload` has changed type of `onUpload` argument from `OnUploadCallback` to `ProgressListener`
* All extensions `HttpClient.uniUpload` have changed type of `onUpload` argument from `OnUploadCallback` to `ProgressListener`
* `Server`:
* Remove redundant `ApplicationCall.respond` extension due to its presence in the ktor library
## 0.22.9
* `Repos`:
* `Cache`:
* Add direct caching repos
## 0.22.8
* `Common`:
* Add `List.breakAsPairs` extension
* Add `Sequence.padWith`/`Sequence.padStart`/`Sequence.padEnd` and `List.padWith`/`List.padStart`/`List.padEnd` extensions
## 0.22.7
* `Versions`:
* `Kotlin`: `2.0.20` -> `2.0.21`
* `Compose`: `1.7.0-rc01` -> `1.7.0`
* `KSP`:
* `Sealed`:
* Change package of `GenerateSealedWorkaround`. Migration: replace `dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround` -> `dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround`
## 0.22.6
* `KSP`:
* `Generator`:
* Add extension `KSClassDeclaration.buildSubFileName`
* Add extension `KSClassDeclaration.companion`
* Add extension `KSClassDeclaration.resolveSubclasses`
* `Sealed`:
* Improvements
## 0.22.5
* `Versions`:
* `Compose`: `1.7.0-beta02` -> `1.7.0-rc01`
* `SQLite`: `3.46.1.2` -> `3.46.1.3`
* `AndroidXFragment`: `1.8.3` -> `1.8.4`
* `Common`:
* Add extension `withReplacedAt`/`withReplaced` ([#489](https://github.com/InsanusMokrassar/MicroUtils/issues/489))
* `Coroutines`:
* Add extension `Flow.debouncedBy`
* `Ktor`:
* `Server`:
* Add `KtorApplicationConfigurator.Routing.Static` as solution for [#488](https://github.com/InsanusMokrassar/MicroUtils/issues/488)
## 0.22.4
* `Versions`:
* `Exposed`: `0.54.0` -> `0.55.0`
* `SQLite`: `3.46.1.0` -> `3.46.1.2`
## 0.22.3
* `Versions`:
* `Serialization`: `1.7.2` -> `1.7.3`
* `Coroutines`: `1.8.1` -> `1.9.0`
* `Compose`: `1.7.0-alpha03` -> `1.7.0-beta02`
* `Koin`: `3.5.6` -> `4.0.0`
* `Okio`: `3.9.0` -> `3.9.1`
* `AndroidFragment`: `1.8.2` -> `1.8.3`
* `androidx.compose.material3:material3` has been replaced with `org.jetbrains.compose.material3:material3`
* `Common`:
* `JS`:
* Add several useful extensions
* `Compose`:
* `JS`:
* Add several useful extensions
## 0.22.2
* `Versions`:
* `Exposed`: `0.53.0` -> `0.54.0`
* `SQLite`: `3.46.0.1` -> `3.46.1.0`
## 0.22.1
* `Versions`:
* `Kotlin`: `2.0.10` -> `2.0.20`
* `Serialization`: `1.7.1` -> `1.7.2`
* `KSLog`: `1.3.5` -> `1.3.6`
* `Compose`: `1.7.0-alpha02` -> `1.7.0-alpha03`
* `Ktor`: `2.3.11` -> `2.3.12`
## 0.22.0
**THIS UPDATE CONTAINS BREAKING CHANGES ACCORDING TO UPDATE UP TO KOTLIN 2.0**
* `Versions`:
* `Kotlin`: `1.9.23` -> `2.0.10`
* `Serialization`: `1.6.3` -> `1.7.1`
* `KSLog`: `1.3.4` -> `1.3.5`
* `Compose`: `1.6.2` -> `1.7.0-alpha02`
* `Exposed`: `0.50.1` -> `0.53.0`
* `AndroidAppCompat`: `1.6.1` -> `1.7.0`
* `AndroidFragment`: `1.7.1` -> `1.8.2`
## 0.21.6
* `KSP`:
* `Sealed`:
* Fixes in generation
## 0.21.5
* `KSP`:
* Add utility functions `KSClassDeclaration.findSubClasses`
* `Sealed`:
* Improve generation
## 0.21.4
* `Common`:
* `Compose`:
* Add support of mingw, linux, arm64 targets
* `Coroutines`:
* `Compose`:
* Add support of mingw, linux, arm64 targets
* `Koin`:
* Add support of mingw, linux, arm64 targets
* `KSP`:
* `ClassCasts`:
* Add support of mingw, linux, arm64 targets
* `Sealed`:
* Add support of mingw, linux, arm64 targets
## 0.21.3
* `Colors`:

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
/**
* Utility functions for creating Android AlertDialogs with simplified API.
*/
@file:Suppress("NOTHING_TO_INLINE", "unused")
package dev.inmo.micro_utils.android.alerts.common
@@ -6,8 +9,21 @@ import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
/**
* Type alias for alert dialog button callbacks.
*/
typealias AlertDialogCallback = (DialogInterface) -> Unit
/**
* Creates an [AlertDialog.Builder] template with configurable title and buttons.
* This provides a simplified API for creating alert dialogs with positive, negative, and neutral buttons.
*
* @param title Optional dialog title
* @param positivePair Optional positive button as a pair of (text, callback)
* @param neutralPair Optional neutral button as a pair of (text, callback)
* @param negativePair Optional negative button as a pair of (text, callback)
* @return An [AlertDialog.Builder] configured with the specified parameters
*/
inline fun Context.createAlertDialogTemplate(
title: String? = null,
positivePair: Pair<String, AlertDialogCallback?>? = null,

View File

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

View File

@@ -3,15 +3,17 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
apply from: "$mppProjectWithSerializationAndCompose"
kotlin {
sourceSets {
androidMain {
dependencies {
api project(":micro_utils.android.smalltextfield")
api libs.jb.compose.icons
}
}
}

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.*
/**
* Performs a fling animation with an optional target adjustment.
* If [adjustTarget] is provided, animates to the adjusted target. Otherwise, performs a decay animation.
*
* @param initialVelocity The initial velocity of the fling
* @param animationSpec The decay animation specification
* @param adjustTarget Optional function to adjust the target value based on the calculated target
* @param block Optional block to be executed during the animation
* @return The result of the animation
*/
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,

View File

@@ -41,6 +41,18 @@ private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
/**
* A Compose number picker component that allows users to select a number by dragging, using arrow buttons,
* or manually entering a value.
*
* @param number The currently selected number
* @param modifier The modifier to be applied to the picker
* @param range Optional range of valid numbers. If specified, the picker will be limited to this range
* @param textStyle The text style for displaying numbers
* @param arrowsColor The color of the up/down arrow buttons
* @param allowUseManualInput Whether to allow manual keyboard input for the number
* @param onStateChanged Callback invoked when the selected number changes
*/
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun NumberPicker(

View File

@@ -22,6 +22,18 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.*
/**
* A Compose picker component that allows users to select an item from a list by dragging or using arrow buttons.
*
* @param T The type of items in the list
* @param current The currently selected item
* @param dataList The list of items to choose from
* @param modifier The modifier to be applied to the picker
* @param textStyle The text style for displaying items
* @param arrowsColor The color of the up/down arrow buttons
* @param dataToString A composable function to convert items to strings for display. Defaults to [Any.toString]
* @param onStateChanged Callback invoked when the selected item changes
*/
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun <T> SetPicker(

View File

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

View File

@@ -3,15 +3,16 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
apply from: "$mppProjectWithSerializationAndCompose"
kotlin {
sourceSets {
androidMain {
dependencies {
api libs.android.compose.material3
api libs.jb.compose.material3
}
}
}

View File

@@ -19,24 +19,35 @@ buildscript {
plugins {
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 {
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
}
// temporal crutch until legacy tests will be stabled or legacy target will be removed
if (it != rootProject.findProject("docs")) {
tasks.whenTaskAdded { task ->
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
task.enabled = false
}
}
it.tasks.withType(AbstractTestTask).configureEach {
it.failOnNoDiscoveredTests = false
}
}

View File

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

View File

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

View File

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

View File

@@ -1,298 +1,890 @@
/**
* Standard HTML/CSS color constants as [HEXAColor] extension properties.
* Provides convenient access to all standard web colors like red, blue, green, etc.
* All colors are defined with full opacity (alpha = 0xFF).
*/
package dev.inmo.micro_utils.colors
import dev.inmo.micro_utils.colors.common.HEXAColor
/**
* Alice Blue - A pale blue color (#F0F8FF).
*/
val HEXAColor.Companion.aliceblue
get() = HEXAColor(0xF0F8FFFFu)
/**
* Antique White - A pale beige color (#FAEBD7).
*/
val HEXAColor.Companion.antiquewhite
get() = HEXAColor(0xFAEBD7FFu)
/**
* Aqua - A bright cyan color (#00FFFF).
*/
val HEXAColor.Companion.aqua
get() = HEXAColor(0x00FFFFFFu)
/**
* Aquamarine - A medium blue-green color (#7FFFD4).
*/
val HEXAColor.Companion.aquamarine
get() = HEXAColor(0x7FFFD4FFu)
/**
* Azure - A pale cyan-blue color (#F0FFFF).
*/
val HEXAColor.Companion.azure
get() = HEXAColor(0xF0FFFFFFu)
/**
* Beige - A pale sandy tan color (#F5F5DC).
*/
val HEXAColor.Companion.beige
get() = HEXAColor(0xF5F5DCFFu)
/**
* Bisque - A pale orange color (#FFE4C4).
*/
val HEXAColor.Companion.bisque
get() = HEXAColor(0xFFE4C4FFu)
/**
* Black - Pure black color (#000000).
*/
val HEXAColor.Companion.black
get() = HEXAColor(0x000000FFu)
/**
* Blanched Almond - A pale peachy color (#FFEBCD).
*/
val HEXAColor.Companion.blanchedalmond
get() = HEXAColor(0xFFEBCDFFu)
/**
* Blue - Pure blue color (#0000FF).
*/
val HEXAColor.Companion.blue
get() = HEXAColor(0x0000FFFFu)
/**
* Blue Violet - A vivid purple-blue color (#8A2BE2).
*/
val HEXAColor.Companion.blueviolet
get() = HEXAColor(0x8A2BE2FFu)
/**
* Brown - A dark reddish-brown color (#A52A2A).
*/
val HEXAColor.Companion.brown
get() = HEXAColor(0xA52A2AFFu)
/**
* Burlywood - A sandy tan color (#DEB887).
*/
val HEXAColor.Companion.burlywood
get() = HEXAColor(0xDEB887FFu)
/**
* Cadet Blue - A grayish-blue color (#5F9EA0).
*/
val HEXAColor.Companion.cadetblue
get() = HEXAColor(0x5F9EA0FFu)
/**
* Chartreuse - A bright yellow-green color (#7FFF00).
*/
val HEXAColor.Companion.chartreuse
get() = HEXAColor(0x7FFF00FFu)
/**
* Chocolate - A medium brown color (#D2691E).
*/
val HEXAColor.Companion.chocolate
get() = HEXAColor(0xD2691EFFu)
/**
* Coral - A vibrant orange-pink color (#FF7F50).
*/
val HEXAColor.Companion.coral
get() = HEXAColor(0xFF7F50FFu)
/**
* Cornflower Blue - A medium blue color (#6495ED).
*/
val HEXAColor.Companion.cornflowerblue
get() = HEXAColor(0x6495EDFFu)
/**
* Cornsilk - A pale yellow color (#FFF8DC).
*/
val HEXAColor.Companion.cornsilk
get() = HEXAColor(0xFFF8DCFFu)
/**
* Crimson - A vivid red color (#DC143C).
*/
val HEXAColor.Companion.crimson
get() = HEXAColor(0xDC143CFFu)
/**
* Cyan - A bright cyan color (#00FFFF).
*/
val HEXAColor.Companion.cyan
get() = HEXAColor(0x00FFFFFFu)
/**
* Dark Blue - A dark blue color (#00008B).
*/
val HEXAColor.Companion.darkblue
get() = HEXAColor(0x00008BFFu)
/**
* Dark Cyan - A dark cyan color (#008B8B).
*/
val HEXAColor.Companion.darkcyan
get() = HEXAColor(0x008B8BFFu)
/**
* Dark Goldenrod - A dark golden yellow color (#B8860B).
*/
val HEXAColor.Companion.darkgoldenrod
get() = HEXAColor(0xB8860BFFu)
/**
* Dark Gray - A dark gray color (#A9A9A9).
*/
val HEXAColor.Companion.darkgray
get() = HEXAColor(0xA9A9A9FFu)
/**
* Dark Green - A dark green color (#006400).
*/
val HEXAColor.Companion.darkgreen
get() = HEXAColor(0x006400FFu)
/**
* Dark Grey - A dark gray color (#A9A9A9).
*/
val HEXAColor.Companion.darkgrey
get() = HEXAColor(0xA9A9A9FFu)
/**
* Dark Khaki - A brownish-tan color (#BDB76B).
*/
val HEXAColor.Companion.darkkhaki
get() = HEXAColor(0xBDB76BFFu)
/**
* Dark Magenta - A dark magenta/purple color (#8B008B).
*/
val HEXAColor.Companion.darkmagenta
get() = HEXAColor(0x8B008BFFu)
/**
* Dark Olive Green - A dark olive green color (#556B2F).
*/
val HEXAColor.Companion.darkolivegreen
get() = HEXAColor(0x556B2FFFu)
/**
* Dark Orange - A vivid dark orange color (#FF8C00).
*/
val HEXAColor.Companion.darkorange
get() = HEXAColor(0xFF8C00FFu)
/**
* Dark Orchid - A dark purple color (#9932CC).
*/
val HEXAColor.Companion.darkorchid
get() = HEXAColor(0x9932CCFFu)
/**
* Dark Red - A dark red color (#8B0000).
*/
val HEXAColor.Companion.darkred
get() = HEXAColor(0x8B0000FFu)
/**
* Dark Salmon - A muted salmon color (#E9967A).
*/
val HEXAColor.Companion.darksalmon
get() = HEXAColor(0xE9967AFFu)
/**
* Dark Sea Green - A muted sea green color (#8FBC8F).
*/
val HEXAColor.Companion.darkseagreen
get() = HEXAColor(0x8FBC8FFFu)
/**
* Dark Slate Blue - A dark grayish-blue color (#483D8B).
*/
val HEXAColor.Companion.darkslateblue
get() = HEXAColor(0x483D8BFFu)
/**
* Dark Slate Gray - A very dark grayish-cyan color (#2F4F4F).
*/
val HEXAColor.Companion.darkslategray
get() = HEXAColor(0x2F4F4FFFu)
/**
* Dark Slate Grey - A very dark grayish-cyan color (#2F4F4F).
*/
val HEXAColor.Companion.darkslategrey
get() = HEXAColor(0x2F4F4FFFu)
/**
* Dark Turquoise - A dark turquoise color (#00CED1).
*/
val HEXAColor.Companion.darkturquoise
get() = HEXAColor(0x00CED1FFu)
/**
* Dark Violet - A dark violet color (#9400D3).
*/
val HEXAColor.Companion.darkviolet
get() = HEXAColor(0x9400D3FFu)
/**
* Deep Pink - A vivid pink color (#FF1493).
*/
val HEXAColor.Companion.deeppink
get() = HEXAColor(0xFF1493FFu)
/**
* Deep Sky Blue - A bright sky blue color (#00BFFF).
*/
val HEXAColor.Companion.deepskyblue
get() = HEXAColor(0x00BFFFFFu)
/**
* Dim Gray - A dim gray color (#696969).
*/
val HEXAColor.Companion.dimgray
get() = HEXAColor(0x696969FFu)
/**
* Dim Grey - A dim gray color (#696969).
*/
val HEXAColor.Companion.dimgrey
get() = HEXAColor(0x696969FFu)
/**
* Dodger Blue - A bright blue color (#1E90FF).
*/
val HEXAColor.Companion.dodgerblue
get() = HEXAColor(0x1E90FFFFu)
/**
* Firebrick - A dark red brick color (#B22222).
*/
val HEXAColor.Companion.firebrick
get() = HEXAColor(0xB22222FFu)
/**
* Floral White - A very pale cream color (#FFFAF0).
*/
val HEXAColor.Companion.floralwhite
get() = HEXAColor(0xFFFAF0FFu)
/**
* Forest Green - A medium forest green color (#228B22).
*/
val HEXAColor.Companion.forestgreen
get() = HEXAColor(0x228B22FFu)
/**
* Fuchsia - A vivid magenta color (#FF00FF).
*/
val HEXAColor.Companion.fuchsia
get() = HEXAColor(0xFF00FFFFu)
/**
* Gainsboro - A light gray color (#DCDCDC).
*/
val HEXAColor.Companion.gainsboro
get() = HEXAColor(0xDCDCDCFFu)
/**
* Ghost White - A very pale blue-white color (#F8F8FF).
*/
val HEXAColor.Companion.ghostwhite
get() = HEXAColor(0xF8F8FFFFu)
/**
* Gold - A bright golden yellow color (#FFD700).
*/
val HEXAColor.Companion.gold
get() = HEXAColor(0xFFD700FFu)
/**
* Goldenrod - A golden yellow color (#DAA520).
*/
val HEXAColor.Companion.goldenrod
get() = HEXAColor(0xDAA520FFu)
/**
* Gray - A medium gray color (#808080).
*/
val HEXAColor.Companion.gray
get() = HEXAColor(0x808080FFu)
/**
* Green - A pure green color (#008000).
*/
val HEXAColor.Companion.green
get() = HEXAColor(0x008000FFu)
/**
* Green Yellow - A bright yellow-green color (#ADFF2F).
*/
val HEXAColor.Companion.greenyellow
get() = HEXAColor(0xADFF2FFFu)
/**
* Grey - A medium gray color (#808080).
*/
val HEXAColor.Companion.grey
get() = HEXAColor(0x808080FFu)
/**
* Honeydew - A very pale green color (#F0FFF0).
*/
val HEXAColor.Companion.honeydew
get() = HEXAColor(0xF0FFF0FFu)
/**
* Hot Pink - A vibrant pink color (#FF69B4).
*/
val HEXAColor.Companion.hotpink
get() = HEXAColor(0xFF69B4FFu)
/**
* Indian Red - A medium red color (#CD5C5C).
*/
val HEXAColor.Companion.indianred
get() = HEXAColor(0xCD5C5CFFu)
/**
* Indigo - A deep blue-violet color (#4B0082).
*/
val HEXAColor.Companion.indigo
get() = HEXAColor(0x4B0082FFu)
/**
* Ivory - A very pale cream color (#FFFFF0).
*/
val HEXAColor.Companion.ivory
get() = HEXAColor(0xFFFFF0FFu)
/**
* Khaki - A light tan color (#F0E68C).
*/
val HEXAColor.Companion.khaki
get() = HEXAColor(0xF0E68CFFu)
/**
* Lavender - A pale purple color (#E6E6FA).
*/
val HEXAColor.Companion.lavender
get() = HEXAColor(0xE6E6FAFFu)
/**
* Lavender Blush - A very pale pink color (#FFF0F5).
*/
val HEXAColor.Companion.lavenderblush
get() = HEXAColor(0xFFF0F5FFu)
/**
* Lawn Green - A bright chartreuse green color (#7CFC00).
*/
val HEXAColor.Companion.lawngreen
get() = HEXAColor(0x7CFC00FFu)
/**
* Lemon Chiffon - A very pale yellow color (#FFFACD).
*/
val HEXAColor.Companion.lemonchiffon
get() = HEXAColor(0xFFFACDFFu)
/**
* Light Blue - A light blue color (#ADD8E6).
*/
val HEXAColor.Companion.lightblue
get() = HEXAColor(0xADD8E6FFu)
/**
* Light Coral - A light coral pink color (#F08080).
*/
val HEXAColor.Companion.lightcoral
get() = HEXAColor(0xF08080FFu)
/**
* Light Cyan - A very pale cyan color (#E0FFFF).
*/
val HEXAColor.Companion.lightcyan
get() = HEXAColor(0xE0FFFFFFu)
/**
* Light Goldenrod Yellow - A pale yellow color (#FAFAD2).
*/
val HEXAColor.Companion.lightgoldenrodyellow
get() = HEXAColor(0xFAFAD2FFu)
/**
* Light Gray - A light gray color (#D3D3D3).
*/
val HEXAColor.Companion.lightgray
get() = HEXAColor(0xD3D3D3FFu)
/**
* Light Green - A light green color (#90EE90).
*/
val HEXAColor.Companion.lightgreen
get() = HEXAColor(0x90EE90FFu)
/**
* Light Grey - A light gray color (#D3D3D3).
*/
val HEXAColor.Companion.lightgrey
get() = HEXAColor(0xD3D3D3FFu)
/**
* Light Pink - A light pink color (#FFB6C1).
*/
val HEXAColor.Companion.lightpink
get() = HEXAColor(0xFFB6C1FFu)
/**
* Light Salmon - A light salmon color (#FFA07A).
*/
val HEXAColor.Companion.lightsalmon
get() = HEXAColor(0xFFA07AFFu)
/**
* Light Sea Green - A medium sea green color (#20B2AA).
*/
val HEXAColor.Companion.lightseagreen
get() = HEXAColor(0x20B2AAFFu)
/**
* Light Sky Blue - A light sky blue color (#87CEFA).
*/
val HEXAColor.Companion.lightskyblue
get() = HEXAColor(0x87CEFAFFu)
/**
* Light Slate Gray - A light slate gray color (#778899).
*/
val HEXAColor.Companion.lightslategray
get() = HEXAColor(0x778899FFu)
/**
* Light Slate Grey - A light slate gray color (#778899).
*/
val HEXAColor.Companion.lightslategrey
get() = HEXAColor(0x778899FFu)
/**
* Light Steel Blue - A light steel blue color (#B0C4DE).
*/
val HEXAColor.Companion.lightsteelblue
get() = HEXAColor(0xB0C4DEFFu)
/**
* Light Yellow - A very pale yellow color (#FFFFE0).
*/
val HEXAColor.Companion.lightyellow
get() = HEXAColor(0xFFFFE0FFu)
/**
* Lime - A bright lime green color (#00FF00).
*/
val HEXAColor.Companion.lime
get() = HEXAColor(0x00FF00FFu)
/**
* Lime Green - A lime green color (#32CD32).
*/
val HEXAColor.Companion.limegreen
get() = HEXAColor(0x32CD32FFu)
/**
* Linen - A pale beige color (#FAF0E6).
*/
val HEXAColor.Companion.linen
get() = HEXAColor(0xFAF0E6FFu)
/**
* Magenta - A bright magenta color (#FF00FF).
*/
val HEXAColor.Companion.magenta
get() = HEXAColor(0xFF00FFFFu)
/**
* Maroon - A dark reddish-brown color (#800000).
*/
val HEXAColor.Companion.maroon
get() = HEXAColor(0x800000FFu)
/**
* Medium Aquamarine - A medium aquamarine color (#66CDAA).
*/
val HEXAColor.Companion.mediumaquamarine
get() = HEXAColor(0x66CDAAFFu)
/**
* Medium Blue - A medium blue color (#0000CD).
*/
val HEXAColor.Companion.mediumblue
get() = HEXAColor(0x0000CDFFu)
/**
* Medium Orchid - A medium orchid purple color (#BA55D3).
*/
val HEXAColor.Companion.mediumorchid
get() = HEXAColor(0xBA55D3FFu)
/**
* Medium Purple - A medium purple color (#9370DB).
*/
val HEXAColor.Companion.mediumpurple
get() = HEXAColor(0x9370DBFFu)
/**
* Medium Sea Green - A medium sea green color (#3CB371).
*/
val HEXAColor.Companion.mediumseagreen
get() = HEXAColor(0x3CB371FFu)
/**
* Medium Slate Blue - A medium slate blue color (#7B68EE).
*/
val HEXAColor.Companion.mediumslateblue
get() = HEXAColor(0x7B68EEFFu)
/**
* Medium Spring Green - A medium spring green color (#00FA9A).
*/
val HEXAColor.Companion.mediumspringgreen
get() = HEXAColor(0x00FA9AFFu)
/**
* Medium Turquoise - A medium turquoise color (#48D1CC).
*/
val HEXAColor.Companion.mediumturquoise
get() = HEXAColor(0x48D1CCFFu)
/**
* Medium Violet Red - A medium violet-red color (#C71585).
*/
val HEXAColor.Companion.mediumvioletred
get() = HEXAColor(0xC71585FFu)
/**
* Midnight Blue - A very dark blue color (#191970).
*/
val HEXAColor.Companion.midnightblue
get() = HEXAColor(0x191970FFu)
/**
* Mint Cream - A very pale mint color (#F5FFFA).
*/
val HEXAColor.Companion.mintcream
get() = HEXAColor(0xF5FFFAFFu)
/**
* Misty Rose - A very pale pink color (#FFE4E1).
*/
val HEXAColor.Companion.mistyrose
get() = HEXAColor(0xFFE4E1FFu)
/**
* Moccasin - A pale peach color (#FFE4B5).
*/
val HEXAColor.Companion.moccasin
get() = HEXAColor(0xFFE4B5FFu)
/**
* Navajo White - A pale peach color (#FFDEAD).
*/
val HEXAColor.Companion.navajowhite
get() = HEXAColor(0xFFDEADFFu)
/**
* Navy - A very dark blue color (#000080).
*/
val HEXAColor.Companion.navy
get() = HEXAColor(0x000080FFu)
/**
* Old Lace - A very pale cream color (#FDF5E6).
*/
val HEXAColor.Companion.oldlace
get() = HEXAColor(0xFDF5E6FFu)
/**
* Olive - A dark yellowish-green color (#808000).
*/
val HEXAColor.Companion.olive
get() = HEXAColor(0x808000FFu)
/**
* Olive Drab - A dark olive green color (#6B8E23).
*/
val HEXAColor.Companion.olivedrab
get() = HEXAColor(0x6B8E23FFu)
/**
* Orange - A bright orange color (#FFA500).
*/
val HEXAColor.Companion.orange
get() = HEXAColor(0xFFA500FFu)
/**
* Orange Red - A bright red-orange color (#FF4500).
*/
val HEXAColor.Companion.orangered
get() = HEXAColor(0xFF4500FFu)
/**
* Orchid - A medium orchid purple color (#DA70D6).
*/
val HEXAColor.Companion.orchid
get() = HEXAColor(0xDA70D6FFu)
/**
* Pale Goldenrod - A pale goldenrod yellow color (#EEE8AA).
*/
val HEXAColor.Companion.palegoldenrod
get() = HEXAColor(0xEEE8AAFFu)
/**
* Pale Green - A pale green color (#98FB98).
*/
val HEXAColor.Companion.palegreen
get() = HEXAColor(0x98FB98FFu)
/**
* Pale Turquoise - A pale turquoise color (#AFEEEE).
*/
val HEXAColor.Companion.paleturquoise
get() = HEXAColor(0xAFEEEEFFu)
/**
* Pale Violet Red - A medium violet-red color (#DB7093).
*/
val HEXAColor.Companion.palevioletred
get() = HEXAColor(0xDB7093FFu)
/**
* Papaya Whip - A pale peach color (#FFEFD5).
*/
val HEXAColor.Companion.papayawhip
get() = HEXAColor(0xFFEFD5FFu)
/**
* Peach Puff - A light peach color (#FFDAB9).
*/
val HEXAColor.Companion.peachpuff
get() = HEXAColor(0xFFDAB9FFu)
/**
* Peru - A medium brown color (#CD853F).
*/
val HEXAColor.Companion.peru
get() = HEXAColor(0xCD853FFFu)
/**
* Pink - A light pink color (#FFC0CB).
*/
val HEXAColor.Companion.pink
get() = HEXAColor(0xFFC0CBFFu)
/**
* Plum - A medium purple color (#DDA0DD).
*/
val HEXAColor.Companion.plum
get() = HEXAColor(0xDDA0DDFFu)
/**
* Powder Blue - A light blue color (#B0E0E6).
*/
val HEXAColor.Companion.powderblue
get() = HEXAColor(0xB0E0E6FFu)
/**
* Purple - A pure purple color (#800080).
*/
val HEXAColor.Companion.purple
get() = HEXAColor(0x800080FFu)
/**
* Red - Pure red color (#FF0000).
*/
val HEXAColor.Companion.red
get() = HEXAColor(0xFF0000FFu)
/**
* Rosy Brown - A rosy brown color (#BC8F8F).
*/
val HEXAColor.Companion.rosybrown
get() = HEXAColor(0xBC8F8FFFu)
/**
* Royal Blue - A vibrant royal blue color (#4169E1).
*/
val HEXAColor.Companion.royalblue
get() = HEXAColor(0x4169E1FFu)
/**
* Saddle Brown - A dark brown color (#8B4513).
*/
val HEXAColor.Companion.saddlebrown
get() = HEXAColor(0x8B4513FFu)
/**
* Salmon - A light salmon pink color (#FA8072).
*/
val HEXAColor.Companion.salmon
get() = HEXAColor(0xFA8072FFu)
/**
* Sandy Brown - A sandy brown color (#F4A460).
*/
val HEXAColor.Companion.sandybrown
get() = HEXAColor(0xF4A460FFu)
/**
* Sea Green - A dark sea green color (#2E8B57).
*/
val HEXAColor.Companion.seagreen
get() = HEXAColor(0x2E8B57FFu)
/**
* Seashell - A very pale pink-orange color (#FFF5EE).
*/
val HEXAColor.Companion.seashell
get() = HEXAColor(0xFFF5EEFFu)
/**
* Sienna - A reddish-brown color (#A0522D).
*/
val HEXAColor.Companion.sienna
get() = HEXAColor(0xA0522DFFu)
/**
* Silver - A light gray-silver color (#C0C0C0).
*/
val HEXAColor.Companion.silver
get() = HEXAColor(0xC0C0C0FFu)
/**
* Sky Blue - A light sky blue color (#87CEEB).
*/
val HEXAColor.Companion.skyblue
get() = HEXAColor(0x87CEEBFFu)
/**
* Slate Blue - A medium slate blue color (#6A5ACD).
*/
val HEXAColor.Companion.slateblue
get() = HEXAColor(0x6A5ACDFFu)
/**
* Slate Gray - A slate gray color (#708090).
*/
val HEXAColor.Companion.slategray
get() = HEXAColor(0x708090FFu)
/**
* Slate Grey - A slate gray color (#708090).
*/
val HEXAColor.Companion.slategrey
get() = HEXAColor(0x708090FFu)
/**
* Snow - A very pale pinkish-white color (#FFFAFA).
*/
val HEXAColor.Companion.snow
get() = HEXAColor(0xFFFAFAFFu)
/**
* Spring Green - A bright spring green color (#00FF7F).
*/
val HEXAColor.Companion.springgreen
get() = HEXAColor(0x00FF7FFFu)
/**
* Steel Blue - A medium steel blue color (#4682B4).
*/
val HEXAColor.Companion.steelblue
get() = HEXAColor(0x4682B4FFu)
/**
* Tan - A light brown tan color (#D2B48C).
*/
val HEXAColor.Companion.tan
get() = HEXAColor(0xD2B48CFFu)
/**
* Teal - A dark cyan-blue color (#008080).
*/
val HEXAColor.Companion.teal
get() = HEXAColor(0x008080FFu)
/**
* Thistle - A light purple-pink color (#D8BFD8).
*/
val HEXAColor.Companion.thistle
get() = HEXAColor(0xD8BFD8FFu)
/**
* Tomato - A vibrant red-orange color (#FF6347).
*/
val HEXAColor.Companion.tomato
get() = HEXAColor(0xFF6347FFu)
/**
* Turquoise - A medium turquoise color (#40E0D0).
*/
val HEXAColor.Companion.turquoise
get() = HEXAColor(0x40E0D0FFu)
/**
* Violet - A violet color (#EE82EE).
*/
val HEXAColor.Companion.violet
get() = HEXAColor(0xEE82EEFFu)
/**
* Wheat - A light tan color (#F5DEB3).
*/
val HEXAColor.Companion.wheat
get() = HEXAColor(0xF5DEB3FFu)
/**
* White - Pure white color (#FFFFFF).
*/
val HEXAColor.Companion.white
get() = HEXAColor(0xFFFFFFFFu)
/**
* White Smoke - A very light gray color (#F5F5F5).
*/
val HEXAColor.Companion.whitesmoke
get() = HEXAColor(0xF5F5F5FFu)
/**
* Yellow - Pure yellow color (#FFFF00).
*/
val HEXAColor.Companion.yellow
get() = HEXAColor(0xFFFF00FFu)
/**
* Yellow Green - A medium yellow-green color (#9ACD32).
*/
val HEXAColor.Companion.yellowgreen
get() = HEXAColor(0x9ACD32FFu)

View File

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

View File

@@ -3,15 +3,17 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
api libs.kt.coroutines
}
}
}

View File

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

View File

@@ -0,0 +1,19 @@
package dev.inmo.micro_utils.common.compose
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.w3c.dom.Element
operator fun <T : Element> AttrBuilderContext<T>?.plus(
other: AttrBuilderContext<T>?
) = when (this) {
null -> other ?: {}
else -> when (other) {
null -> this ?: {}
else -> {
{
invoke(this)
other(this)
}
}
}
}

View File

@@ -0,0 +1,22 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import org.jetbrains.compose.web.attributes.AttrsScope
import org.jetbrains.compose.web.dom.ElementScope
import org.w3c.dom.Element
/**
* This function must be called in the context of your tag content. It works like default [AttrsScope.ref],
* but able to be used several times. Uses [DisposableEffect] under the hood
*/
@Composable
fun <T : Element> ElementScope<T>.ref(
block: DisposableEffectScope.(T) -> DisposableEffectResult
) {
DisposableEffect(0) {
block(scopeElement)
}
}

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.common.compose
import org.jetbrains.compose.web.dom.AttrBuilderContext
fun tagClasses(vararg classnames: String): AttrBuilderContext<*> = {
classes(*classnames)
}
fun tagId(id: String): AttrBuilderContext<*> = {
id(id)
}

View File

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

View File

@@ -0,0 +1,23 @@
package dev.inmo.micro_utils.common
/**
* Breaks this list into a list of consecutive pairs.
* Each element is paired with the next element in the list.
* For a list of size n, the result will contain n-1 pairs.
*
* Example: `[1, 2, 3, 4].breakAsPairs()` returns `[(1, 2), (2, 3), (3, 4)]`
*
* @param T The type of elements in the list
* @return A list of pairs where each pair consists of consecutive elements
*/
fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> {
val result = mutableListOf<Pair<T, T>>()
for (i in 0 until size - 1) {
val first = get(i)
val second = get(i + 1)
result.add(first to second)
}
return result
}

View File

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

View File

@@ -8,7 +8,7 @@ private inline fun <T> getObject(
additional: MutableList<T>,
iterator: Iterator<T>
): T? = when {
additional.isNotEmpty() -> additional.removeFirst()
additional.isNotEmpty() -> additional.removeAt(0)
iterator.hasNext() -> iterator.next()
else -> null
}

View File

@@ -1,5 +1,12 @@
package dev.inmo.micro_utils.common
/**
* Executes the given [block] and returns its result if this Boolean is true, otherwise returns null.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is true
* @return The result of [block] if true, null otherwise
*/
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
return if (this) {
block()
@@ -8,6 +15,13 @@ inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
}
}
/**
* Executes the given [block] and returns its result if this Boolean is false, otherwise returns null.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is false
* @return The result of [block] if false, null otherwise
*/
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
return if (this) {
null
@@ -16,16 +30,37 @@ inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
}
}
/**
* Executes the given [block] if this Boolean is true and returns this Boolean.
* Similar to [also], but only executes the block when the Boolean is true.
*
* @param block The function to execute if this Boolean is true
* @return This Boolean value
*/
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
letIfTrue(block)
return this
}
/**
* Executes the given [block] if this Boolean is false and returns this Boolean.
* Similar to [also], but only executes the block when the Boolean is false.
*
* @param block The function to execute if this Boolean is false
* @return This Boolean value
*/
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
letIfFalse(block)
return this
}
/**
* Alias for [letIfTrue]. Executes the given [block] and returns its result if this Boolean is true.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is true
* @return The result of [block] if true, null otherwise
*/
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
return if (this) {
block()
@@ -34,6 +69,13 @@ inline fun <T> Boolean.ifTrue(block: () -> T): T? {
}
}
/**
* Alias for [letIfFalse]. Executes the given [block] and returns its result if this Boolean is false.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is false
* @return The result of [block] if false, null otherwise
*/
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
return if (this) {
null

View File

@@ -6,19 +6,45 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* A function type that allocates and returns a [ByteArray].
*/
typealias ByteArrayAllocator = () -> ByteArray
/**
* A suspending function type that allocates and returns a [ByteArray].
*/
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
/**
* Converts this [ByteArray] to a [ByteArrayAllocator] that returns this array.
*/
val ByteArray.asAllocator: ByteArrayAllocator
get() = { this }
/**
* Converts this [ByteArray] to a [SuspendByteArrayAllocator] that returns this array.
*/
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
get() = { this }
/**
* Converts this [ByteArrayAllocator] to a [SuspendByteArrayAllocator].
*/
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
get() = { this() }
/**
* Converts this [SuspendByteArrayAllocator] to a [ByteArrayAllocator] by invoking it and
* wrapping the result in a non-suspending allocator.
*/
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
return invoke().asAllocator
}
/**
* Serializer for [ByteArrayAllocator]. Serializes the result of invoking the allocator.
*/
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
private val realSerializer = ByteArraySerializer()
override val descriptor: SerialDescriptor = realSerializer.descriptor

View File

@@ -1,3 +1,10 @@
package dev.inmo.micro_utils.common
/**
* Returns the first non-null element in this iterable.
*
* @param T The type of elements in the iterable (nullable)
* @return The first non-null element
* @throws NoSuchElementException if the iterable contains no non-null elements
*/
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!

View File

@@ -1,5 +1,19 @@
package dev.inmo.micro_utils.common
/**
* Joins elements of this iterable into a list with separators between elements.
* Each element is transformed using [transform], and separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separator function are skipped.
*
* @param I The type of elements in the input iterable
* @param R The type of elements in the result list
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return A list of transformed elements with separators
*/
inline fun <I, R> Iterable<I>.joinTo(
separatorFun: (I) -> R?,
prefix: R? = null,
@@ -25,6 +39,20 @@ inline fun <I, R> Iterable<I>.joinTo(
return result
}
/**
* Joins elements of this iterable into a list with a constant separator between elements.
* Each element is transformed using [transform].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separators are skipped.
*
* @param I The type of elements in the input iterable
* @param R The type of elements in the result list
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return A list of transformed elements with separators
*/
inline fun <I, R> Iterable<I>.joinTo(
separator: R? = null,
prefix: R? = null,
@@ -32,18 +60,55 @@ inline fun <I, R> Iterable<I>.joinTo(
transform: (I) -> R?
): List<R> = joinTo({ separator }, prefix, postfix, transform)
/**
* Joins elements of this iterable into a list with separators between elements.
* Separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from separator function are skipped.
*
* @param I The type of elements
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @return A list of elements with separators
*/
inline fun <I> Iterable<I>.joinTo(
separatorFun: (I) -> I?,
prefix: I? = null,
postfix: I? = null
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
/**
* Joins elements of this iterable into a list with a constant separator between elements.
* Optional [prefix] and [postfix] can be added to the result.
* Null separators are skipped.
*
* @param I The type of elements
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @return A list of elements with separators
*/
inline fun <I> Iterable<I>.joinTo(
separator: I? = null,
prefix: I? = null,
postfix: I? = null
): List<I> = joinTo<I>({ separator }, prefix, postfix)
/**
* Joins elements of this array into an array with separators between elements.
* Each element is transformed using [transform], and separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separator function are skipped.
*
* @param I The type of elements in the input array
* @param R The type of elements in the result array
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return An array of transformed elements with separators
*/
inline fun <I, reified R> Array<I>.joinTo(
separatorFun: (I) -> R?,
prefix: R? = null,
@@ -51,6 +116,20 @@ inline fun <I, reified R> Array<I>.joinTo(
transform: (I) -> R?
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
/**
* Joins elements of this array into an array with a constant separator between elements.
* Each element is transformed using [transform].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separators are skipped.
*
* @param I The type of elements in the input array
* @param R The type of elements in the result array
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return An array of transformed elements with separators
*/
inline fun <I, reified R> Array<I>.joinTo(
separator: R? = null,
prefix: R? = null,

View File

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

View File

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

View File

@@ -2,16 +2,43 @@ package dev.inmo.micro_utils.common
import kotlin.jvm.JvmName
/**
* A bidirectional mapper that can convert between two types [T1] and [T2].
*
* @param T1 The first type
* @param T2 The second type
*/
interface SimpleMapper<T1, T2> {
fun convertToT1(from: T2): T1
fun convertToT2(from: T1): T2
}
/**
* Converts [from] of type [T2] to type [T1] using this mapper.
*
* @param from The value to convert
* @return The converted value of type [T1]
*/
@JvmName("convertFromT2")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
/**
* Converts [from] of type [T1] to type [T2] using this mapper.
*
* @param from The value to convert
* @return The converted value of type [T2]
*/
@JvmName("convertFromT1")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
/**
* Implementation of [SimpleMapper] that uses lambda functions for conversion.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Function to convert from [T2] to [T1]
* @param t2 Function to convert from [T1] to [T2]
*/
class SimpleMapperImpl<T1, T2>(
private val t1: (T2) -> T1,
private val t2: (T1) -> T2,
@@ -21,22 +48,58 @@ class SimpleMapperImpl<T1, T2>(
override fun convertToT2(from: T1): T2 = t2.invoke(from)
}
/**
* Creates a [SimpleMapper] using the provided conversion functions.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Function to convert from [T2] to [T1]
* @param t2 Function to convert from [T1] to [T2]
* @return A new [SimpleMapperImpl] instance
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleMapper(
noinline t1: (T2) -> T1,
noinline t2: (T1) -> T2,
) = SimpleMapperImpl(t1, t2)
/**
* A bidirectional mapper that can convert between two types [T1] and [T2] using suspending functions.
*
* @param T1 The first type
* @param T2 The second type
*/
interface SimpleSuspendableMapper<T1, T2> {
suspend fun convertToT1(from: T2): T1
suspend fun convertToT2(from: T1): T2
}
/**
* Converts [from] of type [T2] to type [T1] using this suspending mapper.
*
* @param from The value to convert
* @return The converted value of type [T1]
*/
@JvmName("convertFromT2")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
/**
* Converts [from] of type [T1] to type [T2] using this suspending mapper.
*
* @param from The value to convert
* @return The converted value of type [T2]
*/
@JvmName("convertFromT1")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
/**
* Implementation of [SimpleSuspendableMapper] that uses suspending lambda functions for conversion.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Suspending function to convert from [T2] to [T1]
* @param t2 Suspending function to convert from [T1] to [T2]
*/
class SimpleSuspendableMapperImpl<T1, T2>(
private val t1: suspend (T2) -> T1,
private val t2: suspend (T1) -> T2,
@@ -46,6 +109,15 @@ class SimpleSuspendableMapperImpl<T1, T2>(
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
}
/**
* Creates a [SimpleSuspendableMapper] using the provided suspending conversion functions.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Suspending function to convert from [T2] to [T1]
* @param t2 Suspending function to convert from [T1] to [T2]
* @return A new [SimpleSuspendableMapperImpl] instance
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleSuspendableMapper(
noinline t1: suspend (T2) -> T1,

View File

@@ -0,0 +1,119 @@
package dev.inmo.micro_utils.common
/**
* Pads this sequence to the specified [size] using a custom [inserter] function.
* The [inserter] is repeatedly called until the sequence reaches the desired size.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param inserter A function that takes the current sequence and returns a new padded sequence
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> {
var result = this
while (result.count() < size) {
result = inserter(result)
}
return result
}
/**
* Pads this sequence at the end to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param padBlock A function that generates padding elements based on the current sequence size
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padEnd(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { it + padBlock(it.count()) }
/**
* Pads this sequence at the end to the specified [size] using the given element [o].
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param o The element to use for padding
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padEnd(size: Int, o: T) = padEnd(size) { o }
/**
* Pads this list to the specified [size] using a custom [inserter] function.
* The [inserter] is repeatedly called until the list reaches the desired size.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param inserter A function that takes the current list and returns a new padded list
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> {
var result = this
while (result.size < size) {
result = inserter(result)
}
return result
}
/**
* Pads this list at the end to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param padBlock A function that generates padding elements based on the current list size
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padEnd(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padEnd(size, padBlock).toList()
/**
* Pads this list at the end to the specified [size] using the given element [o].
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param o The element to use for padding
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padEnd(size: Int, o: T): List<T> = asSequence().padEnd(size, o).toList()
/**
* Pads this sequence at the start to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param padBlock A function that generates padding elements based on the current sequence size
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padStart(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { sequenceOf(padBlock(it.count())) + it }
/**
* Pads this sequence at the start to the specified [size] using the given element [o].
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param o The element to use for padding
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padStart(size: Int, o: T) = padStart(size) { o }
/**
* Pads this list at the start to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param padBlock A function that generates padding elements based on the current list size
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padStart(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padStart(size, padBlock).toList()
/**
* Pads this list at the start to the specified [size] using the given element [o].
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param o The element to use for padding
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padStart(size: Int, o: T): List<T> = asSequence().padStart(size, o).toList()

View File

@@ -0,0 +1,76 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
/**
* Contains [of1] as main value, where 100% of percentage is when of1 == 1
*
* @see invoke
* @see partOfTotal
* @see of100
*/
@Serializable
@JvmInline
value class Percentage private constructor(
/**
* Value of percentage. When it equals to 1, means 100%
*/
val of1: Double
) {
/**
* Same as [of1], but float (using [Double.toFloat])
*/
val of1Float
get() = of1.toFloat()
/**
* Represent this percentage as common percentage where 100% is 100%
*/
val of100
get() = of1 * 100
/**
* Same as [of100], but float (using [Double.toFloat])
*/
val of100Float
get() = of100.toFloat()
/**
* Same as [of100], but int (using [Double.toInt])
*/
val of100Int
get() = of100.toInt()
companion object {
val rangeOfValues = 0.0 .. 1.0
val START = Percentage(rangeOfValues.start)
val COMPLETED = Percentage(rangeOfValues.endInclusive)
operator fun invoke(of1: Double) = Percentage(of1.coerceIn(rangeOfValues))
operator fun invoke(part: Number, total: Number) = Percentage(
part.toDouble() / total.toDouble()
)
fun of1(of1: Double) = Percentage(of1 = of1)
fun of100(of100: Double) = Percentage(of1 = of100 / 100)
fun partOfTotal(part: Number, total: Number) = Percentage(part = part, total = total)
}
}
typealias Progress = Percentage
/**
* Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range
*/
fun Progress.ensureStrictOrNull(): Progress? = if (of1 in Percentage.rangeOfValues) this else null
/**
* Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range. Otherwise, will throw error
* [IllegalArgumentException] due to [require] failure
*/
fun Progress.ensureStrictOrThrow(): Progress {
require(of1 in Percentage.rangeOfValues) {
"For strict checks value of percentage must be in ${Percentage.rangeOfValues}, but actual value is $of1"
}
return this
}

View File

@@ -0,0 +1,80 @@
@file:Suppress(
"RemoveRedundantCallsOfConversionMethods",
"RedundantVisibilityModifier",
)
package dev.inmo.micro_utils.common
import kotlin.Byte
import kotlin.Double
import kotlin.Float
import kotlin.Int
import kotlin.Long
import kotlin.Short
import kotlin.Suppress
public operator fun Percentage.plus(other: Percentage): Percentage = Percentage(of1 + other.of1)
public operator fun Percentage.minus(other: Percentage): Percentage = Percentage(of1 - other.of1)
public operator fun Percentage.plus(i: Byte): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Byte): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Byte): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Byte): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Byte): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Short): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Short): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Short): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Short): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Short): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Int): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Int): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Int): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Int): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Int): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Long): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Long): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Long): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Long): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Long): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Float): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Float): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Float): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Float): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Float): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.plus(i: Double): Percentage = Percentage((of1 + i).toDouble())
public operator fun Percentage.minus(i: Double): Percentage = Percentage((of1 - i).toDouble())
public operator fun Percentage.times(i: Double): Percentage = Percentage((of1 * i).toDouble())
public operator fun Percentage.div(i: Double): Percentage = Percentage((of1 / i).toDouble())
public operator fun Percentage.rem(i: Double): Percentage = Percentage((of1 % i).toDouble())
public operator fun Percentage.compareTo(other: Percentage): Int = (of1.compareTo(other.of1))

View File

@@ -1,37 +0,0 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
@Serializable
@JvmInline
value class Progress private constructor(
val of1: Double
) {
val of1Float
get() = of1.toFloat()
val of100
get() = of1 * 100
val of100Float
get() = of100.toFloat()
val of100Int
get() = of100.toInt()
init {
require(of1 in rangeOfValues) {
"Progress main value should be in $rangeOfValues, but incoming value is $of1"
}
}
companion object {
val rangeOfValues = 0.0 .. 1.0
val START = Progress(rangeOfValues.start)
val COMPLETED = Progress(rangeOfValues.endInclusive)
operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues))
operator fun invoke(part: Number, total: Number) = Progress(
part.toDouble() / total.toDouble()
)
}
}

View File

@@ -1,80 +0,0 @@
@file:Suppress(
"RemoveRedundantCallsOfConversionMethods",
"RedundantVisibilityModifier",
)
package dev.inmo.micro_utils.common
import kotlin.Byte
import kotlin.Double
import kotlin.Float
import kotlin.Int
import kotlin.Long
import kotlin.Short
import kotlin.Suppress
public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1)
public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1)
public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble())
public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble())
public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble())
public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble())
public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble())
public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt()

View File

@@ -1,17 +1,39 @@
package dev.inmo.micro_utils.common
/**
* Computes the intersection of this range with [other]. Returns a pair representing
* the intersecting range, or null if the ranges don't overlap.
*
* @param T The type of comparable values in the range
* @param other The other range to intersect with
* @return A pair (start, end) representing the intersection, or null if no intersection exists
*/
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
start > other.endInclusive || other.start > endInclusive -> null
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
}
/**
* Computes the intersection of this [IntRange] with [other].
* Returns the intersecting range, or null if the ranges don't overlap.
*
* @param other The other range to intersect with
* @return An [IntRange] representing the intersection, or null if no intersection exists
*/
fun IntRange.intersect(
other: IntRange
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
it.first .. it.second
}
/**
* Computes the intersection of this [LongRange] with [other].
* Returns the intersecting range, or null if the ranges don't overlap.
*
* @param other The other range to intersect with
* @return A [LongRange] representing the intersection, or null if no intersection exists
*/
fun LongRange.intersect(
other: LongRange
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {

View File

@@ -0,0 +1,26 @@
package dev.inmo.micro_utils.common
/**
* Will try to execute [action] and, if any exception will happen, execution will be retried.
* This process will happen at most [count] times. There is no any limits on [count] value, but [action] will run at
* least once and [retryOnFailure] will return its result if it is successful
*/
inline fun <T> retryOnFailure(count: Int, action: () -> T): T {
var triesCount = 0
while (true) {
val result = runCatching {
action()
}.onFailure {
triesCount++
if (triesCount >= count) {
throw it
} else {
null
}
}
if (result.isSuccess) return result.getOrThrow()
}
error("Unreachable code: retry must throw latest exception if error happen or success value if not")
}

View File

@@ -0,0 +1,24 @@
package dev.inmo.micro_utils.common
/**
* Returns a new list with the element at index [i] replaced by applying [block] to it.
* All other elements remain unchanged.
*
* @param T The type of elements in the iterable
* @param i The index of the element to replace
* @param block A function that transforms the element at the given index
* @return A new list with the replaced element
*/
fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1)
/**
* Returns a new list with the first occurrence of element [t] replaced by applying [block] to it.
* All other elements remain unchanged.
*
* @param T The type of elements in the iterable
* @param t The element to replace
* @param block A function that transforms the found element
* @return A new list with the replaced element
*/
fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block)

View File

@@ -0,0 +1,29 @@
package dev.inmo.micro_utils.common
import kotlin.test.Test
import kotlin.test.assertEquals
class PercentageTests {
@Test
fun testCompareTo() {
val step = 0.01
var i = Percentage.START.of1
while (i <= Percentage.COMPLETED.of1) {
val percentageI = Percentage(i)
var j = Percentage.START.of1
while (j <= Percentage.COMPLETED.of1) {
val percentageJ = Percentage(j)
assertEquals(percentageI.of1.compareTo(percentageJ.of1), percentageI.compareTo(percentageJ))
assertEquals(percentageI.of1 > percentageJ.of1, percentageI > percentageJ)
assertEquals(percentageI.of1 < percentageJ.of1, percentageI < percentageJ)
j += step
}
i += step
}
}
}

View File

@@ -0,0 +1,21 @@
package dev.inmo.micro_utils.common
import kotlin.test.Test
import kotlin.test.assertEquals
class WithReplacedTest {
@Test
fun testReplaced() {
val data = 0 until 10
val testData = Int.MAX_VALUE
for (i in 0 until data.last) {
val withReplaced = data.withReplacedAt(i) {
testData
}
val dataAsMutableList = data.toMutableList()
dataAsMutableList[i] = testData
assertEquals(withReplaced, dataAsMutableList.toList())
}
}
}

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,29 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
import kotlin.js.json
external class ClipboardItem(data: dynamic)
inline fun Blob.convertToClipboardItem(): ClipboardItem {
val itemData: dynamic = json(this.type to this)
return ClipboardItem(itemData)
}
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
return runCatching {
val response = window.fetch(imageUrl).await()
val blob = response.blob().await()
val data = arrayOf(
Blob(
arrayOf(blob),
BlobPropertyBag("image/png")
).convertToClipboardItem()
).asDynamic()
window.navigator.clipboard.write(data)
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

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

View File

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

View File

@@ -11,6 +11,10 @@ actual typealias MPPFile = Path
*/
actual val MPPFile.filename: FileName
get() = FileName(toString())
actual val MPPFilePathSeparator: Char = Path.DIRECTORY_SEPARATOR.first()
/**
* @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,13 +4,14 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {
commonMain {
dependencies {
api libs.kt.coroutines
api libs.kslog
}
}
jsMain {

View File

@@ -3,9 +3,10 @@ plugins {
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin {
sourceSets {

View File

@@ -0,0 +1,66 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import org.jetbrains.compose.web.css.CSSRulesHolder
import org.jetbrains.compose.web.css.Style
import org.jetbrains.compose.web.css.StyleSheet
/**
* Aggregator of Compose CSS StyleSheet. Allowing to add [StyleSheet] in it and draw it in one place without requiring
* to add `Style(stylesheet)` on every compose function call
*/
object StyleSheetsAggregator {
private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable
fun draw() {
_stylesFlow.debounce(13L).collectAsState(emptySet()).value.forEach {
Style(it)
}
}
/**
* Adding [styleSheet] into the [Set] of included stylesheets. If you called [enableStyleSheetsAggregator],
* new styles will be enabled in the document
*/
fun addStyleSheet(styleSheet: CSSRulesHolder) {
_stylesFlow.value += styleSheet
}
/**
* Removed [styleSheet] into the [Set] of included stylesheets
*/
fun removeStyleSheet(styleSheet: CSSRulesHolder) {
_stylesFlow.value -= styleSheet
}
}
/**
* Drawing [StyleSheetsAggregator] in place. You may pass [Set] of [CSSRulesHolder]/[StyleSheet]s as preset of styles
*/
@Composable
fun enableStyleSheetsAggregator(
stylesPreset: Set<CSSRulesHolder> = emptySet(),
) {
remember {
stylesPreset.forEach {
StyleSheetsAggregator.addStyleSheet(it)
}
}
StyleSheetsAggregator.draw()
}
/**
* Will include [this] [CSSRulesHolder]/[StyleSheet] in the [StyleSheetsAggregator] using its
* [StyleSheetsAggregator.addStyleSheet]
*/
fun CSSRulesHolder.includeInStyleSheetsAggregator() {
StyleSheetsAggregator.addStyleSheet(this)
}

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
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 kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test
@TestOnly
fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0)
val flowState = MutableRedeliverStateFlow(0)
setContent {
Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString())

View File

@@ -68,9 +68,9 @@ class AccumulatorFlow<T>(
override suspend fun collectSafely(collector: FlowCollector<T>) {
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
steps.send(SubscribeAccumulatorFlowStep(channel))
val result = runCatchingSafely {
val result = runCatching {
for (data in channel) {
val emitResult = runCatchingSafely {
val emitResult = runCatching {
collector.emit(data)
}
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {

View File

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

View File

@@ -1,19 +1,34 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
/**
* Creates an actor-style channel that processes messages asynchronously based on markers.
* Messages with the same marker will be processed sequentially, while messages with different markers can be processed concurrently.
*
* @param T The type of messages to process
* @param channelCapacity The capacity of the underlying channel. Defaults to [Channel.UNLIMITED]
* @param markerFactory A factory function that produces a marker for each message. Messages with the same marker
* will be processed sequentially. Defaults to returning null, meaning all messages will be processed sequentially
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each message
* @return A [Channel] that accepts messages to be processed
*/
fun <T> CoroutineScope.actorAsync(
channelCapacity: Int = Channel.UNLIMITED,
markerFactory: suspend (T) -> Any? = { null },
logger: KSLog = KSLog,
block: suspend (T) -> Unit
): Channel<T> {
val channel = Channel<T>(channelCapacity)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, logger, block)
return channel
}
@Deprecated("Use standard actosAsync instead", ReplaceWith("actorAsync(channelCapacity, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.actorAsync"))
inline fun <T> CoroutineScope.safeActorAsync(
channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -3,5 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
/**
* Wraps this value in a completed [Deferred]. The resulting [Deferred] is immediately completed with this value.
* Useful for converting synchronous values to [Deferred] in contexts that expect deferred values.
*
* @param T The type of the value
* @return A [Deferred] that is already completed with this value
*/
val <T> T.asDeferred: Deferred<T>
get() = CompletableDeferred(this)

View File

@@ -3,20 +3,53 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
/**
* Convenience property to access [Dispatchers.Main] for UI operations.
*/
inline val UI
get() = Dispatchers.Main
/**
* Convenience property to access [Dispatchers.Default] for CPU-intensive operations.
*/
inline val Default
get() = Dispatchers.Default
/**
* Executes the given [block] in the specified coroutine [context] and returns its result.
* This is a convenience wrapper around [withContext].
*
* @param T The return type of the block
* @param context The [CoroutineContext] in which to execute the block
* @param block The suspending function to execute
* @return The result of executing the block
*/
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
context,
block
)
/**
* Executes the given [block] on the UI/Main dispatcher and returns its result.
* This is a convenience function for executing UI operations.
*
* @param T The return type of the block
* @param block The suspending function to execute on the UI thread
* @return The result of executing the block
*/
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
UI,
block
)
/**
* Executes the given [block] on the Default dispatcher and returns its result.
* This is a convenience function for executing CPU-intensive operations.
*
* @param T The return type of the block
* @param block The suspending function to execute on the Default dispatcher
* @return The result of executing the block
*/
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
Default,
block

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
/**
* Represents a deferred action that combines a [Deferred] value with a callback to be executed on that value.
*
* @param T The type of the deferred value
* @param O The type of the result after applying the callback
* @param deferred The deferred value to await
* @param callback The suspending function to apply to the deferred value
*/
class DeferredAction<T, O>(
val deferred: Deferred<T>,
val callback: suspend (T) -> O
@@ -9,6 +17,13 @@ class DeferredAction<T, O>(
suspend operator fun invoke() = callback(deferred.await())
}
/**
* A builder for creating multiple deferred computations that can be executed, with only the first completing
* one being used. This is useful for race conditions where you want the result of whichever computation finishes first.
*
* @param T The type of values produced by the deferred computations
* @param scope The [CoroutineScope] in which to create the deferred computations
*/
class DoWithFirstBuilder<T>(
private val scope: CoroutineScope
) {
@@ -22,8 +37,25 @@ class DoWithFirstBuilder<T>(
fun build() = deferreds.toList()
}
/**
* Creates a [DeferredAction] from this [Deferred] and a [callback] function.
*
* @param T The type of the deferred value
* @param O The type of the result after applying the callback
* @param callback The suspending function to apply to the deferred value
* @return A [DeferredAction] combining the deferred and callback
*/
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
/**
* Invokes the first [DeferredAction] whose deferred value completes, executing its callback and returning the result.
* Other deferred actions are cancelled if [cancelOnResult] is true.
*
* @param O The type of the result after applying callbacks
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred actions after the first completes. Defaults to true
* @return The result of invoking the first completed deferred action
*/
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
scope: CoroutineScope,
cancelOnResult: Boolean = true
@@ -33,18 +65,50 @@ suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
}
}
/**
* Invokes the first [DeferredAction] from the given [variants] whose deferred value completes,
* executing its callback and returning the result. Other deferred actions are cancelled if [cancelOnResult] is true.
*
* @param O The type of the result after applying callbacks
* @param scope The [CoroutineScope] in which to await the deferred values
* @param variants The deferred actions to race
* @param cancelOnResult If true, cancels all other deferred actions after the first completes. Defaults to true
* @return The result of invoking the first completed deferred action
*/
suspend fun <O> invokeFirstOf(
scope: CoroutineScope,
vararg variants: DeferredAction<*, O>,
cancelOnResult: Boolean = true
): O = variants.toList().invokeFirstOf(scope, cancelOnResult)
/**
* Awaits the first [Deferred] to complete and invokes the [callback] on its value.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst(
scope: CoroutineScope,
cancelOnResult: Boolean = true,
callback: suspend (T) -> O
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult)
/**
* Builds multiple deferred computations using [DoWithFirstBuilder] and invokes [callback] on the first one to complete.
* Other deferred computations are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param cancelOnResult If true, cancels all other computations after the first completes. Defaults to true
* @param block Builder DSL to define the deferred computations
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit,
@@ -54,6 +118,18 @@ suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult
).let { callback(it) }
/**
* Awaits the first [Deferred] from the given [variants] to complete and invokes the [callback] on its value.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param scope The [CoroutineScope] in which to await the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> invokeOnFirst(
scope: CoroutineScope,
vararg variants: Deferred<T>,
@@ -61,11 +137,29 @@ suspend fun <T, O> invokeOnFirst(
callback: suspend (T) -> O
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
/**
* Returns the value of the first [Deferred] from the given [variants] to complete.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> CoroutineScope.firstOf(
variants: Iterable<Deferred<T>>,
cancelOnResult: Boolean = true
) = variants.invokeOnFirst(this, cancelOnResult) { it }
/**
* Builds multiple deferred computations using [DoWithFirstBuilder] and returns the value of the first one to complete.
* Other deferred computations are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param cancelOnResult If true, cancels all other computations after the first completes. Defaults to true
* @param block Builder DSL to define the deferred computations
* @return The value of the first completed computation
*/
suspend fun <T> CoroutineScope.firstOf(
cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit
@@ -74,11 +168,29 @@ suspend fun <T> CoroutineScope.firstOf(
cancelOnResult
)
/**
* Returns the value of the first [Deferred] from the given [variants] to complete.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> CoroutineScope.firstOf(
vararg variants: Deferred<T>,
cancelOnResult: Boolean = true
) = firstOf(variants.toList(), cancelOnResult)
/**
* Returns the value of the first [Deferred] from this list to complete, using the given [scope].
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> List<Deferred<T>>.first(
scope: CoroutineScope,
cancelOnResult: Boolean = true

View File

@@ -2,4 +2,11 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.FlowCollector
/**
* Operator function that allows a [FlowCollector] to be invoked like a function to emit a value.
* This is a convenient syntax sugar for [FlowCollector.emit].
*
* @param T The type of values the collector can emit
* @param value The value to emit
*/
suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value)

View File

@@ -0,0 +1,68 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.jvm.JvmInline
import kotlin.time.Duration
@JvmInline
private value class DebouncedByData<T>(
val millisToData: Pair<Long, T>
)
/**
* Debounces a [Flow] with per-marker timeout control. Values with the same marker will be debounced independently.
* For each marker, only the last value within the timeout period will be emitted.
*
* @param T The type of values emitted by the flow
* @param timeout A function that determines the debounce timeout in milliseconds for each value
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
val jobs = mutableMapOf<Any?, Job>()
val mutex = Mutex()
subscribe(this) {
mutex.withLock {
val marker = markerFactory(it)
lateinit var job: Job
job = async {
delay(timeout(it))
mutex.withLock {
if (jobs[marker] === job) {
this@channelFlow.send(it)
jobs.remove(marker)
}
}
}
jobs[marker] ?.cancel()
jobs[marker] = job
}
}
}
/**
* Debounces a [Flow] with a fixed timeout in milliseconds and per-marker control.
* Values with the same marker will be debounced independently.
*
* @param T The type of values emitted by the flow
* @param timeout The debounce timeout in milliseconds
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory)
/**
* Debounces a [Flow] with a fixed timeout as [Duration] and per-marker control.
* Values with the same marker will be debounced independently.
*
* @param T The type of values emitted by the flow
* @param timeout The debounce timeout as a [Duration]
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory)

View File

@@ -3,4 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
/**
* Returns the first non-null element emitted by this [Flow].
* Suspends until a non-null element is found.
*
* @param T The type of elements in the flow
* @return The first non-null element
* @throws NoSuchElementException if the flow completes without emitting a non-null element
*/
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!

View File

@@ -4,6 +4,14 @@ import kotlinx.coroutines.flow.*
import kotlin.js.JsName
import kotlin.jvm.JvmName
/**
* Transforms each inner [Flow] element using the given [mapper] function and flattens the result into a single [Flow].
*
* @param T The type of elements in the inner flows
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of mapped and flattened elements
*/
inline fun <T, R> Flow<Flow<T>>.flatMap(
crossinline mapper: suspend (T) -> R
) = flow {
@@ -14,6 +22,14 @@ inline fun <T, R> Flow<Flow<T>>.flatMap(
}
}
/**
* Transforms each element from inner [Iterable]s using the given [mapper] function and flattens the result into a single [Flow].
*
* @param T The type of elements in the iterables
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of mapped and flattened elements
*/
@JsName("flatMapIterable")
@JvmName("flatMapIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMap(
@@ -22,18 +38,48 @@ inline fun <T, R> Flow<Iterable<T>>.flatMap(
it.asFlow()
}.flatMap(mapper)
/**
* Transforms each inner [Flow] element using the given [mapper] function, flattens the result,
* and filters out null values.
*
* @param T The type of elements in the inner flows
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of non-null mapped and flattened elements
*/
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull()
/**
* Transforms each element from inner [Iterable]s using the given [mapper] function, flattens the result,
* and filters out null values.
*
* @param T The type of elements in the iterables
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of non-null mapped and flattened elements
*/
@JsName("flatMapNotNullIterable")
@JvmName("flatMapNotNullIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull()
/**
* Flattens a [Flow] of [Flow]s into a single [Flow] by collecting all inner flows sequentially.
*
* @param T The type of elements in the inner flows
* @return A [Flow] containing all elements from all inner flows
*/
fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
/**
* Flattens a [Flow] of [Iterable]s into a single [Flow] by emitting all elements from each iterable.
*
* @param T The type of elements in the iterables
* @return A [Flow] containing all elements from all iterables
*/
@JsName("flattenIterable")
@JvmName("flattenIterable")
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }

View File

@@ -2,5 +2,18 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.*
/**
* Filters out null values from this [Flow], returning only non-null elements.
*
* @param T The type of elements in the flow (nullable)
* @return A [Flow] containing only non-null elements
*/
fun <T> Flow<T>.takeNotNull() = mapNotNull { it }
/**
* Alias for [takeNotNull]. Filters out null values from this [Flow], returning only non-null elements.
*
* @param T The type of elements in the flow (nullable)
* @return A [Flow] containing only non-null elements
*/
fun <T> Flow<T>.filterNotNull() = takeNotNull()

View File

@@ -2,6 +2,7 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
@@ -16,6 +17,45 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
* Use [subscribe], but all [block]s will be called inside of [safely] function.
* Use [onException] to set up your reaction for [Throwable]s
*/
inline fun <T> Flow<T>.subscribeLogging(
scope: CoroutineScope,
noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
noinline block: suspend (T) -> Unit
) = subscribe(scope) {
it.runCatchingLogging(
errorMessageBuilder,
logger
) {
block(it)
}.getOrThrow()
}
/**
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
*/
inline fun <T> Flow<T>.subscribeLoggingDropExceptions(
scope: CoroutineScope,
noinline errorMessageBuilder: T.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
noinline block: suspend (T) -> Unit
) = subscribe(scope) {
it.runCatchingLogging(
errorMessageBuilder,
logger
) {
block(it)
}
}
/**
* Use [subscribe], but all [block]s will be called inside of [safely] function.
* Use [onException] to set up your reaction for [Throwable]s
*/
@Deprecated(
"Will be removed soon due to replacement by subscribeLogging",
ReplaceWith("this.subscribeLogging(scope = scope, block = block)")
)
inline fun <T> Flow<T>.subscribeSafely(
scope: CoroutineScope,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
@@ -29,6 +69,10 @@ inline fun <T> Flow<T>.subscribeSafely(
/**
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
*/
@Deprecated(
"Will be removed soon due to replacement by subscribeLoggingDropExceptions",
ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)")
)
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
scope: CoroutineScope,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
@@ -42,6 +86,10 @@ inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
/**
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
*/
@Deprecated(
"Will be removed soon due to replacement by subscribeLoggingDropExceptions",
ReplaceWith("this.subscribeLoggingDropExceptions(scope = scope, block = block)")
)
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
scope: CoroutineScope,
noinline block: suspend (T) -> Unit

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
@@ -8,6 +9,7 @@ import kotlinx.coroutines.sync.withLock
private class SubscribeAsyncReceiver<T>(
val scope: CoroutineScope,
val logger: KSLog,
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
) {
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
@@ -15,7 +17,7 @@ private class SubscribeAsyncReceiver<T>(
get() = dataChannel
init {
scope.launchSafelyWithoutExceptions {
scope.launchLoggingDropExceptions(logger = logger) {
for (data in dataChannel) {
output(data)
}
@@ -33,13 +35,16 @@ private data class AsyncSubscriptionCommandData<T, M>(
val scope: CoroutineScope,
val markerFactory: suspend (T) -> M,
val block: suspend (T) -> Unit,
val logger: KSLog,
val onEmpty: suspend (M) -> Unit
) : AsyncSubscriptionCommand<T, M> {
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
val marker = markerFactory(data)
markersMap.getOrPut(marker) {
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) {
safelyWithoutExceptions { block(it) }
SubscribeAsyncReceiver(scope.LinkedSupervisorScope(), logger) {
runCatchingLogging(logger = logger) {
block(it)
}
if (isEmpty()) {
onEmpty(marker)
}
@@ -60,9 +65,24 @@ private data class AsyncSubscriptionCommandClearReceiver<T, M>(
}
}
/**
* Subscribes to a [Flow] with asynchronous processing based on markers.
* Each value from the flow will be processed by the [block] function. Values with the same marker
* will be processed sequentially in the same coroutine scope, while values with different markers
* can be processed concurrently in separate coroutine scopes.
*
* @param T The type of values emitted by the flow
* @param M The type of markers used to group values
* @param scope The [CoroutineScope] in which to subscribe to the flow
* @param markerFactory A factory function that produces a marker for each emitted value
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each emitted value
* @return A [Job] representing the subscription that can be cancelled
*/
fun <T, M> Flow<T>.subscribeAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
): Job {
val subscope = scope.LinkedSupervisorScope()
@@ -71,8 +91,14 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap)
}
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
val job = subscribeLoggingDropExceptions(subscope, logger = logger) { data ->
val dataCommand = AsyncSubscriptionCommandData(
data = data,
scope = subscope,
markerFactory = markerFactory,
block = block,
logger = logger
) { marker ->
actor.send(
AsyncSubscriptionCommandClearReceiver(marker)
)
@@ -85,17 +111,20 @@ fun <T, M> Flow<T>.subscribeAsync(
return job
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
) = subscribeAsync(scope, markerFactory, logger) {
safely(onException) {
block(it)
}
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
@@ -107,11 +136,36 @@ fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
}
}
/**
* Subscribes to a [Flow] with asynchronous processing based on markers, automatically logging and dropping exceptions.
* Each value from the flow will be processed by the [block] function. Values with the same marker
* will be processed sequentially, while values with different markers can be processed concurrently.
* Any exceptions thrown during processing will be logged and dropped without affecting other messages.
*
* @param T The type of values emitted by the flow
* @param M The type of markers used to group values
* @param scope The [CoroutineScope] in which to subscribe to the flow
* @param markerFactory A factory function that produces a marker for each emitted value
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each emitted value
* @return A [Job] representing the subscription that can be cancelled
*/
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(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
) = subscribeAsync(scope, markerFactory, logger) {
safelyWithoutExceptions({ /* do nothing */}) {
block(it)
}

View File

@@ -5,4 +5,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge
/**
* Merges two flows into a single flow. Values from both flows will be emitted as they become available.
* This is a convenient operator syntax for [merge].
*
* @param T The type of elements in the flows
* @param other The flow to merge with this flow
* @return A [Flow] that emits values from both flows
*/
inline operator fun <T> Flow<T>.plus(other: Flow<T>) = merge(this, other)

View File

@@ -15,6 +15,10 @@ import kotlin.coroutines.coroutineContext
*
* @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution
*/
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }")
)
suspend inline fun <T> runCatchingSafely(
onException: ExceptionHandler<T>,
block: suspend () -> T
@@ -29,6 +33,10 @@ suspend inline fun <T> runCatchingSafely(
}
}
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }")
)
suspend inline fun <T, R> R.runCatchingSafely(
onException: ExceptionHandler<T>,
block: suspend R.() -> T
@@ -39,10 +47,18 @@ suspend inline fun <T, R> R.runCatchingSafely(
/**
* Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter
*/
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }")
)
suspend inline fun <T> runCatchingSafely(
block: suspend () -> T
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }")
)
suspend inline fun <T, R> R.runCatchingSafely(
block: suspend R.() -> T
): Result<T> = runCatchingSafely<T> {
@@ -73,6 +89,9 @@ suspend fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExce
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
*/
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
)
suspend fun <T> safelyWithContextExceptionHandler(
contextExceptionHandler: ExceptionHandler<Unit>,
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
@@ -94,6 +113,10 @@ suspend fun <T> safelyWithContextExceptionHandler(
*
* @see runCatchingSafely
*/
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrThrow()")
)
suspend inline fun <T> safely(
onException: ExceptionHandler<T>,
block: suspend () -> T
@@ -104,9 +127,17 @@ suspend inline fun <T> safely(
*
* @see runCatchingSafely
*/
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()")
)
suspend inline fun <T> safely(
block: suspend () -> T
): T = safely(defaultSafelyExceptionHandler, block)
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { defaultSafelyExceptionHandler(it) }.getOrThrow()")
)
suspend inline fun <T, R> R.safely(
block: suspend R.() -> T
): T = safely<T> { block() }
@@ -137,11 +168,19 @@ val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
* result from exception (instead of throwing it, by default always returns null)
*/
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()")
)
suspend fun <T> safelyWithoutExceptions(
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
block: suspend () -> T
): T? = runCatchingSafely(onException, block).getOrNull()
@Deprecated(
"This function become redundant since coroutines correctly handling throwing exceptions",
replaceWith = ReplaceWith("runCatching(block).replaceIfFailure { onException(it) }.getOrNull()")
)
suspend fun <T> runCatchingSafelyWithoutExceptions(
onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler,
block: suspend () -> T

View File

@@ -0,0 +1,101 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* Launches a new coroutine with automatic exception logging. If an exception occurs, it will be logged
* using the provided [logger] and then rethrown.
*
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute in the new coroutine
* @return A [Job] representing the launched coroutine
*/
fun CoroutineScope.launchLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) = launch(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
}.getOrThrow()
}
/**
* Launches a new coroutine with automatic exception logging and dropping. If an exception occurs, it will be logged
* using the provided [logger] and then dropped (not rethrown), allowing the coroutine to complete normally.
*
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute in the new coroutine
* @return A [Job] representing the launched coroutine
*/
fun CoroutineScope.launchLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) = launch(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
} // just dropping exception
}
/**
* Creates a new async coroutine with automatic exception logging. If an exception occurs, it will be logged
* using the provided [logger] and then rethrown when the [Deferred] is awaited.
*
* @param T The return type of the async computation
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute that returns a value of type [T]
* @return A [Deferred] representing the async computation
*/
fun <T> CoroutineScope.asyncLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
) = async(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
}.getOrThrow()
}
/**
* Creates a new async coroutine with automatic exception logging and dropping. If an exception occurs, it will be logged
* using the provided [logger] and wrapped in a [Result], which can be checked when the [Deferred] is awaited.
*
* @param T The return type of the async computation
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute that returns a value of type [T]
* @return A [Deferred] containing a [Result] representing the async computation
*/
fun <T> CoroutineScope.asyncLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
) = async(context, start) {
runCatching { block() }.onFailure {
logger.e(it) { errorMessageBuilder(it) }
}
}

View File

@@ -4,6 +4,10 @@ import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@Deprecated(
"This method will be removed soon. Use launchLogging instead",
ReplaceWith("this.launchLogging(context = context, start = start, block = block)")
)
fun CoroutineScope.launchSafely(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
@@ -15,6 +19,10 @@ fun CoroutineScope.launchSafely(
}
}
@Deprecated(
"This method will be removed soon. Use launchLoggingDropExceptions instead",
ReplaceWith("this.launchLoggingDropExceptions(context = context, start = start, block = block)")
)
fun CoroutineScope.launchSafelyWithoutExceptions(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
@@ -26,6 +34,10 @@ fun CoroutineScope.launchSafelyWithoutExceptions(
}
}
@Deprecated(
"This method will be removed soon. Use asyncLogging instead",
ReplaceWith("this.asyncLogging(context = context, start = start, block = block)")
)
fun <T> CoroutineScope.asyncSafely(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
@@ -37,6 +49,10 @@ fun <T> CoroutineScope.asyncSafely(
}
}
@Deprecated(
"This method will be removed soon. Use asyncLoggingDropExceptions instead",
ReplaceWith("this.asyncLoggingDropExceptions(context = context, start = start, block = block)")
)
fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber
*/
open class SpecialMutableStateFlow<T>(
open class MutableRedeliverStateFlow<T>(
initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class)
@@ -68,3 +65,6 @@ open class SpecialMutableStateFlow<T>(
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,13 @@
package dev.inmo.micro_utils.coroutines
/**
* Replaces a failed [Result] with a new value computed from the exception.
* If this [Result] is successful, it is returned as-is. If it represents a failure,
* the [onException] handler is called with the exception to compute a replacement value,
* which is then wrapped in a new [Result].
*
* @param T The type of the successful value
* @param onException A function that computes a replacement value from the caught exception
* @return The original [Result] if successful, or a new [Result] containing the replacement value
*/
inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } }

View File

@@ -0,0 +1,28 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e
import kotlin.coroutines.cancellation.CancellationException
/**
* Executes the given [block] within a `runCatching` context and logs any exceptions that occur, excluding
* `CancellationException` which is rethrown. This method simplifies error handling by automatically logging
* the errors using the provided [logger].
*
* @param T The result type of the [block].
* @param R The receiver type on which this function operates.
* @param errorMessageBuilder A lambda to build the error log message. By default, it returns a generic error message.
* @param logger The logging instance used for logging errors. Defaults to [KSLog].
* @param block The code block to execute within the `runCatching` context.
* @return A [Result] representing the outcome of executing the [block].
*/
inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
block: R.() -> T
) = runCatching(block).onFailure {
when (it) {
is CancellationException -> throw it
else -> logger.e(it) { errorMessageBuilder(it) }
}
}

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
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/
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()
private val internalChangesMutex = Mutex()

View File

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

View File

@@ -0,0 +1,40 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
/**
* Executes the given [block] and unlocks all provided [lockers] for writing if the block succeeds.
* If the block throws an exception, the lockers will remain locked.
*
* @param lockers Variable number of [SmartRWLocker] instances to unlock on successful execution
* @param block The suspending function to execute
* @return A [Result] containing [Unit] on success or the exception that occurred
*/
suspend inline fun alsoWithUnlockingOnSuccess(
vararg lockers: SmartRWLocker,
block: suspend () -> Unit
): Result<Unit> {
return runCatching {
block()
}.onSuccess {
lockers.forEach { it.unlockWrite() }
}
}
/**
* Asynchronously executes the given [block] and unlocks all provided [lockers] for writing if the block succeeds.
* This function launches a new coroutine in the given [scope] and automatically logs and drops any exceptions.
*
* @param scope The [CoroutineScope] in which to launch the coroutine
* @param lockers Variable number of [SmartRWLocker] instances to unlock on successful execution
* @param block The suspending function to execute
* @return A [Job] representing the launched coroutine
*/
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
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -24,6 +23,7 @@ import kotlin.contracts.contract
* [Mutable] creator
*/
sealed interface SmartSemaphore {
val maxPermits: 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]
*/
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
class Immutable(override val permitsStateFlow: StateFlow<Int>, override val maxPermits: Int) : SmartSemaphore
/**
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
@@ -44,15 +44,16 @@ sealed interface SmartSemaphore {
*
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
*/
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits)
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
override val maxPermits: Int = permits
private val _freePermitsStateFlow = MutableRedeliverStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false)
fun immutable() = Immutable(permitsStateFlow)
fun immutable() = Immutable(permitsStateFlow, maxPermits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.maxPermits)
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
@@ -71,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits
}
if (shouldContinue) {
waitRelease()
waitRelease(checkedPermits - acquiredPermits)
}
} while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) {
@@ -126,10 +127,10 @@ sealed interface SmartSemaphore {
*/
suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value < this.permits) {
return if (_freePermitsStateFlow.value < this.maxPermits) {
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < this.permits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
if (_freePermitsStateFlow.value < this.maxPermits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.maxPermits)
true
} else {
false
@@ -166,3 +167,4 @@ suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, acti
* the fact that some other parties may lock it again
*/
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }
suspend fun SmartSemaphore.waitReleaseAll() = permitsStateFlow.first { it == maxPermits }

View File

@@ -3,19 +3,49 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
/**
* Creates a [SupervisorJob] linked to this [CoroutineContext]'s job. The new supervisor job will be a child
* of the current job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A [CoroutineContext] containing the new supervisor job
*/
fun CoroutineContext.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
/**
* Creates a [SupervisorJob] linked to this [CoroutineScope]'s job. The new supervisor job will be a child
* of the current scope's job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A [CoroutineContext] containing the new supervisor job
*/
fun CoroutineScope.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorJob(additionalContext)
/**
* Creates a new [CoroutineScope] with a [SupervisorJob] linked to this [CoroutineContext]'s job.
* The new scope's supervisor job will be a child of the current job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A new [CoroutineScope] with a linked supervisor job
*/
fun CoroutineContext.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null
) = CoroutineScope(
this + LinkedSupervisorJob(additionalContext)
)
/**
* Creates a new [CoroutineScope] with a [SupervisorJob] linked to this [CoroutineScope]'s job.
* The new scope's supervisor job will be a child of the current scope's job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A new [CoroutineScope] with a linked supervisor job
*/
fun CoroutineScope.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorScope(additionalContext)

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
/**
* Ensures that the current coroutine context is still active and throws a [kotlinx.coroutines.CancellationException]
* if the coroutine has been canceled.
*
* This function provides a convenient way to check the active status of a coroutine, which is useful
* to identify cancellation points in long-running or suspendable operations.
*
* @throws kotlinx.coroutines.CancellationException if the coroutine context is no longer active.
*/
suspend fun suspendPoint() = currentCoroutineContext().ensureActive()

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
import dev.inmo.micro_utils.coroutines.debouncedBy
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class DebouncedByTests {
@Test
fun testThatParallelDebouncingWorksCorrectly() = runTest {
val dataToMarkerFactories = listOf(
1 to 0,
2 to 1,
3 to 2,
4 to 0,
5 to 1,
6 to 2,
7 to 0,
8 to 1,
9 to 2,
)
val collected = mutableListOf<Int>()
dataToMarkerFactories.asFlow().debouncedBy(10L) {
it.second
}.collect {
when (it.second) {
0 -> assertEquals(7, it.first)
1 -> assertEquals(8, it.first)
2 -> assertEquals(9, it.first)
else -> error("wtf")
}
collected.add(it.first)
}
val expectedList = listOf(7, 8, 9)
assertEquals(expectedList, collected)
assertTrue { collected.containsAll(expectedList) }
assertTrue { expectedList.containsAll(collected) }
}
}

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