mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
513 Commits
Author | SHA1 | Date | |
---|---|---|---|
19857930a4 | |||
d0dbe3ed2f | |||
8b7e78b63a | |||
92a4ecb523 | |||
6a5ad4d728 | |||
be4aa8daac | |||
b5eac37782 | |||
b1ad3c5a39 | |||
ba16bad029 | |||
ca8ae4cd72 | |||
53d35d74b3 | |||
49c139e235 | |||
caf9c821f3 | |||
ca4c6db96f | |||
6b2298c752 | |||
a1bf43def9 | |||
15e9254e00 | |||
afe5a72c6f | |||
750a8b9ecf | |||
27fc3f93e0 | |||
8166d4b99b | |||
b61d2ae2eb | |||
4790fe0aea | |||
bc37b11cee | |||
223fed910f | |||
b85ab7b061 | |||
888dc299c9 | |||
e113dc28ed | |||
31e55d2307 | |||
e90645f248 | |||
4bb7ba2571 | |||
8d31c25bf8 | |||
c7ee1c28b2 | |||
99b09c8b28 | |||
a328c4425a | |||
c0f61ca896 | |||
86e70c0961 | |||
d87a3a039f | |||
6279a2c40a | |||
f377ebea88 | |||
7373fef964 | |||
41ef86dbda | |||
7bc2b2336d | |||
0a615e6d78 | |||
8f928e16e1 | |||
72bc8da1e7 | |||
51e349a5db | |||
3cbb19ba2c | |||
ae3a5bf45d | |||
9c667f4b78 | |||
21195e1bcb | |||
03117ac565 | |||
d13fbdf176 | |||
7cecc0e0b6 | |||
203e781f5d | |||
3eb6cd77cd | |||
51855b2405 | |||
00cc26d874 | |||
02520636ad | |||
76813fae8e | |||
4693483c2b | |||
6dbd12df59 | |||
9e84dc5031 | |||
8f790360bc | |||
808375cea6 | |||
d9c15db9de | |||
cf2be8ed43 | |||
97339c9b1d | |||
eb35903de9 | |||
43e88e00da | |||
1e51f9d77b | |||
dad738357c | |||
cb828ab3f2 | |||
aefaf4a8cc | |||
b29f37a251 | |||
88854020ac | |||
14ebe01fc6 | |||
771aed0f0f | |||
16c57fcd6a | |||
75397a7ccb | |||
b05404f828 | |||
edea942874 | |||
8a8f568b9a | |||
4dc8d30c52 | |||
cb4e08e823 | |||
c443bf4fa0 | |||
a6982de822 | |||
4f1a663e75 | |||
dab262d626 | |||
87a3f61ca6 | |||
506e937a68 | |||
5a037c76dd | |||
313f622f7e | |||
6cba1fe1a2 | |||
fd2d0e80b7 | |||
96ab2e8aca | |||
0202988cae | |||
d619d59947 | |||
85b3e48d18 | |||
7a9b7d98a1 | |||
b212acfcaf | |||
3a45e5dc70 | |||
73190518d5 | |||
03f78180dc | |||
1c0b8cf842 | |||
a1624ea2a9 | |||
23a050cf1e | |||
916f2f96f4 | |||
00cc214754 | |||
b2e38f72b9 | |||
e7107d238d | |||
ed9ebdbd1a | |||
e80676d3d2 | |||
02d02fa8f2 | |||
bd783fb74f | |||
50386adf70 | |||
f4ee6c2890 | |||
d45aef9fe5 | |||
a56cd3dddd | |||
419e7070ee | |||
612cf40b5f | |||
8b39882e83 | |||
e639ae172b | |||
d0446850ae | |||
c48465b90b | |||
f419fd03d2 | |||
494812a660 | |||
eb78f21eec | |||
4bda70268b | |||
f037ce4371 | |||
3d2196e35d | |||
a74f061b02 | |||
11ade14676 | |||
eb562d8784 | |||
1ee5b4bfd4 | |||
d97892080b | |||
6f37125724 | |||
ed1baaade7 | |||
bb9669f8fd | |||
bdac715d48 | |||
acf4971298 | |||
249bc83a8c | |||
0fbb92f03f | |||
ca27cb3f82 | |||
3a5771a0cc | |||
527a2a91ac | |||
6763e5c4c6 | |||
06918d8310 | |||
89ccaa1b57 | |||
5d0bdb9bcf | |||
31fdcf74a5 | |||
afca09cc1d | |||
531d89d9db | |||
6bbbea0bc3 | |||
e337cd98c8 | |||
bcbab3b380 | |||
fb63de7568 | |||
aa45a4ab13 | |||
2af7e2f681 | |||
34fd9edce0 | |||
2a4cb8c5f9 | |||
50ea40bc3a | |||
a77654052d | |||
88aafce552 | |||
4e95d6bfff | |||
38d0e34fb5 | |||
8fbc6b9041 | |||
e8219d6cf4 | |||
6c20fc4ca6 | |||
85cd975492 | |||
1171a717fe | |||
bbe5320312 | |||
00acb9fddd | |||
de3d14dc41 | |||
67ff9cc9b3 | |||
af132103a0 | |||
3b1124a804 | |||
f226c2dfd6 | |||
69d6e63846 | |||
02c3d397ad | |||
67a1050646 | |||
8cd0775a6c | |||
162294d6c6 | |||
c4dd19dd00 | |||
d2314422f1 | |||
6fedd6f859 | |||
e52b59665f | |||
cda9d09689 | |||
c9237b3f00 | |||
18bba66c4a | |||
63418c4a8a | |||
2e66c6f4e3 | |||
e9c5df4c13 | |||
bc7789ad2c | |||
e3da761249 | |||
4082f65afa | |||
5d1cab075d | |||
bcf67f7e59 | |||
7d3b1f8e75 | |||
119a0588cc | |||
fab789d9c0 | |||
ceba81c08f | |||
a061af0558 | |||
c7a53846ad | |||
a683cccf0c | |||
50d41e35c1 | |||
aa0e831cea | |||
44e26ccb4f | |||
2a783f6e2b | |||
6058d6a724 | |||
2e9c7eb5fa | |||
e75465ad10 | |||
de01ad54e9 | |||
eeea7ddbe3 | |||
e0b18bec05 | |||
410e89bba9 | |||
9ef19dc42b | |||
0337d1b82d | |||
f5bd4c5ccb | |||
630f9bc0d4 | |||
18b4ffece1 | |||
f64e1effa3 | |||
847fcbb488 | |||
88002ec8e7 | |||
7f8db6a29d | |||
b183b82443 | |||
5dad27de72 | |||
6b66084d0e | |||
50b56a7c39 | |||
7ab7d14471 | |||
bdcc179b7b | |||
55ffd4b46f | |||
7fc5ee70e1 | |||
a24a335743 | |||
ef9af71960 | |||
925702d315 | |||
d50dffec8c | |||
cef2081a13 | |||
06c8bde7c9 | |||
c9bbfa3820 | |||
eed7cfdc42 | |||
bd9b0d16ab | |||
ea6c33b497 | |||
dc80ade2fb | |||
f6a06ee8ea | |||
2644f27975 | |||
3dc68a7b8b | |||
97fc1d6239 | |||
662f4d22a3 | |||
b70aa12be9 | |||
71f12f5f19 | |||
e10504eeeb | |||
2dea9f3bc0 | |||
35c9dda5bc | |||
e831f3949a | |||
b0b39cc693 | |||
fc03be3f73 | |||
b61f6b81f1 | |||
f5bc1c1fce | |||
a729f9568c | |||
5749e00377 | |||
ef73c24a0c | |||
94717ee351 | |||
9a18ded65b | |||
b23220f491 | |||
6e6bb03246 | |||
1ae6bae3b8 | |||
1239ca3256 | |||
57b7797ea4 | |||
5ee5bfd1d5 | |||
7229a3e198 | |||
bee083582f | |||
9d7f99f286 | |||
6ef403853c | |||
6ae7ccb9a1 | |||
dafc50c463 | |||
e89e2c931d | |||
43a67b99e4 | |||
46c48f4f31 | |||
bf0fe85aa6 | |||
42c5bd3a7f | |||
d170e86c8a | |||
e3078169b1 | |||
a33ad123f6 | |||
7e14fa2f5c | |||
ba698b41e1 | |||
e76215987e | |||
d1a247af8c | |||
2b7e9534f3 | |||
38521558a1 | |||
100f3d214b | |||
1309867611 | |||
611f64f2e1 | |||
f118ebce6e | |||
59fc90e556 | |||
fb9e4d57fb | |||
960c38b696 | |||
39895e58a6 | |||
b420d85be5 | |||
19ea2f340a | |||
11b0d059bf | |||
c8a25ce544 | |||
509583ea2e | |||
1c86f3f4bf | |||
6d999be590 | |||
e715772dbf | |||
63eb7b7ea8 | |||
b07683b815 | |||
96e97d1691 | |||
261d8827e3 | |||
c3156f2e41 | |||
8c08801460 | |||
aaf1299da7 | |||
a411355b4f | |||
eba41066b4 | |||
f295dff8a2 | |||
a16815143c | |||
6ff3f6ae42 | |||
84071881af | |||
7cccf7e56e | |||
2516d5e381 | |||
cdec8bac75 | |||
fa30aae194 | |||
eb959a3135 | |||
24033e0cac | |||
71f9a505e0 | |||
979b8f017b | |||
af78f01682 | |||
0b16d5c826 | |||
597e14bc7e | |||
04a95867e2 | |||
e0d5eb45b7 | |||
b90cab318e | |||
3252b61abe | |||
2a2da21ff3 | |||
04ef371337 | |||
623e0cd369 | |||
1f466747f0 | |||
2215462f99 | |||
ac4c0a2e4c | |||
f7496db5ac | |||
3028fe975d | |||
23a5034493 | |||
65e339f811 | |||
2020e48659 | |||
9566d6f81f | |||
a00d734712 | |||
27a3e8706a | |||
e601efcfc0 | |||
2bfad9f885 | |||
e78e984943 | |||
242f4b02d0 | |||
041be5a1d1 | |||
976ce056c1 | |||
00c23c73a8 | |||
9dd1848337 | |||
9b30efd9a2 | |||
5853f7cc49 | |||
7b00a06f3e | |||
9ef9be0f37 | |||
13ca419473 | |||
b80f1a0773 | |||
e85101c74e | |||
90668bdf63 | |||
e1a00079a5 | |||
e3add4df42 | |||
ced1a3bccb | |||
8d13a14343 | |||
7238e1ea8a | |||
bd0423f243 | |||
b0441c134c | |||
5860901c30 | |||
df4eaea4b9 | |||
e9223d5502 | |||
702c5a3e5d | |||
ccbed95cdc | |||
c4e2c06cf5 | |||
a6135738a3 | |||
ad1ea985b8 | |||
2058950d07 | |||
4dc27f4489 | |||
122daa3220 | |||
e30928e23d | |||
30292306bb | |||
2f9aa585f1 | |||
9e02c3e5ff | |||
7f813a519b | |||
b5072486b4 | |||
50c1cd8215 | |||
ed4812e6d8 | |||
59e0e751f1 | |||
a5ae5e6c2b | |||
a2b87e63c9 | |||
d95c283653 | |||
6a9ae0c148 | |||
040dd517d8 | |||
9663c1ca64 | |||
ffc2d23be7 | |||
49b009e59b | |||
778b6a555b | |||
b3730998e9 | |||
837758aebe | |||
6ac4149aa1 | |||
278584ae6a | |||
126f9d5f41 | |||
ce15ff4e0a | |||
382b956beb | |||
36deab4909 | |||
550fc59d9d | |||
9100a57458 | |||
74d9bbccd9 | |||
75e602a349 | |||
e73644db10 | |||
148e6bdae7 | |||
1b540199f0 | |||
30b70e9984 | |||
c1557cff27 | |||
2d662f91b3 | |||
c4a08e52e5 | |||
08c371c142 | |||
8e62dd460c | |||
1f9302dc94 | |||
16f445f699 | |||
b4abd564ec | |||
14ffafb0a7 | |||
e0cc780887 | |||
ab7d277167 | |||
8308c1df4d | |||
12c29f5180 | |||
2963098870 | |||
9f56b0a26d | |||
75851312fd | |||
3a3be138a5 | |||
fb7d1f18b0 | |||
d1c6c7696a | |||
5f38d9635d | |||
8185ea87b1 | |||
98bd07d025 | |||
c502c70a21 | |||
d933dc532b | |||
42594f0656 | |||
4b83ca19c3 | |||
bab13f5e83 | |||
14c5f5a26c | |||
b26a4f24d4 | |||
498ec673dc | |||
5c67ab6aec | |||
f78359b5d7 | |||
dbce612cb2 | |||
4322ffdb0a | |||
123b848d74 | |||
b120fbd2b1 | |||
d41c3c2de4 | |||
a3cabd7a9e | |||
3dde486126 | |||
5978122dda | |||
4b4806ce34 | |||
987c6cc709 | |||
012c7e9bdf | |||
d06bb265e5 | |||
e2fb8bf21e | |||
60dea2a518 | |||
3877f49278 | |||
a299a5b505 | |||
a03f7201d2 | |||
94e26ee8a0 | |||
2d8ad47083 | |||
e9aec238a1 | |||
844c33166c | |||
cfe9f2159f | |||
61bccd5f48 | |||
e5a608a315 | |||
9b4f35eea1 | |||
602ae2385b | |||
48a4246aac | |||
a4020cb484 | |||
d074f29b82 | |||
a67bef6853 | |||
14e666f18d | |||
cce914091c | |||
d7cd3db8e2 | |||
0d8f844314 | |||
abd0cb2031 | |||
79ef03ed0c | |||
a3087cb650 | |||
9acb9af338 | |||
0c9283eb87 | |||
493d838201 | |||
c0523469fa | |||
4f00eaa6d4 | |||
9706524572 | |||
3174b84367 | |||
6cccd5ff6c | |||
972857268d | |||
f3cd92cc5e | |||
502a49644c | |||
51719b5868 | |||
17821bd094 | |||
6a89ffaa8a | |||
d5a8d0f4d4 | |||
9739bd871e | |||
632d2545d4 | |||
ecfa273a81 | |||
a36828116e | |||
14aa9ca26c | |||
069e51f2ff | |||
a15cbdfb1a | |||
4af8114eda | |||
90dc84e900 | |||
67c595b440 | |||
830b7aee56 | |||
1890608cb3 | |||
bd396959a9 |
24
.github/workflows/dokka_push.yml
vendored
Normal file
24
.github/workflows/dokka_push.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Publish KDocs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
publishing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Build
|
||||
run: ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dokka/build/dokka/html
|
||||
publish_branch: kdocs
|
28
.github/workflows/packages_push.yml
vendored
Normal file
28
.github/workflows/packages_push.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
name: Publish package to GitHub Packages
|
||||
on: [push]
|
||||
jobs:
|
||||
publishing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||
rm gradle.properties
|
||||
mv gradle.properties.tmp gradle.properties
|
||||
- name: Build
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
continue-on-error: true
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
|
||||
env:
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,5 +11,6 @@ out/
|
||||
|
||||
secret.gradle
|
||||
local.properties
|
||||
kotlin-js-store
|
||||
|
||||
publishing.sh
|
||||
|
8
.space.kts
Normal file
8
.space.kts
Normal file
@@ -0,0 +1,8 @@
|
||||
job("Build and run tests") {
|
||||
container(displayName = "Run gradle build", image = "openjdk:11") {
|
||||
kotlinScript { api ->
|
||||
// here can be your complex logic
|
||||
api.gradlew("build")
|
||||
}
|
||||
}
|
||||
}
|
27
.travis.yml
27
.travis.yml
@@ -1,27 +0,0 @@
|
||||
language: android
|
||||
install: true
|
||||
|
||||
os: linux
|
||||
dist: trusty
|
||||
jdk: oraclejdk8
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-30.0.2
|
||||
- android-30
|
||||
- add-on
|
||||
- extra
|
||||
|
||||
before_script:
|
||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2"
|
||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30"
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: build
|
||||
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
|
||||
# Tests are temporarily disabled on public travis due to the problems of launching
|
||||
# - state: test
|
||||
# script: ./gradlew allTests
|
832
CHANGELOG.md
832
CHANGELOG.md
@@ -1,5 +1,837 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.24
|
||||
|
||||
* `Ktor`:
|
||||
* `Common`:
|
||||
* New extension fun `MPPFile#input`
|
||||
|
||||
## 0.9.23
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* New property `ExposedRepo#selectAll` to retrieve all the rows in the table
|
||||
|
||||
## 0.9.22
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Now `createKtorServer` fun is fully customizable
|
||||
|
||||
## 0.9.21
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* fixes in `AbstractExposedWriteCRUDRepo`
|
||||
|
||||
## 0.9.20
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Fixes in `OneToManyAndroidRepo`
|
||||
* New `CursorIterator`
|
||||
|
||||
## 0.9.19
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.0` -> `1.6.1`
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fixes in `ExposedStandardVersionsRepoProxy`
|
||||
|
||||
## 0.9.18
|
||||
|
||||
* `Common`
|
||||
* New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
|
||||
|
||||
## 0.9.17
|
||||
|
||||
* `Common`:
|
||||
* New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
|
||||
* `Coroutines`:
|
||||
* New extension `Element.visibilityFlow()`
|
||||
* `FSM`:
|
||||
* Now it is possible to resolve conflicts on `startChain`
|
||||
|
||||
## 0.9.16
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.6.3` -> `2.7.0`
|
||||
* `Common`:
|
||||
* New extension `Node#onRemoved`
|
||||
* `Compose`:
|
||||
* New extension `Composition#linkWithRoot` for removing of composition with root element
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
|
||||
|
||||
## 0.9.15
|
||||
|
||||
* `FSM`:
|
||||
* Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
|
||||
* `DefaultStatesManager` now is extendable
|
||||
* `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
|
||||
|
||||
## 0.9.14
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.6.2` -> `2.6.3`
|
||||
* `Ktor`: `1.6.7` -> `1.6.8`
|
||||
* `Ktor`:
|
||||
* Add temporal files uploading functionality (for clients to upload and for server to receive)
|
||||
|
||||
## 0.9.13
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.1.0` -> `1.1.1`
|
||||
|
||||
## 0.9.12
|
||||
|
||||
* `Common`:
|
||||
* `JS`:
|
||||
* New function `openLink`
|
||||
* New function `selectFile`
|
||||
* New function `triggerDownloadFile`
|
||||
* `Compose`:
|
||||
* Created :)
|
||||
* `Common`:
|
||||
* `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
|
||||
* `JS`:
|
||||
* `openLink` on top of `openLink` with `String` target from common
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* `Common`:
|
||||
* New extension `Flow.toMutableState`
|
||||
* New extension `StateFlow.toMutableState`
|
||||
* `JS`:
|
||||
* New function `selectFileOrThrow` on top of `selectFile` from `common`
|
||||
* New function `selectFileOrNull` on top of `selectFile` from `common`
|
||||
|
||||
## 0.9.11
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.6.1` -> `2.6.2`
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* Created :)
|
||||
* New extensions and function:
|
||||
* `Composition#linkWithJob`
|
||||
* `Composition#linkWithContext`
|
||||
* `renderComposableAndLinkToContext`
|
||||
|
||||
## 0.9.10
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.5.2` -> `2.6.1`
|
||||
* Ktor:
|
||||
* Client:
|
||||
* New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
|
||||
* Server:
|
||||
* Now it is possible to filter data in `Route#includeWebsocketHandling`
|
||||
* Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
|
||||
* Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
|
||||
|
||||
## 0.9.9
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.5.1` -> `2.5.2`
|
||||
* `Common`:
|
||||
* Add new diff tool - `applyDiff`
|
||||
* Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
|
||||
|
||||
## 0.9.8
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.37.2` -> `0.37.3`
|
||||
* `Klock`: `2.4.13` -> `2.5.1`
|
||||
* `AppCompat`: `1.4.0` -> `1.4.1`
|
||||
|
||||
## 0.9.7
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore`
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not
|
||||
|
||||
## 0.9.6
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere`
|
||||
|
||||
## 0.9.5
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.12` -> `2.4.13`
|
||||
|
||||
## 0.9.4
|
||||
|
||||
* `Pagination`:
|
||||
* `Common`:
|
||||
* Add several `optionallyReverse` functions
|
||||
* `Common`:
|
||||
* Changes in `Either`:
|
||||
* Now `Either` uses `optionalT1` and `optionalT2` as main properties
|
||||
* `Either#t1` and `Either#t2` are deprecated
|
||||
* New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
|
||||
|
||||
## 0.9.3
|
||||
|
||||
* `Versions`:
|
||||
* `UUID`: `0.3.1` -> `0.4.0`
|
||||
|
||||
## 0.9.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.10` -> `2.4.12`
|
||||
|
||||
## 0.9.1
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Default realizations of standard interfaces for exposed DB are using public fields for now:
|
||||
* `ExposedReadKeyValueRepo`
|
||||
* `ExposedReadOneToManyKeyValueRepo`
|
||||
* `ExposedStandardVersionsRepoProxy`
|
||||
* New typealiases for one to many exposed realizations:
|
||||
* `ExposedReadKeyValuesRepo`
|
||||
* `ExposedKeyValuesRepo`
|
||||
|
||||
## 0.9.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.5.31` -> `1.6.10`
|
||||
* `Coroutines`: `1.5.2` -> `1.6.0`
|
||||
* `Serialization`: `1.3.1` -> `1.3.2`
|
||||
* `Exposed`: `0.36.2` -> `0.37.2`
|
||||
* `Ktor`: `1.6.5` -> `1.6.7`
|
||||
* `Klock`: `2.4.8` -> `2.4.10`
|
||||
|
||||
## 0.8.9
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Fixes in `uniloadMultipart`
|
||||
* `Client`:
|
||||
* Fixes in `unimultipart`
|
||||
* `FSM`:
|
||||
* Fixes in `DefaultUpdatableStatesMachine`
|
||||
|
||||
## 0.8.8
|
||||
|
||||
* `Versions`:
|
||||
* `AppCompat`: `1.3.1` -> `1.4.0`
|
||||
* Android Compile SDK: `31.0.0` -> `32.0.0`
|
||||
* `FSM`:
|
||||
* `DefaultStatesMachine` now is extendable
|
||||
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
|
||||
|
||||
## 0.8.7
|
||||
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* `UnifiedRequester` now have no private fields
|
||||
* Add preview work with multipart
|
||||
* `Server`
|
||||
* `UnifiedRouter` now have no private fields
|
||||
* Add preview work with multipart
|
||||
|
||||
## 0.8.6
|
||||
|
||||
* `Common`:
|
||||
* `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
|
||||
* All `joinTo` now accept not `crossinline` callbacks
|
||||
|
||||
## 0.8.5
|
||||
|
||||
* `Common`:
|
||||
* `repeatOnFailure`
|
||||
|
||||
## 0.8.4
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Several new `createKtorServer`
|
||||
|
||||
## 0.8.3
|
||||
|
||||
* `Common`:
|
||||
* Ranges intersection functionality
|
||||
* New type `Optional`
|
||||
* `Pagination`:
|
||||
* `Pagination` now extends `ClosedRange<Int>`
|
||||
* `Pagination` intersection functionality
|
||||
|
||||
## 0.8.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.7` -> `2.4.8`
|
||||
* `Serialization`: `1.3.0` -> `1.3.1`
|
||||
* `FSM`:
|
||||
* Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
|
||||
* Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
|
||||
|
||||
## 0.8.1
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.36.1` -> `0.36.2`
|
||||
* `Core KTX`: `1.6.0` -> `1.7.0`
|
||||
|
||||
## 0.8.0
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.6` -> `2.4.7`
|
||||
* `Ktor`: `1.6.4` -> `1.6.5`
|
||||
* `Exposed`: `0.35.3` -> `0.36.1`
|
||||
* `Common`:
|
||||
* Type `Either` got its own serializer
|
||||
* `FSM`:
|
||||
* `Common`:
|
||||
* Full rework of FSM:
|
||||
* Now it is more flexible for checking of handler opportunity to handle state
|
||||
* Now machine and states managers are type-oriented
|
||||
* `StateHandlerHolder` has been renamed to `CheckableHandlerHolder`
|
||||
* Add opportunity for comfortable adding default state handler
|
||||
|
||||
## 0.7.4
|
||||
|
||||
* `Common`:
|
||||
* New type `Either`
|
||||
* `Serialization`:
|
||||
* `TypedSerializer`
|
||||
* New factory fun which accept vararg pairs of type and its serializer
|
||||
* `Repos`:
|
||||
* `Common` (`Android`):
|
||||
* `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that
|
||||
android crud repo _WILL NOT_ send previous events to the
|
||||
* `Exposed`:
|
||||
* New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows`
|
||||
* KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in
|
||||
inheritors
|
||||
* `Pagination`:
|
||||
* `Common`:
|
||||
* New types `getAllBy*` for current, next and custom paging
|
||||
|
||||
## 0.7.3
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.35.2` -> `0.35.3`
|
||||
|
||||
## 0.7.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.5` -> `2.4.6`
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.3` -> `2.4.5`
|
||||
* `Exposed`: `0.35.1` -> `0.35.2`
|
||||
* `Coroutines`:
|
||||
* `Common`:
|
||||
* New `Flow` - `AccumulatorFlow`
|
||||
* `FSM`:
|
||||
* `Common`:
|
||||
* `InMemoryStatesManager` has been replaced
|
||||
* `StatesMachine` became an interface
|
||||
* New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of
|
||||
data info
|
||||
|
||||
## 0.7.0
|
||||
|
||||
**THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL**
|
||||
|
||||
* `Versions`
|
||||
* `kotlinx.datetime` -> `Klock`
|
||||
|
||||
## 0.6.0 DO NOT RECOMMENDED
|
||||
|
||||
**THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL**
|
||||
**ALL DEPRECATION HAVE BEEN REMOVED**
|
||||
|
||||
* `Versions`
|
||||
* `Klock` -> `kotlinx.datetime`
|
||||
|
||||
## 0.5.31
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.2` -> `2.4.3`
|
||||
* `Ktor`: `1.6.3` -> `1.6.4`
|
||||
|
||||
## 0.5.30
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.2.2` -> `1.3.0`
|
||||
|
||||
## 0.5.29
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.34.2` -> `0.35.1`
|
||||
|
||||
## 0.5.28
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.5.30` -> `1.5.31`
|
||||
* `Klock`: `2.4.1` -> `2.4.2`
|
||||
|
||||
## 0.5.27
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.34.1` -> `0.34.2`
|
||||
|
||||
## 0.5.26
|
||||
|
||||
* `Repos`:
|
||||
* `InMemory`:
|
||||
* `MapCRUDRepo`s and `MapKeyValueRepo`s got `protected` methods and properties instead of private
|
||||
|
||||
## 0.5.25
|
||||
|
||||
* `Versions`:
|
||||
* `UUID`: `0.3.0` -> `0.3.1`
|
||||
* `Common`:
|
||||
* New property `MPPFile#withoutSlashAtTheEnd`
|
||||
* Extension `clamp` has been deprecated
|
||||
* New extension `Iterable#diff`
|
||||
* `Serialization`:
|
||||
* New operators `TypedSerializer#plusAssign` and `TypedSerializer#minusAssign`
|
||||
|
||||
## 0.5.24
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.5.1` -> `1.5.2`
|
||||
* `Klock`: `2.3.4` -> `2.4.1`
|
||||
* `Coroutines`:
|
||||
* New function `CoroutineScope` with safely exceptions handler as second parameter
|
||||
|
||||
## 0.5.23
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.33.1` -> `0.34.1`
|
||||
* `Common`:
|
||||
* New extensions `Iterable#joinTo` and `Array#joinTo`
|
||||
|
||||
## 0.5.22
|
||||
|
||||
* `Versions`
|
||||
* `Kotlin`: `1.5.21` -> `1.5.30`
|
||||
* `Klock`: `2.3.2` -> `2.3.4`
|
||||
* `AppCompat`: `1.3.0` -> `1.3.1`
|
||||
* `Ktor`: `1.6.2` -> `1.6.3`
|
||||
|
||||
## 0.5.21
|
||||
|
||||
* `Versions`
|
||||
* `Klock`: `2.3.1` -> `2.3.2`
|
||||
* `Serialization`
|
||||
* `Typed Serializer`:
|
||||
* `TypedSerializer` Descriptor serial name has been fixed
|
||||
|
||||
## 0.5.20
|
||||
|
||||
* `Repos`:
|
||||
* `Common`
|
||||
* `Android`:
|
||||
* `*OrNull` analogs of `Cursor.get*(String)` extensions have been added
|
||||
* Extensions `Cursor.getFloat` and `Cursor.getFloatOrNull` have been added
|
||||
|
||||
## 0.5.19
|
||||
|
||||
* `LanguageCode`:
|
||||
* `IetfLanguageCode` became as sealed class
|
||||
* `IetfLanguageCode` now override `toString` and returns its code
|
||||
|
||||
## 0.5.18
|
||||
|
||||
* `Versions`
|
||||
* `Kotlin Exposed`: `0.32.1` -> `0.33.1`
|
||||
* `LanguageCode`:
|
||||
* Module has been created
|
||||
|
||||
## 0.5.17
|
||||
|
||||
**SINCE THIS UPDATE JS PARTS WILL BE COMPILED WITH IR COMPILER ONLY**
|
||||
|
||||
* `Versions`
|
||||
* `Kotlin`: `1.5.20` -> `1.5.21`
|
||||
* `Ktor`: `1.6.1` -> `1.6.2`
|
||||
* `Klock`: `2.2.0` -> `2.3.1`
|
||||
* `CryptoJS`: `4.0.0` -> `4.1.1`
|
||||
|
||||
## 0.5.16
|
||||
|
||||
* `Versions`
|
||||
* `Coroutines`: `1.5.0` -> `1.5.1`
|
||||
* `Serialization`: `1.2.1` -> `1.2.2`
|
||||
* `Ktor`: `1.6.0` -> `1.6.1`
|
||||
* `Klock`: `2.1.2` -> `2.2.0`
|
||||
* `Core KTX`: `1.5.0` -> `1.6.0`
|
||||
|
||||
## 0.5.15 HOTFIX FOR 0.5.14
|
||||
|
||||
* `Coroutines`
|
||||
* Fixes in `subscribeAsync`
|
||||
|
||||
## 0.5.14 NOT RECOMMENDED
|
||||
|
||||
* `Versions`
|
||||
* `Kotlin`: `1.5.10` -> `1.5.20`
|
||||
* `Coroutines`
|
||||
* `subscribeSafelyWithoutExceptions` got new parameter `onException` by analogue with `safelyWithoutExceptions`
|
||||
* New extensions `Flow#subscribeAsync` and subsequent analogs of `subscribe` with opportunity to set up custom marker
|
||||
|
||||
## 0.5.13
|
||||
|
||||
* `Common`:
|
||||
* Add functionality for multiplatform working with files:
|
||||
* Main class for files `MPPFile`
|
||||
* Inline class for filenames work encapsulation `FileName`
|
||||
* `FSM`
|
||||
* Module inited and in preview state
|
||||
|
||||
## 0.5.12
|
||||
|
||||
* `Common`:
|
||||
* `Android`
|
||||
* Extension `View#changeVisibility` has been fixed
|
||||
* `Android`
|
||||
* `RecyclerView`
|
||||
* Default adapter got `dataCountFlow` property
|
||||
* New subtype of adapter based on `StateFlow`: `StateFlowBasedRecyclerViewAdapter`
|
||||
|
||||
## 0.5.11
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Fixes in `WriteOneToManyRepo#add`
|
||||
* `Exposed`:
|
||||
* Fixes in `ExposedOneToManyKeyValueRepo#add`
|
||||
|
||||
## 0.5.10
|
||||
|
||||
* `Versions`
|
||||
* `Core KTX`: `1.3.2` -> `1.5.0`
|
||||
* `AndroidX Recycler`: `1.2.0` -> `1.2.1`
|
||||
* `AppCompat`: `1.2.0` -> `1.3.0`
|
||||
* `Android`
|
||||
* `RecyclerView`:
|
||||
* `data` of `RecyclerViewAdapter` became an abstract field
|
||||
* New function `RecyclerViewAdapter`
|
||||
* `Common`:
|
||||
* New extension `View#changeVisibility`
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* `WriteOneToManyRepo` got new function `clearWithValue`
|
||||
* `Android`:
|
||||
* New extension `SQLiteDatabase#selectDistinct`
|
||||
* Fixes in `OneToManyAndroidRepo`
|
||||
* `Ktor`
|
||||
* `Server`
|
||||
* All elements in configurators became a `fun interface`
|
||||
* `Pagination`
|
||||
* New function `doForAllWithCurrentPaging`
|
||||
|
||||
## 0.5.9
|
||||
|
||||
* `Repos`
|
||||
* `Common`
|
||||
* `OneToManyAndroidRepo` got new primary constructor
|
||||
|
||||
## 0.5.8
|
||||
|
||||
* `Common`:
|
||||
* New extension `Iterable#firstNotNull`
|
||||
* `Coroutines`
|
||||
* New extension `Flow#firstNotNull`
|
||||
* New extensions `CoroutineContext#LinkedSupervisorJob`, `CoroutineScope#LinkedSupervisorJob` and
|
||||
`CoroutineScope#LinkedSupervisorScope`
|
||||
|
||||
## 0.5.7
|
||||
|
||||
* `Pagination`
|
||||
* `Ktor`
|
||||
* `Server`
|
||||
* Fixes in extension `extractPagination`
|
||||
* `Repos`
|
||||
* `Cache`
|
||||
* All standard cache repos have been separated to read and read/write repos
|
||||
|
||||
## 0.5.6
|
||||
|
||||
* `Versions`
|
||||
* `Exposed`: `0.31.1` -> `0.32.1`
|
||||
* `Coroutines`
|
||||
* `JVM`
|
||||
* `launchSynchronously` and subsequent functions got improved mechanism
|
||||
* New method `safelyWithResult`
|
||||
|
||||
## 0.5.5
|
||||
|
||||
* `Versions`
|
||||
* `Ktor`: `1.5.4` -> `1.6.0`
|
||||
|
||||
## 0.5.4
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.1.0` -> `2.1.2`
|
||||
|
||||
## 0.5.3
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.5.0` -> `1.5.10`
|
||||
* `Coroutines`:
|
||||
* Extensions `doInUI` and `doInDefault` were replaced in common and available on any supported platform
|
||||
* Extension `doInIO` replaced into `jvm` and available on any `JVM` platform
|
||||
* Old extension `safelyWithouException` without `onException` has been replaced by its copy with `onException` and
|
||||
default value
|
||||
* New value `defaultSafelyWithoutExceptionHandlerWithNull` which is used in all `*WithoutExceptions` by default
|
||||
* Analogs of `launch` and `async` for `safely` and `safelyWithoutExceptions` were added
|
||||
* Analogs of `runCatching` for `safely` and `safelyWithoutExceptions` were added
|
||||
|
||||
## 0.5.2
|
||||
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* Fixes in `UnifiedRequester`
|
||||
|
||||
## 0.5.1
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Serialization`: `1.2.0` -> `1.2.1`
|
||||
|
||||
## 0.5.0
|
||||
|
||||
**Notice**: This version is still depend on Kotlin
|
||||
Exposed 0.31.1. That means that this version
|
||||
may work improperly in modules based on Kotlin
|
||||
Exposed
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Exposed`: `0.30.2` -> `0.31.1`
|
||||
* `Kotlin Coroutines`: `1.4.3` -> `1.5.0`
|
||||
* `RecyclerView`: `1.1.0` -> `1.2.0`
|
||||
* `Ktor`: `1.5.3` -> `1.5.4`
|
||||
* `Klock`: `2.0.7` -> `2.1.0`
|
||||
* `UUID`: `0.2.4` -> `0.3.0`
|
||||
* **ALL DEPRECATIONS WERE REMOVED**
|
||||
* `Android`:
|
||||
* `Alerts`:
|
||||
* `RecyclerView`:
|
||||
* Classes `ActionViewHolder` and `ActionsRecyclerViewAdapter` became public
|
||||
* `Coroutines`:
|
||||
* New extension and function `doSynchronously` which are the same as `launchSynchronously`
|
||||
* New extensions `doInDefault` and `doInIO`
|
||||
|
||||
## 0.4.36
|
||||
|
||||
* All `Android` targets inside common mpp modules now includes JVM code
|
||||
|
||||
## 0.4.35
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Exposed`: `0.30.1` -> `0.30.2`
|
||||
* `Serialization`:
|
||||
* `TypedSerializer`:
|
||||
* Project has been inited
|
||||
|
||||
## 0.4.34
|
||||
|
||||
* `Versions`:
|
||||
* `uuid`: `0.2.3` -> `0.2.4`
|
||||
* `Repos`:
|
||||
* `AbstractExposedCRUDRepo` now implements `StandardCRUDRepo`
|
||||
* `AbstractMutableAndroidCRUDRepo` now implements `StandardCRUDRepo`
|
||||
|
||||
## 0.4.33
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `1.5.2` -> `1.5.3`
|
||||
* `Coroutines`
|
||||
* Add `WeakJob` workaround:
|
||||
* `CoroutineScope#weakLaunch`
|
||||
* `CoroutineScope#weakAsync`
|
||||
|
||||
## 0.4.32
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin Exposed`: `0.29.1` -> `0.30.1`
|
||||
|
||||
## 0.4.31
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.31` -> `1.4.32`
|
||||
* `Pagination`:
|
||||
* New extensions `PaginationResult.changeResultsUnchecked` and `PaginationResult.changeResults` for mapping results
|
||||
with the same parameters, but different data
|
||||
* Extension `PaginationResult.thisPageIfNotEmpty` now is typed and will return `PaginationResult?` with the same
|
||||
generic type as income `PaginationResult`
|
||||
* New extension `PaginationResult.currentPageIfNotEmpty` - shortcut for `PaginationResult.thisPageIfNotEmpty`
|
||||
* New common functions. They were created as replacements for currently available for more comfortable work
|
||||
with repos pagination:
|
||||
* `doForAll`
|
||||
* `doForAllWithNextPaging`
|
||||
* `doForAllWithCurrentPaging`
|
||||
* `getAll`
|
||||
* `getAllWithNextPaging`
|
||||
* `getAllWithCurrentPaging`
|
||||
* `Coroutines`:
|
||||
* Rewrite `subscribeSafelyWithoutExceptions`
|
||||
* Now `subscribeSafelyWithoutExceptions` will use default handler instead of skipping
|
||||
* New extension `subscribeSafelySkippingExceptions`
|
||||
* `Repos`
|
||||
* New subproject `repos.cache` - this subproject will contain repos with data caching mechanisms
|
||||
* Most old `doForAll` methods have been deprecated
|
||||
|
||||
## 0.4.30
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.0.6` -> `2.0.7`
|
||||
* `Pagination`:
|
||||
* New variable `defaultPaginationPageSize` has been added to be able to change default pagination size
|
||||
* Add new value `firstPageWithOneElementPagination`
|
||||
|
||||
## 0.4.29
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.4.2` -> `1.4.3`
|
||||
* `Repos`:
|
||||
* `Common`
|
||||
* `Android`:
|
||||
* New `blockingReadableTransaction`/`blockingWritableTransaction`
|
||||
* Android databases realizations now use blocking transactions where it is possible
|
||||
* Several improvements in transactions work
|
||||
|
||||
## 0.4.28
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.30` -> `1.4.31`
|
||||
* `Ktor`: `1.5.1` -> `1.5.2`
|
||||
* `Coroutines`
|
||||
* Add `createActionsActor`/`createSafeActionsActor` and `doWithSuspending`
|
||||
|
||||
## 0.4.27
|
||||
|
||||
* `Repos`
|
||||
* `Exposed`
|
||||
* Fix in `AbstractExposedWriteCRUDRepo`
|
||||
|
||||
## 0.4.26
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.0.1` -> `1.1.0`
|
||||
|
||||
## 0.4.25
|
||||
|
||||
* `Matrix`:
|
||||
* Subproject has been created
|
||||
|
||||
## 0.4.24
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.21` -> `1.4.30`
|
||||
* `Klock`: `2.0.4` -> `2.0.6`
|
||||
* `Coroutines`:
|
||||
* New class `DoWithFirstBuilder`
|
||||
* Several new extensions like `firstOf`/`first`/`invokeOnFirstOf`
|
||||
|
||||
## 0.4.23
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `1.5.0` -> `1.5.1`
|
||||
* `Serialization`
|
||||
* `Base64`
|
||||
* New serializer `Base64BytesToFromStringSerializer` has been added
|
||||
|
||||
## 0.4.22
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.28.1` -> `0.29.1`
|
||||
* `Klock`: `2.0.2` -> `2.0.4`
|
||||
|
||||
## 0.4.21
|
||||
|
||||
* `Common`
|
||||
* `JS`
|
||||
* Extension `DataView#toByteArray` has been added
|
||||
* Extension `ArrayBuffer#toByteArray` has been added
|
||||
* Extension `ByteArray#toDataView` has been added
|
||||
* Extension `ByteArray#toArrayBuffer` has been added
|
||||
* `Coroutines`
|
||||
* `JS`
|
||||
* Extension `Blob#toByteArray` has been added
|
||||
* `Crypto`
|
||||
* Subproject has been created
|
||||
* `Serialization`
|
||||
* `Base64`
|
||||
* Currently, it is not depended on `common` project and use `crypto` instead
|
||||
|
||||
## 0.4.20
|
||||
|
||||
* `Serialization`
|
||||
* `Encapsulator`:
|
||||
* Has been created
|
||||
|
||||
## 0.4.19
|
||||
|
||||
* `Coroutines`:
|
||||
* New extension `Iterable<Deferred>#awaitFirstWithDeferred` has been added to identify which of `Deferred`s was
|
||||
normally completed
|
||||
* New extensions `Iterable<Deferred<T>>#invokeOnFirst` and `Iterable<DeferredAction<*, O>>.invokeFirstOf` have been
|
||||
added
|
||||
|
||||
## 0.4.18
|
||||
|
||||
* `Coroutines`:
|
||||
* New extension `Iterable<Deferred>#awaitFirst` has been added
|
||||
* `Serialization`
|
||||
* `Base 64`
|
||||
* New `Base64ByteArraySerializer` has been added
|
||||
|
||||
## 0.4.17
|
||||
|
||||
* `Common`
|
||||
* Functionality for decode/encode base 64 to/from `ByteArray`/`String` have been added
|
||||
* `Serialization`
|
||||
* `Base 64`
|
||||
* Project has been initiated
|
||||
|
||||
## 0.4.16
|
||||
|
||||
* `Coroutines`:
|
||||
* `safely`:
|
||||
* New `safelyWithoutExceptions` function may accept `onException` parameter with nullable result
|
||||
* Old `safelyWithoutExceptions` now using `defaultSafelyWithoutExceptionHandler` to handle exceptions "like in
|
||||
`safely`", but it is expected that `defaultSafelyWithoutExceptionHandler` will not throw any exception
|
||||
|
||||
## 0.4.15
|
||||
|
||||
* `Coroutines`:
|
||||
* `safely`:
|
||||
* `SafelyExceptionHandlerKey` has been deprecated
|
||||
* `SafelyExceptionHandler` has been deprecated
|
||||
* `ContextSafelyExceptionHandlerKey` has been added
|
||||
* `ContextSafelyExceptionHandler` has been added
|
||||
* `safelyWithContextExceptionHandler` has been added
|
||||
|
||||
## 0.4.14
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.20` -> `1.4.21`
|
||||
* `Ktor`: `1.4.3` -> `1.5.0`
|
||||
* `Klock`: `2.0.1` -> `2.0.2`
|
||||
* `Coroutines`:
|
||||
* Add global variable `defaultSafelyExceptionHandler`
|
||||
* Add `SafelyExceptionHandlerKey` and `SafelyExceptionHandler` classes to be able to overwrite
|
||||
`defaultSafelyExceptionHandler` using context of coroutine
|
||||
|
||||
## 0.4.13
|
||||
|
||||
* `Common`
|
||||
* `Android`
|
||||
* Add expand/collapse functionality for horizontal expand/collapse
|
||||
|
||||
## 0.4.12
|
||||
|
||||
* `Coroutines`
|
||||
|
@@ -22,6 +22,7 @@ You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUti
|
||||
* `ktor` is a set of modules for `client`s and `server`s
|
||||
* `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types
|
||||
* `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination
|
||||
* `serialization` is a collection of projects with serializers for `kotlinx.serialization`
|
||||
* `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories
|
||||
|
||||
## Complex modules structure
|
||||
|
@@ -10,7 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
api libs.android.appCompat.resources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ data class AlertAction(
|
||||
val callback: (DialogInterface) -> Unit
|
||||
)
|
||||
|
||||
private class ActionViewHolder(
|
||||
class ActionViewHolder(
|
||||
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
||||
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
||||
private lateinit var action: AlertAction
|
||||
@@ -34,10 +34,10 @@ private class ActionViewHolder(
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionsRecyclerViewAdapter(
|
||||
data: List<AlertAction>,
|
||||
class ActionsRecyclerViewAdapter(
|
||||
override val data: List<AlertAction>,
|
||||
private val dialogInterfaceGetter: () -> DialogInterface
|
||||
) : RecyclerViewAdapter<AlertAction>(data) {
|
||||
) : RecyclerViewAdapter<AlertAction>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||
parent, dialogInterfaceGetter
|
||||
)
|
||||
|
@@ -10,12 +10,13 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||
api libs.android.recyclerView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,21 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
|
||||
abstract class RecyclerViewAdapter<T>(
|
||||
val data: List<T>
|
||||
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||
protected abstract val data: List<T>
|
||||
|
||||
private val _dataCountState by lazy {
|
||||
MutableStateFlow<Int>(data.size)
|
||||
}
|
||||
val dataCountState: StateFlow<Int> by lazy {
|
||||
_dataCountState.asStateFlow()
|
||||
}
|
||||
|
||||
var emptyView: View? = null
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>(
|
||||
object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeChanged(positionStart, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onChanged() {
|
||||
super.onChanged()
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeRemoved(positionStart, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
}
|
||||
@@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>(
|
||||
|
||||
private fun checkEmpty() {
|
||||
emptyView ?. let {
|
||||
if (data.isEmpty()) {
|
||||
if (dataCountState.value == 0) {
|
||||
it.visibility = View.VISIBLE
|
||||
} else {
|
||||
it.visibility = View.GONE
|
||||
@@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> RecyclerViewAdapter(
|
||||
data: List<T>,
|
||||
onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T>
|
||||
) = object : RecyclerViewAdapter<T>() {
|
||||
override val data: List<T> = data
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
|
@@ -0,0 +1,50 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import dev.inmo.micro_utils.common.Diff
|
||||
import dev.inmo.micro_utils.common.PreviewFeature
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@PreviewFeature("This feature in preview state and may contains different bugs. " +
|
||||
"Besides, this feature can be changed in future in non-compatible way")
|
||||
abstract class StateFlowBasedRecyclerViewAdapter<T>(
|
||||
listeningScope: CoroutineScope,
|
||||
dataState: StateFlow<List<T>>
|
||||
) : RecyclerViewAdapter<T>() {
|
||||
override var data: List<T> = emptyList()
|
||||
|
||||
init {
|
||||
dataState.onEach {
|
||||
try {
|
||||
val diffForRemoves = Diff(data, it)
|
||||
val removedIndexes = diffForRemoves.removed.map { it.index }
|
||||
val leftRemove = removedIndexes.toMutableList()
|
||||
data = data.filterIndexed { i, _ ->
|
||||
if (i in leftRemove) {
|
||||
leftRemove.remove(i)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
removedIndexes.sortedDescending().forEach {
|
||||
notifyItemRemoved(it)
|
||||
}
|
||||
}
|
||||
val diffAddsAndReplaces = Diff(data, it)
|
||||
data = it
|
||||
withContext(Dispatchers.Main) {
|
||||
diffAddsAndReplaces.replaced.forEach { (from, to) ->
|
||||
notifyItemMoved(from.index, to.index)
|
||||
}
|
||||
diffAddsAndReplaces.added.forEach {
|
||||
notifyItemInserted(it.index)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// currently do nothing
|
||||
}
|
||||
}.launchIn(listeningScope)
|
||||
}
|
||||
}
|
24
build.gradle
24
build.gradle
@@ -1,6 +1,5 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
@@ -8,22 +7,29 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
classpath libs.buildscript.jb.dokka
|
||||
classpath libs.buildscript.gh.release
|
||||
classpath libs.buildscript.android.gradle
|
||||
classpath libs.buildscript.android.dexcount
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,3 +5,18 @@ plugins {
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
common/compose/build.gradle
Normal file
18
common/compose/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.DisposableEffectResult
|
||||
|
||||
class DefaultDisposableEffectResult(
|
||||
private val onDispose: () -> Unit
|
||||
) : DisposableEffectResult {
|
||||
override fun dispose() {
|
||||
onDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DoNothing = DefaultDisposableEffectResult {}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composition
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Composition.linkWithElement(element: Element) {
|
||||
element.onRemoved { dispose() }
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.attributes.ATarget
|
||||
|
||||
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
||||
link,
|
||||
mode.targetStr,
|
||||
features
|
||||
)
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun <TElement : Element> renderComposableAndLinkToRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithElement(root)
|
||||
}
|
1
common/compose/src/main/AndroidManifest.xml
Normal file
1
common/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
@@ -12,11 +12,9 @@ package dev.inmo.micro_utils.common
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
AnnotationTarget.TYPEALIAS
|
||||
)
|
||||
annotation class PreviewFeature
|
||||
annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases")
|
||||
|
||||
@RequiresOptIn(
|
||||
"This thing is marked as warned. See message of warn to get more info",
|
||||
@@ -30,8 +28,6 @@ annotation class PreviewFeature
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
AnnotationTarget.TYPEALIAS
|
||||
)
|
||||
annotation class Warning(val message: String)
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T {
|
||||
return when {
|
||||
this < min -> min
|
||||
this > max -> max
|
||||
else -> this
|
||||
}
|
||||
}
|
@@ -27,8 +27,8 @@ data class Diff<T> internal constructor(
|
||||
|
||||
private inline fun <T> performChanges(
|
||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||
additionalsInOld: MutableList<T>,
|
||||
additionalsInNew: MutableList<T>,
|
||||
additionsInOld: MutableList<T>,
|
||||
additionsInNew: MutableList<T>,
|
||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
removedList: MutableList<IndexedValue<T>>,
|
||||
addedList: MutableList<IndexedValue<T>>,
|
||||
@@ -52,20 +52,20 @@ private inline fun <T> performChanges(
|
||||
newPotentials.first().second ?.let { addedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
addedList.add(newOne!!)
|
||||
oldOne ?.let { additionalsInOld.add(oldOne.value) }
|
||||
oldOne ?.let { additionsInOld.add(oldOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) }
|
||||
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
|
||||
}
|
||||
}
|
||||
newOneEqualToOldObject -> {
|
||||
newPotentials.first().first ?.let { removedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
removedList.add(oldOne!!)
|
||||
newOne ?.let { additionalsInNew.add(newOne.value) }
|
||||
newOne ?.let { additionsInNew.add(newOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) }
|
||||
newPotentials.last().second ?.value ?.let { additionsInNew.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,10 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
|
||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||
}
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(other, strictComparison)
|
||||
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||
@@ -149,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||
other: Iterable<T>
|
||||
) = calculateDiff(other, strictComparison = true)
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
) = calculateDiff(source, strictComparison).let {
|
||||
for (i in it.removed.indices.sortedDescending()) {
|
||||
removeAt(it.removed[i].index)
|
||||
}
|
||||
it.added.forEach { (i, t) ->
|
||||
add(i, t)
|
||||
}
|
||||
it.replaced.forEach { (_, new) ->
|
||||
set(new.index, new.value)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,166 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
/**
|
||||
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
|
||||
*
|
||||
* @see EitherFirst
|
||||
* @see EitherSecond
|
||||
* @see Either.Companion.first
|
||||
* @see Either.Companion.second
|
||||
* @see Either.onFirst
|
||||
* @see Either.onSecond
|
||||
* @see Either.mapOnFirst
|
||||
* @see Either.mapOnSecond
|
||||
*/
|
||||
@Serializable(EitherSerializer::class)
|
||||
sealed interface Either<T1, T2> {
|
||||
val optionalT1: Optional<T1>
|
||||
val optionalT2: Optional<T2>
|
||||
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
|
||||
val t1: T1?
|
||||
get() = optionalT1.dataOrNull()
|
||||
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
|
||||
val t2: T2?
|
||||
get() = optionalT2.dataOrNull()
|
||||
|
||||
companion object {
|
||||
fun <T1, T2> serializer(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
|
||||
}
|
||||
}
|
||||
|
||||
class EitherSerializer<T1, T2>(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
) : KSerializer<Either<T1, T2>> {
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
) {
|
||||
element("type", String.serializer().descriptor)
|
||||
element("value", ContextualSerializer(Either::class).descriptor)
|
||||
}
|
||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
lateinit var result: Either<T1, T2>
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> type = decodeStringElement(descriptor, 0)
|
||||
1 -> {
|
||||
result = when (type) {
|
||||
"t1" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t1EitherSerializer
|
||||
)
|
||||
"t2" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t2EitherSerializer
|
||||
)
|
||||
else -> error("Unknown type of either: $type")
|
||||
}
|
||||
}
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
when (value) {
|
||||
is EitherFirst -> {
|
||||
encodeStringElement(descriptor, 0, "t1")
|
||||
encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
|
||||
}
|
||||
is EitherSecond -> {
|
||||
encodeStringElement(descriptor, 0, "t2")
|
||||
encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [optionalT1]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherFirst<T1, T2>(
|
||||
override val t1: T1
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = t1.optional
|
||||
override val optionalT2: Optional<T2> = Optional.absent()
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [optionalT2]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherSecond<T1, T2>(
|
||||
override val t2: T2
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = Optional.absent()
|
||||
override val optionalT2: Optional<T2> = t2.optional
|
||||
}
|
||||
|
||||
/**
|
||||
* @return New instance of [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
|
||||
/**
|
||||
* @return New instance of [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
|
||||
optionalT1.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
|
||||
optionalT2.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result of [block] if [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
|
||||
return optionalT1.mapOnPresented(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result of [block] if [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
|
||||
return optionalT2.mapOnPresented(block)
|
||||
}
|
||||
|
||||
inline fun <reified T1, reified T2> Any.either() = when (this) {
|
||||
is T1 -> Either.first<T1, T2>(this)
|
||||
is T2 -> Either.second<T1, T2>(this)
|
||||
else -> error("Incorrect type of either argument $this")
|
||||
}
|
@@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
typealias ByteArrayAllocator = () -> ByteArray
|
||||
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
|
||||
|
||||
val ByteArray.asAllocator: ByteArrayAllocator
|
||||
get() = { this }
|
||||
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
|
||||
get() = { this }
|
||||
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
|
||||
get() = { this() }
|
||||
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
|
||||
return invoke().asAllocator
|
||||
}
|
||||
|
||||
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||
private val realSerializer = ByteArraySerializer()
|
||||
@@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||
|
||||
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
||||
val bytes = realSerializer.deserialize(decoder)
|
||||
return { bytes }
|
||||
return bytes.asAllocator
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
||||
|
@@ -0,0 +1,3 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!
|
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): List<R> {
|
||||
val result = mutableListOf<R>()
|
||||
val iterator = iterator()
|
||||
|
||||
prefix ?.let(result::add)
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val element = iterator.next()
|
||||
result.add(transform(element) ?: continue)
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
result.add(separatorFun(element) ?: continue)
|
||||
}
|
||||
}
|
||||
|
||||
postfix ?.let(result::add)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): List<R> = joinTo({ separator }, prefix, postfix, transform)
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
separatorFun: (I) -> I?,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
separator: I? = null,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I>({ separator }, prefix, postfix)
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
|
@@ -0,0 +1,34 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class FileName(val string: String) {
|
||||
val name: String
|
||||
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' }
|
||||
val extension: String
|
||||
get() = name.takeLastWhile { it != '.' }
|
||||
val nameWithoutExtension: String
|
||||
get() {
|
||||
val filename = name
|
||||
return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let {
|
||||
filename.substring(0, it)
|
||||
} ?: filename
|
||||
}
|
||||
val withoutSlashAtTheEnd: String
|
||||
get() = string.dropLastWhile { it == '/' }
|
||||
override fun toString(): String = string
|
||||
}
|
||||
|
||||
|
||||
expect class MPPFile
|
||||
|
||||
expect val MPPFile.filename: FileName
|
||||
expect val MPPFile.filesize: Long
|
||||
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
fun MPPFile.bytesSync() = bytesAllocatorSync()
|
||||
suspend fun MPPFile.bytes() = bytesAllocator()
|
||||
|
@@ -0,0 +1,92 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
|
||||
* type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
|
||||
* presented by some third type.
|
||||
*
|
||||
* Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
|
||||
* will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
|
||||
* cause this null will say nothing about availability of the row (of course, it is exaggerated example)
|
||||
*
|
||||
* @see Optional.presented
|
||||
* @see Optional.absent
|
||||
* @see Optional.optional
|
||||
* @see Optional.onPresented
|
||||
* @see Optional.onAbsent
|
||||
*/
|
||||
@Serializable
|
||||
data class Optional<T> internal constructor(
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val data: T?,
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val dataPresented: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Will create [Optional] with presented data
|
||||
*/
|
||||
fun <T> presented(data: T) = Optional(data, true)
|
||||
/**
|
||||
* Will create [Optional] without data
|
||||
*/
|
||||
fun <T> absent() = Optional<T>(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
inline val <T> T.optional
|
||||
get() = Optional.presented(this)
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||
if (!dataPresented) { block() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
|
||||
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun IntRange.intersect(
|
||||
other: IntRange
|
||||
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
||||
|
||||
fun LongRange.intersect(
|
||||
other: LongRange
|
||||
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||
*/
|
||||
inline fun <R> repeatOnFailure(
|
||||
times: Int,
|
||||
onEachFailure: (Throwable) -> Unit = {},
|
||||
action: (Int) -> R
|
||||
): Optional<R> {
|
||||
repeat(times) {
|
||||
runCatching {
|
||||
action(it)
|
||||
}.onFailure(onEachFailure).onSuccess {
|
||||
return Optional.presented(it)
|
||||
}
|
||||
}
|
||||
return Optional.absent()
|
||||
}
|
@@ -11,7 +11,7 @@ class DiffUtilsTests {
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class DiffUtilsTests {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in 0 until oldList.size) {
|
||||
for (step in oldList.indices) {
|
||||
for ((i, v) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
val changes = (
|
||||
@@ -73,4 +73,78 @@ class DiffUtilsTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleRemoveApplyWorks() {
|
||||
val oldList = (0 until 10).toList()
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val removedSublist = oldList.subList(i, i + count)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
val targetList = oldList - removedSublist
|
||||
|
||||
mutableOldList.applyDiff(targetList)
|
||||
|
||||
assertEquals(
|
||||
targetList,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleAddApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
||||
val mutable = oldList.toMutableList()
|
||||
mutable.addAll(i, addedSublist)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
|
||||
mutableOldList.applyDiff(mutable)
|
||||
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleChangesApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in oldList.indices) {
|
||||
for ((i, v) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
val changes = (
|
||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||
).map { index ->
|
||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
||||
mutable[index] = it.value
|
||||
}
|
||||
}
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
mutableOldList.applyDiff(mutable)
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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) as ByteArray
|
||||
|
||||
fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also {
|
||||
forEachIndexed { i, byte -> it.setInt8(i, byte) }
|
||||
}
|
||||
|
||||
fun ByteArray.toArrayBuffer() = toDataView().buffer
|
@@ -0,0 +1,61 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
|
||||
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, MutationObserverInit(childList = true, subtree = true))
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previousIntersectionRatio = -1f
|
||||
val observer = IntersectionObserver { entries, observer ->
|
||||
entries.forEach {
|
||||
if (previousIntersectionRatio != it.intersectionRatio) {
|
||||
previousIntersectionRatio = it.intersectionRatio.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
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.DOMRectReadOnly
|
||||
import org.w3c.dom.Element
|
||||
|
||||
external interface IntersectionObserverOptions {
|
||||
/**
|
||||
* 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: Array<Number>?
|
||||
}
|
||||
fun IntersectionObserverOptions(
|
||||
block: IntersectionObserverOptions.() -> Unit = {}
|
||||
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
|
||||
|
||||
external interface IntersectionObserverEntry {
|
||||
/**
|
||||
* 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: Number
|
||||
|
||||
/**
|
||||
* 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: Array<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) {
|
||||
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: Array<Number>
|
||||
|
||||
/**
|
||||
* 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(): Array<IntersectionObserverEntry>
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver to stop observing a particular target element.
|
||||
*/
|
||||
fun unobserve(targetElement: Element)
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.ErrorEvent
|
||||
import org.w3c.files.*
|
||||
import kotlin.js.Promise
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||
val reader = FileReader()
|
||||
reader.onload = {
|
||||
success((reader.result as ArrayBuffer).toByteArray())
|
||||
Unit
|
||||
}
|
||||
reader.onerror = {
|
||||
failure(Exception((it as ErrorEvent).message))
|
||||
Unit
|
||||
}
|
||||
reader.readAsArrayBuffer(this)
|
||||
}
|
||||
|
||||
fun MPPFile.readBytes(): ByteArray {
|
||||
val reader = FileReaderSync()
|
||||
return reader.readAsArrayBuffer(this).toByteArray()
|
||||
}
|
||||
|
||||
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = 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
|
@@ -0,0 +1,38 @@
|
||||
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
|
||||
|
||||
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
|
||||
lateinit var observer: MutationObserver
|
||||
val listener = EventListener {
|
||||
val elementsToCheck = mutableListOf<Element>(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)
|
||||
}
|
||||
}
|
||||
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: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
|
@@ -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()
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.coroutines.*
|
||||
import kotlin.js.Promise
|
||||
|
||||
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||
}
|
@@ -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()
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.doInIO
|
||||
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = length()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
doInIO {
|
||||
doOutsideOfCoroutine {
|
||||
readBytes()
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,23 +5,44 @@ import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.Transformation
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expand(
|
||||
private fun View.performExpand(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
onMeasured: View.() -> Unit,
|
||||
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||
) {
|
||||
measure(targetWidth, targetHeight)
|
||||
val measuredHeight: Int = measuredHeight
|
||||
layoutParams.height = 0
|
||||
visibility = View.VISIBLE
|
||||
onMeasured()
|
||||
show()
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
super.applyTransformation(interpolatedTime, t)
|
||||
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||
onPerformAnimation(interpolatedTime, t)
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean = true
|
||||
}
|
||||
|
||||
a.duration = duration
|
||||
startAnimation(a)
|
||||
}
|
||||
|
||||
private fun View.performCollapse(
|
||||
duration: Long = 500,
|
||||
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||
) {
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
if (interpolatedTime == 1f) {
|
||||
gone()
|
||||
} else {
|
||||
onPerformAnimation(interpolatedTime, t)
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean {
|
||||
return true
|
||||
}
|
||||
@@ -32,27 +53,58 @@ fun View.expand(
|
||||
startAnimation(a)
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expand(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
var measuredHeight = 0
|
||||
performExpand(
|
||||
duration,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
{
|
||||
measuredHeight = this.measuredHeight
|
||||
}
|
||||
) { interpolatedTime, _ ->
|
||||
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expandHorizontally(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
var measuredWidth = 0
|
||||
performExpand(
|
||||
duration,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
{
|
||||
measuredWidth = this.measuredWidth
|
||||
}
|
||||
) { interpolatedTime, _ ->
|
||||
layoutParams.width = if (interpolatedTime == 1f) targetWidth else (measuredWidth * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.collapse(duration: Long = 500) {
|
||||
val initialHeight: Int = measuredHeight
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
if (interpolatedTime == 1f) {
|
||||
visibility = View.GONE
|
||||
} else {
|
||||
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean {
|
||||
return true
|
||||
}
|
||||
performCollapse(duration) { interpolatedTime, _ ->
|
||||
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
a.duration = duration
|
||||
|
||||
startAnimation(a)
|
||||
@PreviewFeature
|
||||
fun View.collapseHorizontally(duration: Long = 500) {
|
||||
val initialWidth: Int = measuredWidth
|
||||
performCollapse(duration) { interpolatedTime, _ ->
|
||||
layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
@@ -74,3 +126,15 @@ fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||
collapse(duration)
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true in case of expanding
|
||||
*/
|
||||
@PreviewFeature
|
||||
fun View.toggleExpandHorizontallyState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||
expandHorizontally(duration)
|
||||
true
|
||||
} else {
|
||||
collapseHorizontally(duration)
|
||||
false
|
||||
}
|
||||
|
@@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
|
||||
if (show) {
|
||||
show()
|
||||
} else {
|
||||
if (goneOnHide) {
|
||||
gone()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,13 +10,18 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines.android
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
coroutines/compose/build.gradle
Normal file
20
coroutines/compose/build.gradle
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.coroutines")
|
||||
api project(":micro_utils.common.compose")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
fun <T> Flow<T>.toMutableState(
|
||||
initial: T,
|
||||
scope: CoroutineScope
|
||||
): MutableState<T> {
|
||||
val state = mutableStateOf(initial)
|
||||
subscribeSafelyWithoutExceptions(scope) { state.value = it }
|
||||
return state
|
||||
}
|
||||
|
||||
inline fun <T> StateFlow<T>.toMutableState(
|
||||
scope: CoroutineScope
|
||||
): MutableState<T> = toMutableState(value, scope)
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun Composition.linkWithJob(job: Job) {
|
||||
job.invokeOnCompletion {
|
||||
this@linkWithJob.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.micro_utils.common.compose.linkWithElement
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(
|
||||
currentCoroutineContext()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(currentCoroutineContext())
|
||||
linkWithElement(root)
|
||||
}
|
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
@@ -0,0 +1,94 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private sealed interface AccumulatorFlowStep
|
||||
private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep
|
||||
private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||
private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||
|
||||
/**
|
||||
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
||||
*
|
||||
* * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new
|
||||
* [FlowCollector]s until anybody will handle it
|
||||
* * Here there are an [activeData] where data [T] will be stored until somebody will handle it
|
||||
*/
|
||||
class AccumulatorFlow<T>(
|
||||
sourceDataFlow: Flow<T>,
|
||||
scope: CoroutineScope
|
||||
) : AbstractFlow<T>() {
|
||||
private val subscope = scope.LinkedSupervisorScope()
|
||||
private val activeData = ArrayDeque<T>()
|
||||
private val dataMutex = Mutex()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
|
||||
private val channelsMutex = Mutex()
|
||||
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
|
||||
when (step) {
|
||||
is DataRetrievedAccumulatorFlowStep -> {
|
||||
if (activeData.first() === step.data) {
|
||||
dataMutex.withLock {
|
||||
activeData.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.add(step.channel)
|
||||
dataMutex.withLock {
|
||||
val dataToSend = activeData.toList()
|
||||
safelyWithoutExceptions {
|
||||
dataToSend.forEach { step.channel.send(it as Any) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.remove(step.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) {
|
||||
dataMutex.withLock {
|
||||
activeData.addLast(it)
|
||||
}
|
||||
channelsMutex.withLock {
|
||||
channelsForBroadcast.forEach { channel ->
|
||||
safelyWithResult {
|
||||
channel.send(it as Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||
for (data in channel) {
|
||||
try {
|
||||
collector.emit(data as T)
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
} finally {
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] as base [Flow]
|
||||
*/
|
||||
fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return AccumulatorFlow(this, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get
|
||||
*/
|
||||
fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return receiveAsFlow().accumulatorFlow(scope)
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlin.coroutines.*
|
||||
|
||||
interface ActorAction<T> {
|
||||
suspend operator fun invoke(): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||
*
|
||||
* @see actor
|
||||
*/
|
||||
fun CoroutineScope.createActionsActor() = actor<suspend () -> Unit> {
|
||||
it()
|
||||
}
|
||||
|
||||
/**
|
||||
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||
*
|
||||
* @see safeActor
|
||||
*/
|
||||
inline fun CoroutineScope.createSafeActionsActor(
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler
|
||||
) = safeActor<suspend () -> Unit>(Channel.UNLIMITED, onException) {
|
||||
it()
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be use with actor created by [createActionsActor] or [createSafeActionsActor]. Will send lambda which will
|
||||
* execute [action] and return result.
|
||||
*
|
||||
* @see suspendCoroutine
|
||||
* @see safely
|
||||
*/
|
||||
suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending(
|
||||
action: ActorAction<T>
|
||||
) = suspendCoroutine<T> {
|
||||
trySend {
|
||||
safely({ e -> it.resumeWithException(e) }) {
|
||||
it.resume(action())
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@ fun <T> CoroutineScope.actor(
|
||||
|
||||
inline fun <T> CoroutineScope.safeActor(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
noinline onException: ExceptionHandler<Unit> = {},
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
crossinline block: suspend (T) -> Unit
|
||||
): Channel<T> = actor(
|
||||
channelCapacity
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.*
|
||||
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
||||
scope.launch(SupervisorJob()) {
|
||||
val scope = this
|
||||
forEach {
|
||||
scope.launch {
|
||||
continuation.resume(it to it.await())
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
if (cancelOnResult) {
|
||||
forEach {
|
||||
try {
|
||||
it.cancel()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): T = awaitFirstWithDeferred(scope, cancelOnResult).second
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||
cancelOthers: Boolean = true
|
||||
): T = awaitFirst(CoroutineScope(coroutineContext), cancelOthers)
|
@@ -0,0 +1,23 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
inline val UI
|
||||
get() = Dispatchers.Main
|
||||
inline val Default
|
||||
get() = Dispatchers.Default
|
||||
|
||||
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||
context,
|
||||
block
|
||||
)
|
||||
|
||||
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||
UI,
|
||||
block
|
||||
)
|
||||
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||
Default,
|
||||
block
|
||||
)
|
@@ -0,0 +1,85 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class DeferredAction<T, O>(
|
||||
val deferred: Deferred<T>,
|
||||
val callback: suspend (T) -> O
|
||||
) {
|
||||
suspend operator fun invoke() = callback(deferred.await())
|
||||
}
|
||||
|
||||
class DoWithFirstBuilder<T>(
|
||||
private val scope: CoroutineScope
|
||||
) {
|
||||
private val deferreds = mutableListOf<Deferred<T>>()
|
||||
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
||||
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
||||
}
|
||||
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
|
||||
fun build() = deferreds.toList()
|
||||
}
|
||||
|
||||
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
|
||||
|
||||
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): O {
|
||||
return map { it.deferred }.awaitFirstWithDeferred(scope, cancelOnResult).let { result ->
|
||||
first { it.deferred == result.first }.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <O> invokeFirstOf(
|
||||
scope: CoroutineScope,
|
||||
vararg variants: DeferredAction<*, O>,
|
||||
cancelOnResult: Boolean = true
|
||||
): O = variants.toList().invokeFirstOf(scope, cancelOnResult)
|
||||
|
||||
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)
|
||||
|
||||
suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
|
||||
cancelOnResult: Boolean = true,
|
||||
block: DoWithFirstBuilder<T>.() -> Unit,
|
||||
callback: suspend (T) -> O
|
||||
) = firstOf(
|
||||
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||
cancelOnResult
|
||||
).let { callback(it) }
|
||||
|
||||
suspend fun <T, O> invokeOnFirst(
|
||||
scope: CoroutineScope,
|
||||
vararg variants: Deferred<T>,
|
||||
cancelOnResult: Boolean = true,
|
||||
callback: suspend (T) -> O
|
||||
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
|
||||
|
||||
suspend fun <T> CoroutineScope.firstOf(
|
||||
variants: Iterable<Deferred<T>>,
|
||||
cancelOnResult: Boolean = true
|
||||
) = variants.invokeOnFirst(this, cancelOnResult) { it }
|
||||
|
||||
suspend fun <T> CoroutineScope.firstOf(
|
||||
cancelOnResult: Boolean = true,
|
||||
block: DoWithFirstBuilder<T>.() -> Unit
|
||||
) = firstOf(
|
||||
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||
cancelOnResult
|
||||
)
|
||||
|
||||
suspend fun <T> CoroutineScope.firstOf(
|
||||
vararg variants: Deferred<T>,
|
||||
cancelOnResult: Boolean = true
|
||||
) = firstOf(variants.toList(), cancelOnResult)
|
||||
|
||||
suspend fun <T> List<Deferred<T>>.first(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
) = scope.firstOf(this, cancelOnResult)
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!
|
@@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
||||
@@ -16,7 +18,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafely(
|
||||
scope: CoroutineScope,
|
||||
noinline onException: ExceptionHandler<Unit> = { throw it },
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribe(scope) {
|
||||
safely(onException) {
|
||||
@@ -25,13 +27,26 @@ inline fun <T> Flow<T>.subscribeSafely(
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||
scope: CoroutineScope,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeSafely(
|
||||
scope,
|
||||
{},
|
||||
block
|
||||
)
|
||||
) = subscribe(scope) {
|
||||
safelyWithoutExceptions(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
|
||||
scope: CoroutineScope,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribe(scope) {
|
||||
safelyWithoutExceptions({ /* skip exceptions */ }) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,118 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private class SubscribeAsyncReceiver<T>(
|
||||
val scope: CoroutineScope,
|
||||
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
|
||||
) {
|
||||
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
|
||||
val channel: SendChannel<T>
|
||||
get() = dataChannel
|
||||
|
||||
init {
|
||||
scope.launchSafelyWithoutExceptions {
|
||||
for (data in dataChannel) {
|
||||
output(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean = dataChannel.isEmpty
|
||||
}
|
||||
|
||||
private sealed interface AsyncSubscriptionCommand<T, M> {
|
||||
suspend operator fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>)
|
||||
}
|
||||
private data class AsyncSubscriptionCommandData<T, M>(
|
||||
val data: T,
|
||||
val scope: CoroutineScope,
|
||||
val markerFactory: suspend (T) -> M,
|
||||
val block: suspend (T) -> Unit,
|
||||
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) }
|
||||
if (isEmpty()) {
|
||||
onEmpty(marker)
|
||||
}
|
||||
}
|
||||
}.channel.send(data)
|
||||
}
|
||||
}
|
||||
|
||||
private data class AsyncSubscriptionCommandClearReceiver<T, M>(
|
||||
val marker: M
|
||||
) : AsyncSubscriptionCommand<T, M> {
|
||||
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||
val receiver = markersMap[marker]
|
||||
if (receiver ?.isEmpty() == true) {
|
||||
markersMap.remove(marker)
|
||||
receiver.scope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T, M> Flow<T>.subscribeAsync(
|
||||
scope: CoroutineScope,
|
||||
markerFactory: suspend (T) -> M,
|
||||
block: suspend (T) -> Unit
|
||||
): Job {
|
||||
val subscope = scope.LinkedSupervisorScope()
|
||||
val markersMap = mutableMapOf<M, SubscribeAsyncReceiver<T>>()
|
||||
val actor = subscope.actor<AsyncSubscriptionCommand<T, M>>(Channel.UNLIMITED) {
|
||||
it.invoke(markersMap)
|
||||
}
|
||||
|
||||
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
|
||||
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
||||
actor.send(
|
||||
AsyncSubscriptionCommandClearReceiver(marker)
|
||||
)
|
||||
}
|
||||
actor.send(dataCommand)
|
||||
}
|
||||
|
||||
job.invokeOnCompletion { if (subscope.isActive) subscope.cancel() }
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safelyWithoutExceptions(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safelyWithoutExceptions({ /* do nothing */}) {
|
||||
block(it)
|
||||
}
|
||||
}
|
@@ -1,30 +1,156 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
||||
*/
|
||||
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
||||
*/
|
||||
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
||||
try {
|
||||
defaultSafelyExceptionHandler(it)
|
||||
} catch (e: Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandler
|
||||
*/
|
||||
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
||||
|
||||
/**
|
||||
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
class ContextSafelyExceptionHandler(
|
||||
val handler: ExceptionHandler<Unit>
|
||||
) : CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<*>
|
||||
get() = ContextSafelyExceptionHandlerKey
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [ContextSafelyExceptionHandler] from [coroutineContext] by key [ContextSafelyExceptionHandlerKey] if
|
||||
* exists
|
||||
*
|
||||
* @see ContextSafelyExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
|
||||
|
||||
/**
|
||||
* This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext]
|
||||
* already contains [ContextSafelyExceptionHandler], [ContextSafelyExceptionHandler.handler] will be used BEFORE
|
||||
* [contextExceptionHandler] in case of exception.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
suspend fun <T> safelyWithContextExceptionHandler(
|
||||
contextExceptionHandler: ExceptionHandler<Unit>,
|
||||
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
val contextSafelyExceptionHandler = contextSafelyExceptionHandler() ?.handler ?.let { oldHandler ->
|
||||
ContextSafelyExceptionHandler {
|
||||
oldHandler(it)
|
||||
contextExceptionHandler(it)
|
||||
}
|
||||
} ?: ContextSafelyExceptionHandler(contextExceptionHandler)
|
||||
return withContext(contextSafelyExceptionHandler) {
|
||||
safely(safelyExceptionHandler, block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions
|
||||
*
|
||||
* Priorities of [ExceptionHandler]s:
|
||||
*
|
||||
* * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler])
|
||||
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||
* * [defaultSafelyExceptionHandler]
|
||||
*
|
||||
* Remember, that [ExceptionHandler] from [CoroutineContext.get] will be used anyway if it is available. After it will
|
||||
* be called [onException]
|
||||
*
|
||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||
* exception will be available for catching
|
||||
*
|
||||
* @see defaultSafelyExceptionHandler
|
||||
* @see safelyWithoutExceptions
|
||||
* @see safelyWithContextExceptionHandler
|
||||
*/
|
||||
suspend inline fun <T> safely(
|
||||
noinline onException: ExceptionHandler<T> = { throw it },
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
return try {
|
||||
supervisorScope(block)
|
||||
} catch (e: Throwable) {
|
||||
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
|
||||
onException(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <T> runCatchingSafely(
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T> = runCatching {
|
||||
safely(onException, block)
|
||||
}
|
||||
|
||||
suspend inline fun <T> safelyWithResult(
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
/**
|
||||
* Shortcut for [safely] without exception handler (instead of this you will receive null as a result)
|
||||
* Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and
|
||||
* returning null at one time
|
||||
*
|
||||
* @see safelyWithoutExceptions
|
||||
* @see launchSafelyWithoutExceptions
|
||||
* @see asyncSafelyWithoutExceptions
|
||||
*/
|
||||
val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
|
||||
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
suspend inline fun <T> safelyWithoutExceptions(
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T? = safely({ null }, block)
|
||||
): T? = safely(onException, block)
|
||||
|
||||
suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T?> = runCatching {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
}
|
||||
|
||||
inline fun CoroutineScope(
|
||||
context: CoroutineContext,
|
||||
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||
) = CoroutineScope(
|
||||
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
|
||||
)
|
||||
|
@@ -0,0 +1,41 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
inline fun CoroutineScope.launchSafely(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> Unit
|
||||
) = launch(context, start) {
|
||||
safely(onException, block)
|
||||
}
|
||||
|
||||
inline fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> Unit
|
||||
) = launch(context, start) {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.asyncSafely(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
) = async(context, start) {
|
||||
safely(onException, block)
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
) = async(context, start) {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun CoroutineContext.LinkedSupervisorJob(
|
||||
additionalContext: CoroutineContext? = null
|
||||
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
|
||||
fun CoroutineScope.LinkedSupervisorJob(
|
||||
additionalContext: CoroutineContext? = null
|
||||
) = coroutineContext.LinkedSupervisorJob(additionalContext)
|
||||
|
||||
fun CoroutineScope.LinkedSupervisorScope(
|
||||
additionalContext: CoroutineContext? = null
|
||||
) = CoroutineScope(
|
||||
coroutineContext + LinkedSupervisorJob(additionalContext)
|
||||
)
|
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope ->
|
||||
coroutineContext.job.invokeOnCompletion { newScope.cancel() }
|
||||
}
|
||||
|
||||
fun CoroutineScope.weakLaunch(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job {
|
||||
val scope = createWeakSubScope()
|
||||
val job = scope.launch(context, start, block)
|
||||
job.invokeOnCompletion { scope.cancel() }
|
||||
return job
|
||||
}
|
||||
|
||||
fun <T> CoroutineScope.weakAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Deferred<T> {
|
||||
val scope = createWeakSubScope()
|
||||
val deferred = scope.async(context, start, block)
|
||||
deferred.invokeOnCompletion { scope.cancel() }
|
||||
return deferred
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.await
|
||||
import org.khronos.webgl.Int8Array
|
||||
import org.w3c.fetch.Response
|
||||
import org.w3c.files.Blob
|
||||
|
||||
suspend fun Blob.toByteArray() = Int8Array(
|
||||
Response(this).arrayBuffer().await()
|
||||
) as ByteArray
|
@@ -0,0 +1,28 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import dev.inmo.micro_utils.common.onVisibilityChanged
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
|
||||
var previousData: Boolean? = null
|
||||
|
||||
val observer = onVisibilityChanged { intersectionRatio, _ ->
|
||||
val currentData = intersectionRatio > 0
|
||||
if (currentData != previousData) {
|
||||
trySend(currentData)
|
||||
}
|
||||
previousData = currentData
|
||||
}
|
||||
|
||||
val removeObserver = onRemoved {
|
||||
observer.disconnect()
|
||||
close()
|
||||
}
|
||||
|
||||
invokeOnClose {
|
||||
observer.disconnect()
|
||||
removeObserver.disconnect()
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.selectFile
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
|
||||
suspend fun selectFileOrThrow(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {}
|
||||
): MPPFile {
|
||||
val result = CompletableDeferred<MPPFile>()
|
||||
|
||||
selectFile(
|
||||
inputSetup,
|
||||
{
|
||||
result.completeExceptionally(it)
|
||||
}
|
||||
) {
|
||||
result.complete(it)
|
||||
}
|
||||
|
||||
return result.await()
|
||||
}
|
||||
|
||||
suspend fun selectFileOrNull(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||
onFailure: (Throwable) -> Unit = {}
|
||||
): MPPFile? {
|
||||
val result = CompletableDeferred<MPPFile?>()
|
||||
|
||||
selectFile(
|
||||
inputSetup,
|
||||
{
|
||||
result.complete(null)
|
||||
onFailure(it)
|
||||
}
|
||||
) {
|
||||
result.complete(it)
|
||||
}
|
||||
|
||||
return result.await()
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
val IO
|
||||
get() = Dispatchers.IO
|
||||
|
||||
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||
IO,
|
||||
block
|
||||
)
|
@@ -3,28 +3,24 @@ package dev.inmo.micro_utils.coroutines
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||
var throwable: Throwable? = null
|
||||
var result: T? = null
|
||||
val objectToSynchronize = java.lang.Object()
|
||||
val launchCallback = {
|
||||
var result: Result<T>? = null
|
||||
val objectToSynchronize = Object()
|
||||
synchronized(objectToSynchronize) {
|
||||
launch {
|
||||
safely(
|
||||
{
|
||||
throwable = it
|
||||
}
|
||||
) {
|
||||
result = block()
|
||||
}
|
||||
result = safelyWithResult(block)
|
||||
}.invokeOnCompletion {
|
||||
synchronized(objectToSynchronize) {
|
||||
objectToSynchronize.notifyAll()
|
||||
}
|
||||
}
|
||||
while (result == null) {
|
||||
objectToSynchronize.wait()
|
||||
}
|
||||
}
|
||||
synchronized(objectToSynchronize) {
|
||||
launchCallback()
|
||||
objectToSynchronize.wait()
|
||||
}
|
||||
throw throwable ?: return result!!
|
||||
return result!!.getOrThrow()
|
||||
}
|
||||
|
||||
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
||||
|
||||
fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||
fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||
|
@@ -0,0 +1,34 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AwaitFirstTests {
|
||||
private fun CoroutineScope.createTestDeferred(value: Int, wait: Long = 100000) = async(start = CoroutineStart.LAZY) { delay(wait); value }
|
||||
@Test
|
||||
fun testThatAwaitFirstIsWorkingCorrectly() {
|
||||
val baseScope = CoroutineScope(Dispatchers.Default)
|
||||
val resultDeferred = baseScope.createTestDeferred(-1, 0)
|
||||
val deferreds = listOf(
|
||||
baseScope.async { createTestDeferred(0) },
|
||||
baseScope.async { createTestDeferred(1) },
|
||||
baseScope.async { createTestDeferred(2) },
|
||||
resultDeferred
|
||||
)
|
||||
val controlJob = baseScope.launch {
|
||||
delay(1000000)
|
||||
}
|
||||
val result = baseScope.launchSynchronously {
|
||||
val result = deferreds.awaitFirst(baseScope)
|
||||
|
||||
assertTrue(baseScope.isActive)
|
||||
assertTrue(controlJob.isActive)
|
||||
|
||||
result
|
||||
}
|
||||
assertEquals(baseScope.launchSynchronously { resultDeferred.await() }, result)
|
||||
assertTrue(deferreds.all { it == resultDeferred || it.isCancelled })
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DoWithFirstTests {
|
||||
@Test
|
||||
fun testHandleOneOf() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
val happenedDeferreds = mutableListOf<Int>()
|
||||
val deferredWhichMustHappen = (-1).asDeferred
|
||||
scope.launchSynchronously {
|
||||
scope.launch {
|
||||
((0 until 100).map {
|
||||
DeferredAction(
|
||||
scope.async { delay(10000); it },
|
||||
happenedDeferreds::add
|
||||
)
|
||||
} + DeferredAction(deferredWhichMustHappen, happenedDeferreds::add)).invokeFirstOf(scope)
|
||||
}.join()
|
||||
}
|
||||
assertEquals(1, happenedDeferreds.size)
|
||||
assertEquals(scope.launchSynchronously { deferredWhichMustHappen.await() }, happenedDeferreds.first())
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.test.Test
|
||||
|
||||
class HandleSafelyCoroutineContextTest {
|
||||
@Test
|
||||
fun testHandleSafelyCoroutineContext() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
var contextHandlerHappen = false
|
||||
var localHandlerHappen = false
|
||||
var defaultHandlerHappen = false
|
||||
defaultSafelyExceptionHandler = {
|
||||
defaultHandlerHappen = true
|
||||
throw it
|
||||
}
|
||||
val contextHandler: ExceptionHandler<Unit> = {
|
||||
contextHandlerHappen = true
|
||||
}
|
||||
val checkJob = scope.launch {
|
||||
safelyWithContextExceptionHandler(contextHandler) {
|
||||
safely(
|
||||
{
|
||||
localHandlerHappen = true
|
||||
}
|
||||
) {
|
||||
error("That must happen :)")
|
||||
}
|
||||
println(coroutineContext)
|
||||
error("That must happen too:)")
|
||||
}
|
||||
}
|
||||
launchSynchronously { checkJob.join() }
|
||||
assert(contextHandlerHappen)
|
||||
assert(localHandlerHappen)
|
||||
assert(defaultHandlerHappen)
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Test
|
||||
|
||||
class WeakJob {
|
||||
@Test
|
||||
fun `test that weak jobs works correctly`() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
lateinit var weakLaunchJob: Job
|
||||
lateinit var weakAsyncJob: Job
|
||||
scope.launchSynchronously {
|
||||
val completeDeferred = Job()
|
||||
coroutineScope {
|
||||
weakLaunchJob = weakLaunch {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
weakAsyncJob = weakAsync {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineContext.job.invokeOnCompletion {
|
||||
scope.launch {
|
||||
delay(1000L)
|
||||
completeDeferred.complete()
|
||||
}
|
||||
}
|
||||
launch { delay(1000L); cancel() }
|
||||
}
|
||||
completeDeferred.join()
|
||||
}
|
||||
|
||||
assert(!weakLaunchJob.isActive)
|
||||
assert(!weakAsyncJob.isActive)
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||
Dispatchers.Main,
|
||||
block
|
||||
)
|
30
crypto/build.gradle
Normal file
30
crypto/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api npm("crypto-js", "$crypto_js_version")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs += [ "src/jvmMain/kotlin" ]
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.experimental.and
|
||||
|
||||
private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
private const val BASE64_MASK: Byte = 0x3f
|
||||
private const val BASE64_PAD = '='
|
||||
private val BASE64_INVERSE_ALPHABET = IntArray(256) {
|
||||
BASE64_ALPHABET.indexOf(it.toChar())
|
||||
}
|
||||
|
||||
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
|
||||
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
|
||||
|
||||
typealias EncodedBase64String = String
|
||||
typealias EncodedByteArray = ByteArray
|
||||
|
||||
fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
|
||||
fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
|
||||
|
||||
fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
|
||||
var i = 0
|
||||
while (this@encodeBase64String.size > i) {
|
||||
val read = kotlin.math.min(3, this@encodeBase64String.size - i)
|
||||
val data = ByteArray(3) {
|
||||
if (it < read) {
|
||||
this@encodeBase64String[it + i]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
val padSize = (data.size - read) * 8 / 6
|
||||
val chunk = ((data[0].toInt() and 0xFF) shl 16) or
|
||||
((data[1].toInt() and 0xFF) shl 8) or
|
||||
(data[2].toInt() and 0xFF)
|
||||
|
||||
for (index in data.size downTo padSize) {
|
||||
val char = (chunk shr (6 * index)) and BASE64_MASK.toInt()
|
||||
append(char.toBase64())
|
||||
}
|
||||
|
||||
repeat(padSize) { append(BASE64_PAD) }
|
||||
|
||||
i += read
|
||||
}
|
||||
}
|
||||
fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
|
||||
|
||||
fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
|
||||
fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString()
|
||||
|
||||
fun EncodedByteArray.decodeBase64(): SourceBytes {
|
||||
val result = mutableListOf<Byte>()
|
||||
val data = ByteArray(4)
|
||||
(0 until size step 4).forEach { i ->
|
||||
var read = 0
|
||||
for (j in 0 until 4) {
|
||||
if (j + i < size) {
|
||||
data[j] = get(j + i)
|
||||
read++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
val chunk = data.foldIndexed(0) { index, result, current ->
|
||||
result or (current.fromBase64().toInt() shl ((3 - index) * 6))
|
||||
}
|
||||
for (index in data.size - 2 downTo (data.size - read)) {
|
||||
val origin = (chunk shr (8 * index)) and 0xff
|
||||
result.add(origin.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString()
|
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
/**
|
||||
* This typealias shows that corresponding [ByteArray] is an raw data
|
||||
*/
|
||||
typealias SourceBytes = ByteArray
|
||||
/**
|
||||
* This typealias shows that corresponding [String] is an raw data
|
||||
*/
|
||||
typealias SourceString = String
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
typealias MD5 = String
|
||||
|
||||
expect fun SourceBytes.md5(): MD5
|
||||
fun SourceString.md5(): MD5 = encodeToByteArray().md5()
|
@@ -0,0 +1,32 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
||||
|
||||
class Base64Text {
|
||||
|
||||
val testText = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"""
|
||||
val base64Text = """TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpTZWQgdXQgcGVyc3BpY2lhdGlzIHVuZGUgb21uaXMgaXN0ZSBuYXR1cyBlcnJvciBzaXQgdm9sdXB0YXRlbSBhY2N1c2FudGl1bSBkb2xvcmVtcXVlIGxhdWRhbnRpdW0sIHRvdGFtIHJlbSBhcGVyaWFtLCBlYXF1ZSBpcHNhIHF1YWUgYWIgaWxsbyBpbnZlbnRvcmUgdmVyaXRhdGlzIGV0IHF1YXNpIGFyY2hpdGVjdG8gYmVhdGFlIHZpdGFlIGRpY3RhIHN1bnQgZXhwbGljYWJvLiBOZW1vIGVuaW0gaXBzYW0gdm9sdXB0YXRlbSBxdWlhIHZvbHVwdGFzIHNpdCBhc3Blcm5hdHVyIGF1dCBvZGl0IGF1dCBmdWdpdCwgc2VkIHF1aWEgY29uc2VxdXVudHVyIG1hZ25pIGRvbG9yZXMgZW9zIHF1aSByYXRpb25lIHZvbHVwdGF0ZW0gc2VxdWkgbmVzY2l1bnQuIE5lcXVlIHBvcnJvIHF1aXNxdWFtIGVzdCwgcXVpIGRvbG9yZW0gaXBzdW0gcXVpYSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIsIGFkaXBpc2NpIHZlbGl0LCBzZWQgcXVpYSBub24gbnVtcXVhbSBlaXVzIG1vZGkgdGVtcG9yYSBpbmNpZHVudCB1dCBsYWJvcmUgZXQgZG9sb3JlIG1hZ25hbSBhbGlxdWFtIHF1YWVyYXQgdm9sdXB0YXRlbS4gVXQgZW5pbSBhZCBtaW5pbWEgdmVuaWFtLCBxdWlzIG5vc3RydW0gZXhlcmNpdGF0aW9uZW0gdWxsYW0gY29ycG9yaXMgc3VzY2lwaXQgbGFib3Jpb3NhbSwgbmlzaSB1dCBhbGlxdWlkIGV4IGVhIGNvbW1vZGkgY29uc2VxdWF0dXI/IFF1aXMgYXV0ZW0gdmVsIGV1bSBpdXJlIHJlcHJlaGVuZGVyaXQgcXVpIGluIGVhIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIHF1YW0gbmloaWwgbW9sZXN0aWFlIGNvbnNlcXVhdHVyLCB2ZWwgaWxsdW0gcXVpIGRvbG9yZW0gZXVtIGZ1Z2lhdCBxdW8gdm9sdXB0YXMgbnVsbGEgcGFyaWF0dXI/"""
|
||||
|
||||
@Test
|
||||
fun decodeEncode() {
|
||||
val encoded = testText.encodeBase64String()
|
||||
assertEquals(base64Text, encoded)
|
||||
val decoded = encoded.decodeBase64String()
|
||||
assertEquals(testText, decoded)
|
||||
}
|
||||
|
||||
val urlTestText = "https://example.com?isitexample=1"
|
||||
val urlTestBase64Text = "aHR0cHM6Ly9leGFtcGxlLmNvbT9pc2l0ZXhhbXBsZT0x"
|
||||
|
||||
@Test
|
||||
fun decodeEncodeUrl() {
|
||||
val encoded = urlTestText.encodeBase64String()
|
||||
assertEquals(urlTestBase64Text, encoded)
|
||||
val decoded = encoded.decodeBase64String()
|
||||
assertEquals(urlTestText, decoded)
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MD5Test {
|
||||
val source = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
val expectedMD5 = "db89bb5ceab87f9c0fcc2ab36c189c2c"
|
||||
@Test
|
||||
fun testMD5() {
|
||||
var i = 0
|
||||
source.md5().forEach {
|
||||
assertEquals(expectedMD5[i], it)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
external interface CryptoJs {
|
||||
fun MD5(data: String): String
|
||||
}
|
||||
|
||||
@JsModule("crypto-js")
|
||||
@JsNonModule
|
||||
external val CryptoJS: CryptoJs
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
||||
1,
|
||||
MessageDigest.getInstance("MD5").digest(this)
|
||||
).toString(16)
|
1
crypto/src/main/AndroidManifest.xml
Normal file
1
crypto/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.crypto"/>
|
@@ -1,40 +0,0 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
android {
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
}
|
67
defaultAndroidSettings.gradle
Normal file
67
defaultAndroidSettings.gradle
Normal file
@@ -0,0 +1,67 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
ext {
|
||||
jvmKotlinFolderFile = {
|
||||
String sep = File.separator
|
||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
if (jvmKotlinFolder.exists()) {
|
||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||
android.sourceSets.main.java.srcDirs = []
|
||||
for (oldDir in oldDirs) {
|
||||
if (oldDir != jvmKotlinFolder.path) {
|
||||
android.sourceSets.main.java.srcDirs += oldDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sep = File.separator
|
||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||
enableIncludingJvmCodeInAndroidPart()
|
||||
}
|
||||
}
|
@@ -7,17 +7,16 @@ plugins {
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(BOTH) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
// js(IR) {
|
||||
// browser()
|
||||
// nodejs()
|
||||
// }
|
||||
android {}
|
||||
|
||||
sourceSets {
|
||||
@@ -30,7 +29,7 @@ kotlin {
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
@@ -39,22 +38,22 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
// jsMain {
|
||||
// dependencies {
|
||||
// implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// project.parent.subprojects.forEach {
|
||||
// if (
|
||||
// it != project
|
||||
// && it.hasProperty("kotlin")
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
// ) {
|
||||
// api it
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
@@ -117,9 +116,9 @@ tasks.dokkaHtml {
|
||||
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
||||
}
|
||||
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
}
|
||||
// named("jsMain") {
|
||||
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
// }
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
|
||||
|
@@ -20,11 +20,12 @@ allprojects {
|
||||
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject"
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings"
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
||||
|
18
fsm/common/build.gradle
Normal file
18
fsm/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Define checkable holder which can be used to precheck that this handler may handle incoming [State]
|
||||
*/
|
||||
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
|
||||
suspend fun checkHandleable(state: O): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
|
||||
* casting in [handleState]
|
||||
*/
|
||||
class CustomizableHandlerHolder<I : O, O : State>(
|
||||
private val delegateTo: StatesHandler<I, O>,
|
||||
private val filter: suspend (state: O) -> Boolean
|
||||
) : CheckableHandlerHolder<I, O> {
|
||||
/**
|
||||
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
|
||||
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
|
||||
*/
|
||||
override suspend fun checkHandleable(state: O) = filter(state)
|
||||
|
||||
/**
|
||||
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
|
||||
* to be sure that this [StatesHandlerHolder] will be able to handle [state]
|
||||
*/
|
||||
override suspend fun StatesMachine<in O>.handleState(state: I): O? {
|
||||
return delegateTo.run { handleState(state) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <I : O, O : State> CheckableHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CustomizableHandlerHolder(
|
||||
StatesHandler<O, O> {
|
||||
delegateTo.run { handleState(it as I) }
|
||||
},
|
||||
if (strict) {
|
||||
{ it::class == inputKlass }
|
||||
} else {
|
||||
{ inputKlass.isInstance(it) }
|
||||
}
|
||||
)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
|
||||
fun <I : O, O : State> StateHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(inputKlass, strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O : State> CheckableHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(I::class, strict, delegateTo)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
|
||||
inline fun <reified I : O, O : State> StateHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
|
||||
strict: Boolean = true
|
||||
) = CheckableHandlerHolder<I, O>(
|
||||
I::class,
|
||||
strict,
|
||||
this
|
||||
)
|
||||
|
||||
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
|
||||
noinline filter: suspend (state: State) -> Boolean
|
||||
) = CustomizableHandlerHolder<O, O>(
|
||||
{ this@holder.run { handleState(it as I) } },
|
||||
filter
|
||||
)
|
@@ -0,0 +1,5 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
interface State {
|
||||
val context: Any
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
/**
|
||||
* Default realization of states handler
|
||||
*/
|
||||
fun interface StatesHandler<I : State, O: State> {
|
||||
/**
|
||||
* Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
|
||||
* [State] it is assumed that chain is not completed.
|
||||
*/
|
||||
suspend fun StatesMachine<in O>.handleState(state: I): O?
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.Optional
|
||||
import dev.inmo.micro_utils.common.onPresented
|
||||
import dev.inmo.micro_utils.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
|
||||
* [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
|
||||
* handling until [start] method will be called
|
||||
*/
|
||||
interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||
suspend fun launchStateHandling(
|
||||
state: T,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
): T? {
|
||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts handling of [State]s
|
||||
*/
|
||||
fun start(scope: CoroutineScope): Job
|
||||
|
||||
/**
|
||||
* Start chain of [State]s witn [state]
|
||||
*/
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates [DefaultStatesMachine]
|
||||
*/
|
||||
operator fun <T: State> invoke(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
) = DefaultStatesMachine(statesManager, handlers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
|
||||
* resolving, and uses [launchStateHandling] for [State] handling.
|
||||
*
|
||||
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
|
||||
*/
|
||||
open class DefaultStatesMachine <T: State>(
|
||||
protected val statesManager: StatesManager<T>,
|
||||
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
) : StatesMachine<T> {
|
||||
/**
|
||||
* Will call [launchStateHandling] for state handling
|
||||
*/
|
||||
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
||||
|
||||
/**
|
||||
* This
|
||||
*/
|
||||
protected val statesJobs = mutableMapOf<T, Job>()
|
||||
protected val statesJobsMutex = Mutex()
|
||||
|
||||
protected open suspend fun performUpdate(state: T) {
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState != null) {
|
||||
statesManager.update(state, newState)
|
||||
} else {
|
||||
statesManager.endChain(state)
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
statesJobs[actualState] = scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
if (statesJobs[actualState] == job) {
|
||||
statesJobs.remove(actualState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
|
||||
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
|
||||
* [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise
|
||||
* [StatesManager.endChain].
|
||||
*/
|
||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
|
||||
launch {
|
||||
statesJobsMutex.withLock {
|
||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
||||
if (stateInMap === removedState) {
|
||||
statesJobs[stateInMap] ?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statesManager.getActiveStates().forEach {
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just calls [StatesManager.startChain] of [statesManager]
|
||||
*/
|
||||
override suspend fun startChain(state: T) {
|
||||
statesManager.startChain(state)
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface StatesManager<T : State> {
|
||||
val onChainStateUpdated: Flow<Pair<T, T>>
|
||||
val onStartChain: Flow<T>
|
||||
val onEndChain: Flow<T>
|
||||
|
||||
|
||||
/**
|
||||
* Must set current set using [State.context]
|
||||
*/
|
||||
suspend fun update(old: T, new: T)
|
||||
|
||||
/**
|
||||
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
|
||||
* busy by the other [State]
|
||||
*/
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
/**
|
||||
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
|
||||
* ignored
|
||||
*/
|
||||
suspend fun endChain(state: T)
|
||||
|
||||
suspend fun getActiveStates(): List<T>
|
||||
}
|
||||
|
@@ -0,0 +1,72 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
|
||||
* must be able to perform update of chain in internal [StatesManager]
|
||||
*/
|
||||
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
|
||||
/**
|
||||
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
|
||||
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
|
||||
* [StatesManager.update] function
|
||||
*/
|
||||
suspend fun updateChain(currentState: T, newState: T)
|
||||
}
|
||||
|
||||
open class DefaultUpdatableStatesMachine<T : State>(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
) : DefaultStatesMachine<T>(
|
||||
statesManager,
|
||||
handlers
|
||||
), UpdatableStatesMachine<T> {
|
||||
protected val jobsStates = mutableMapOf<Job, T>()
|
||||
|
||||
/**
|
||||
* Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if
|
||||
* [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In
|
||||
* other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of
|
||||
* states is solved in [shouldReplaceJob] and can be rewritten in subclasses
|
||||
*/
|
||||
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
if (compare(previousState, actualState)) {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
}
|
||||
val job = previousState.mapOnPresented {
|
||||
statesJobs.remove(it)
|
||||
} ?.takeIf { it.isActive } ?: scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs.remove(
|
||||
jobsStates[job] ?: return@withLock
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobsStates.remove(job)
|
||||
statesJobs[actualState] = job
|
||||
jobsStates[job] = actualState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare if [previous] potentially lead to the same behaviour with [new]
|
||||
*/
|
||||
protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
|
||||
|
||||
@Deprecated("Overwrite shouldReplaceJob instead")
|
||||
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new)
|
||||
|
||||
override suspend fun updateChain(currentState: T, newState: T) {
|
||||
statesManager.update(currentState, newState)
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package dev.inmo.micro_utils.fsm.common.dsl
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class FSMBuilder<T : State>(
|
||||
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
|
||||
val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
|
||||
StatesMachine(
|
||||
statesManager,
|
||||
states
|
||||
)
|
||||
},
|
||||
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
|
||||
) {
|
||||
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
|
||||
|
||||
fun add(handler: CheckableHandlerHolder<T, T>) {
|
||||
states.add(handler)
|
||||
}
|
||||
|
||||
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||
add(CheckableHandlerHolder(kClass, false, handler))
|
||||
}
|
||||
|
||||
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
|
||||
add(handler.holder(filter))
|
||||
}
|
||||
|
||||
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||
states.add(CheckableHandlerHolder(kClass, true, handler))
|
||||
}
|
||||
|
||||
inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) {
|
||||
add(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) {
|
||||
addStrict(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : T> doWhen(
|
||||
noinline filter: suspend (state: State) -> Boolean,
|
||||
handler: StatesHandler<I, T>
|
||||
) {
|
||||
add(filter, handler)
|
||||
}
|
||||
|
||||
fun build() = fsmBuilder(
|
||||
statesManager,
|
||||
states.toList().let { list ->
|
||||
defaultStateHandler ?.let { list + it.holder { true } } ?: list
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun <T : State> buildFSM(
|
||||
block: FSMBuilder<T>.() -> Unit
|
||||
): StatesMachine<T> = FSMBuilder<T>().apply(block).build()
|
@@ -0,0 +1,118 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Implement this repo if you want to use some custom repo for [DefaultStatesManager]
|
||||
*/
|
||||
interface DefaultStatesManagerRepo<T : State> {
|
||||
/**
|
||||
* Must save [state] as current state of chain with [State.context] of [state]
|
||||
*/
|
||||
suspend fun set(state: T)
|
||||
/**
|
||||
* Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should
|
||||
* NOT be removed
|
||||
*/
|
||||
suspend fun removeState(state: T)
|
||||
/**
|
||||
* @return Current list of available and saved states
|
||||
*/
|
||||
suspend fun getStates(): List<T>
|
||||
|
||||
/**
|
||||
* @return Current state by [context]
|
||||
*/
|
||||
suspend fun getContextState(context: Any): T?
|
||||
|
||||
/**
|
||||
* @return Current state by [context]
|
||||
*/
|
||||
suspend fun contains(context: Any): Boolean = getContextState(context) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
|
||||
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used
|
||||
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
|
||||
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
|
||||
* this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
|
||||
* with [endChain] with current state
|
||||
* @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
open class DefaultStatesManager<T : State>(
|
||||
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager<T> {
|
||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||
protected val _onStartChain = MutableSharedFlow<T>(0)
|
||||
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
||||
protected val _onEndChain = MutableSharedFlow<T>(0)
|
||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||
|
||||
protected val mapMutex = Mutex()
|
||||
|
||||
constructor(
|
||||
repo: DefaultStatesManagerRepo<T>,
|
||||
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
|
||||
) : this (
|
||||
repo,
|
||||
onUpdateContextsConflictResolver = onContextsConflictResolver
|
||||
)
|
||||
|
||||
override suspend fun update(old: T, new: T) = mapMutex.withLock {
|
||||
val stateByOldContext: T? = repo.getContextState(old.context)
|
||||
when {
|
||||
stateByOldContext != old -> return@withLock
|
||||
stateByOldContext == null || old.context == new.context -> {
|
||||
repo.removeState(old)
|
||||
repo.set(new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = repo.getContextState(new.context)
|
||||
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||
repo.removeState(old)
|
||||
repo.set(new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
||||
val stateOnContext = repo.getContextState(state.context)
|
||||
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
||||
stateOnContext ?.let {
|
||||
endChainWithoutLock(it)
|
||||
}
|
||||
repo.set(state)
|
||||
_onStartChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun endChainWithoutLock(state: T) {
|
||||
if (repo.getContextState(state.context) == state) {
|
||||
repo.removeState(state)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: T) {
|
||||
mapMutex.withLock {
|
||||
endChainWithoutLock(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<T> = repo.getStates()
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
|
||||
/**
|
||||
* Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive
|
||||
* functionality
|
||||
*/
|
||||
class InMemoryDefaultStatesManagerRepo<T : State>(
|
||||
private val map: MutableMap<Any, T> = mutableMapOf()
|
||||
) : DefaultStatesManagerRepo<T> {
|
||||
override suspend fun set(state: T) {
|
||||
map[state.context] = state
|
||||
}
|
||||
|
||||
override suspend fun removeState(state: T) {
|
||||
map.remove(state.context)
|
||||
}
|
||||
|
||||
override suspend fun getStates(): List<T> = map.values.toList()
|
||||
|
||||
override suspend fun getContextState(context: Any): T? = map[context]
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = map.contains(context)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
|
||||
*
|
||||
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
||||
fun <T: State> InMemoryStatesManager(
|
||||
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
|
||||
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)
|
54
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
54
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
sealed interface TrafficLightState : State {
|
||||
val trafficLightNumber: Int
|
||||
override val context: Int
|
||||
get() = trafficLightNumber
|
||||
}
|
||||
data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||
data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||
data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||
|
||||
class PlayableMain {
|
||||
// @Test
|
||||
fun test() {
|
||||
runBlocking {
|
||||
val countOfTrafficLights = 10
|
||||
val initialStates = (0 until countOfTrafficLights).map {
|
||||
when (0/*Random.nextInt(3)*/) {
|
||||
0 -> GreenCommon(it)
|
||||
1 -> YellowCommon(it)
|
||||
else -> RedCommon(it)
|
||||
}
|
||||
}
|
||||
|
||||
val statesManager = DefaultStatesManager<TrafficLightState>()
|
||||
|
||||
val machine = buildFSM<TrafficLightState> {
|
||||
strictlyOn<GreenCommon> {
|
||||
delay(1000L)
|
||||
YellowCommon(it.context).also(::println)
|
||||
}
|
||||
strictlyOn<YellowCommon> {
|
||||
delay(1000L)
|
||||
RedCommon(it.context).also(::println)
|
||||
}
|
||||
strictlyOn<RedCommon> {
|
||||
delay(1000L)
|
||||
GreenCommon(it.context).also(::println)
|
||||
}
|
||||
this.statesManager = statesManager
|
||||
}
|
||||
|
||||
initialStates.forEach { machine.startChain(it) }
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
machine.start(scope).join()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
1
fsm/common/src/main/AndroidManifest.xml
Normal file
1
fsm/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user