mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
237 Commits
Author | SHA1 | Date | |
---|---|---|---|
6331f13e9a | |||
5213a2ff8e | |||
087d7452fd | |||
703094c924 | |||
eea645c865 | |||
324832a189 | |||
d55d735c51 | |||
e3ff1b9609 | |||
70c31966ca | |||
0e4188882f | |||
6bf0ce92ba | |||
d51bdc5086 | |||
e5dd4363f1 | |||
a3a48bbaac | |||
5e716fb9a8 | |||
11a36153cc | |||
8bee354f04 | |||
f7dd2b5ce7 | |||
8ca10c00bb | |||
905c7e8eda | |||
d4c5e849bf | |||
8250a2a021 | |||
01b3df7b8c | |||
daa6e4aff5 | |||
23bcb26a58 | |||
e55f60c30b | |||
0d0c16e16d | |||
540d5cce7c | |||
a548b00979 | |||
4b7ca6d565 | |||
0473fa238c | |||
cfc7119697 | |||
22a6520d3e | |||
fb25e91191 | |||
c116b270b6 | |||
aa2d598689 | |||
5ef3bb746b | |||
037616e271 | |||
abbea906f1 | |||
9132e216c9 | |||
12a7e3c4af | |||
b40c093917 | |||
7ac12455c8 | |||
5043eec7a2 | |||
cf31f53e01 | |||
cd22d76fa7 | |||
c8759843f7 | |||
781bbcc012 | |||
6da29c0686 | |||
fcdb6fc45a | |||
e785a99bd7 | |||
121e513fdd | |||
9cf01ab54f | |||
c655107681 | |||
91a5af6a9a | |||
86e74c0a6f | |||
d8f01f21a0 | |||
9fb8626d8c | |||
1c52e04cdb | |||
798128256e | |||
72c2df47fd | |||
c9c6d4c0c1 | |||
2f4f9f3003 | |||
22e8f8e5d6 | |||
04cf8c3d9a | |||
52198be543 | |||
80fd5a489b | |||
b187043ee1 | |||
8965752055 | |||
b796620267 | |||
62df81bb4e | |||
0a8e0f6178 | |||
f705020aaa | |||
78bd3b9853 | |||
992b283597 | |||
8fc1ff1d59 | |||
3bbde61f39 | |||
8d955c4b9d | |||
18593c530b | |||
78903cd4eb | |||
eaa143f7d7 | |||
bcb0e42fa2 | |||
8eed435302 | |||
0e4a63057f | |||
1af5faa440 | |||
7412217b0c | |||
3e749d75b7 | |||
846b5c87c9 | |||
7b5e84f80b | |||
27822a8a66 | |||
36f4e7ec37 | |||
187e84ad65 | |||
195fe221c4 | |||
6f9c19bbf6 | |||
65bdab4f7e | |||
c4d5fcfc22 | |||
43232afa62 | |||
18908a01d7 | |||
b22fbfb3bc | |||
57aaea88b6 | |||
140949c5ea | |||
639241e0d3 | |||
e0eb42bc2d | |||
7990b21cc5 | |||
2ba5c97709 | |||
33b7c85fc2 | |||
7f6c02ffdf | |||
f368616e6f | |||
dd632f4203 | |||
a8f3ae501b | |||
ea76963ac2 | |||
99dfa97958 | |||
f68270a5b3 | |||
542ed81034 | |||
404a11f5e7 | |||
411221070e | |||
25d35d0c76 | |||
c72904d61c | |||
b94c9acd26 | |||
bdb4988569 | |||
1dafd1352a | |||
31fbd3cad7 | |||
848bc5ec10 | |||
4d0ad826a0 | |||
6955820fcc | |||
ef530624b9 | |||
9b9e7dd88f | |||
a13cc9e961 | |||
0d2b923378 | |||
fba84c8ac8 | |||
db10fe1b2c | |||
175dd980f8 | |||
8364020671 | |||
eba44cd394 | |||
b3bac8015a | |||
0b48afd251 | |||
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 |
2
.github/workflows/dokka_push.yml
vendored
2
.github/workflows/dokka_push.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
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
|
||||
run: ./gradlew build && ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
|
2
.github/workflows/packages_push.yml
vendored
2
.github/workflows/packages_push.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
continue-on-error: true
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
|
||||
env:
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
351
CHANGELOG.md
351
CHANGELOG.md
@@ -1,5 +1,356 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.14
|
||||
|
||||
* `Pagination`:
|
||||
* `PaginationResult` got new field `objectsNumber` which by default is a times between `pagesNumber` and `size`
|
||||
|
||||
## 0.11.13
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.3` -> `1.6.4`
|
||||
* `Compose`: `1.2.0-alpha01-dev629` -> `1.2.0-alpha01-dev731`
|
||||
|
||||
## 0.11.12
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* `JVM`:
|
||||
* Fixes in `ReadFileKeyValueRepo` methods (`values`/`keys`)
|
||||
|
||||
## 0.11.11
|
||||
|
||||
* `Crypto`:
|
||||
* `hmacSha256` has been deprecated
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* `BodyPair` has been deprecated
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* New interface `CacheRepo`
|
||||
* New interface `FullCacheRepo`
|
||||
* `actualize*` methods inside of full cache repos now open for overriding
|
||||
|
||||
## 0.11.10
|
||||
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* `KVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache`
|
||||
* `SimpleKVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache`
|
||||
* New `KVCache` subtype - `FullKVCache`
|
||||
* Add `Full*` variants of standard repos
|
||||
* Add `cached`/`caching` (for write repos) extensions for all standard types of repos
|
||||
|
||||
## 0.11.9
|
||||
|
||||
* `Versions`
|
||||
* `Coroutines`: `1.6.1` -> `1.6.3`
|
||||
* `Ktor`: `2.0.2` -> `2.0.3`
|
||||
* `Compose`: `1.2.0-alpha01-dev686` -> `1.2.0-alpha01-dev729`
|
||||
|
||||
## 0.11.8
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Fixes in `FileKeyValueRepo`
|
||||
|
||||
## 0.11.7
|
||||
|
||||
* `Common`:
|
||||
* New abstractions `SimpleMapper` and `SimpleSuspendableMapper`
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Add mappers for `CRUDRepo`
|
||||
|
||||
## 0.11.6
|
||||
|
||||
* `FSM`:
|
||||
* `Common`
|
||||
* Several fixes related to the jobs handling
|
||||
|
||||
## 0.11.5
|
||||
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* Add extension `StateFlow#asMutableComposeListState` and `StateFlow#asComposeList`
|
||||
* Add extension `StateFlow#asMutableComposeState`/`StateFlow#asComposeState`
|
||||
|
||||
## 0.11.4
|
||||
|
||||
**THIS VERSION HAS BEEN BROKEN, DO NOT USE IT**
|
||||
|
||||
## 0.11.3
|
||||
|
||||
* `Ktor`:
|
||||
* Support of `WebSockets` has been improved
|
||||
* `Client`:
|
||||
* New extensions: `HttpClient#openBaseWebSocketFlow`, `HttpClient#openWebSocketFlow`, `HttpClient#openSecureWebSocketFlow`
|
||||
|
||||
## 0.11.2
|
||||
|
||||
* `Ktor`:
|
||||
* Support of `WebSockets` has been improved and added fixes inside of clients
|
||||
|
||||
## 0.11.1
|
||||
|
||||
* `Repos`
|
||||
* `Ktor`
|
||||
* In `configureReadKeyValueRepoRoutes` and `configureReadKeyValuesRepoRoutes` configurators fixed requiring of `reversed` property
|
||||
|
||||
## 0.11.0
|
||||
|
||||
* `Versions`
|
||||
* `UUID`: `0.4.0` -> `0.4.1`
|
||||
* `Ktor`
|
||||
* `Client`:
|
||||
* New extension fun `HttpResponse#throwOnUnsuccess`
|
||||
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
|
||||
* `Server`:
|
||||
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
|
||||
* `Repos`
|
||||
* `Ktor`:
|
||||
* Fully rewritten work with all declared repositories
|
||||
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
|
||||
|
||||
## 0.10.8
|
||||
|
||||
* `Common`
|
||||
* Add `Element.isOverflow*` extension properties
|
||||
|
||||
## 0.10.7
|
||||
|
||||
* `Pagination`:
|
||||
* Now it is possible to use `doForAll*` and `getForAll` functions in non suspend places
|
||||
|
||||
## 0.10.6
|
||||
|
||||
* `Versions`
|
||||
* `Ktor`: `2.0.1` -> `2.0.2`
|
||||
* `Common`
|
||||
* `JS`:
|
||||
* Add `ResizeObserver` functionality
|
||||
|
||||
## 0.10.5
|
||||
|
||||
* `Versions`
|
||||
* `Compose`: `1.2.0-alpha01-dev683` -> `1.2.0-alpha01-dev686`
|
||||
* `Repos`
|
||||
* `Android`:
|
||||
* New function `SharedPreferencesKeyValueRepo`
|
||||
* `FSM`
|
||||
* Add `StateHandlingErrorHandler` and opportunity to handle states handling errors
|
||||
|
||||
## 0.10.4
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.3.2` -> `1.3.3`
|
||||
|
||||
## 0.10.3
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.2.0-alpha01-dev682` -> `1.2.0-alpha01-dev683`
|
||||
* `Coroutines`:
|
||||
* Fixes in `AccumulatorFlow`
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.2.0-alpha01-dev675` -> `1.2.0-alpha01-dev682`
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.0.0` -> `2.0.1`
|
||||
* `Crypto`:
|
||||
* Add `hmacSha256`
|
||||
* Add `hex`
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.6.10` -> `1.6.21`
|
||||
* `Compose`: `1.1.1` -> `1.2.0-alpha01-dev675`
|
||||
* `Exposed`: `0.37.3` -> `0.38.2`
|
||||
* `Ktor`: `1.6.8` -> `2.0.0`
|
||||
* `Dokka`: `1.6.10` -> `1.6.21`
|
||||
|
||||
## 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`:
|
||||
|
@@ -10,7 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
api libs.android.appCompat.resources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,13 +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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
build.gradle
13
build.gradle
@@ -7,12 +7,12 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ allprojects {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
}
|
||||
|
||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||
|
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"/>
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@RequiresOptIn(
|
||||
|
@@ -43,6 +43,7 @@ private inline fun <T> performChanges(
|
||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||
changedList.addAll(
|
||||
potentialChanges.take(i).mapNotNull {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
||||
}
|
||||
)
|
||||
@@ -121,7 +122,10 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
|
||||
when {
|
||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
||||
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
|
||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||
})
|
||||
potentiallyChangedObjects.clear()
|
||||
}
|
||||
else -> {
|
||||
@@ -153,3 +157,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)
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
/**
|
||||
* Realization of this interface will contains at least one not null - [t1] or [t2]
|
||||
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
|
||||
*
|
||||
* @see EitherFirst
|
||||
* @see EitherSecond
|
||||
@@ -14,25 +14,26 @@ import kotlinx.serialization.encoding.*
|
||||
* @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?
|
||||
|
||||
companion object {
|
||||
fun <T1, T2> serializer(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
|
||||
}
|
||||
get() = optionalT2.dataOrNull()
|
||||
}
|
||||
|
||||
class EitherSerializer<T1, T2>(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
) : KSerializer<Either<T1, T2>> {
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
@@ -43,7 +44,6 @@ class EitherSerializer<T1, T2>(
|
||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
@@ -75,7 +75,6 @@ class EitherSerializer<T1, T2>(
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
when (value) {
|
||||
@@ -93,25 +92,25 @@ class EitherSerializer<T1, T2>(
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [t1]
|
||||
* This type [Either] will always have not nullable [optionalT1]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherFirst<T1, T2>(
|
||||
override val t1: T1
|
||||
) : Either<T1, T2> {
|
||||
override val t2: T2?
|
||||
get() = null
|
||||
override val optionalT1: Optional<T1> = t1.optional
|
||||
override val optionalT2: Optional<T2> = Optional.absent()
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [t2]
|
||||
* This type [Either] will always have not nullable [optionalT2]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherSecond<T1, T2>(
|
||||
override val t2: T2
|
||||
) : Either<T1, T2> {
|
||||
override val t1: T1?
|
||||
get() = null
|
||||
override val optionalT1: Optional<T1> = Optional.absent()
|
||||
override val optionalT2: Optional<T2> = t2.optional
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,23 +123,35 @@ inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst
|
||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [Either.t1] of [this] is not null
|
||||
* Will call [block] in case when [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
|
||||
val t1 = t1
|
||||
t1 ?.let(block)
|
||||
optionalT1.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [Either.t2] of [this] is not null
|
||||
* Will call [block] in case when [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
|
||||
val t2 = t2
|
||||
t2 ?.let(block)
|
||||
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)
|
||||
|
@@ -0,0 +1,53 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
interface SimpleMapper<T1, T2> {
|
||||
fun convertToT1(from: T2): T1
|
||||
fun convertToT2(from: T1): T2
|
||||
}
|
||||
|
||||
@JvmName("convertFromT2")
|
||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
||||
@JvmName("convertFromT1")
|
||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
||||
|
||||
class SimpleMapperImpl<T1, T2>(
|
||||
private val t1: (T2) -> T1,
|
||||
private val t2: (T1) -> T2,
|
||||
) : SimpleMapper<T1, T2> {
|
||||
override fun convertToT1(from: T2): T1 = t1.invoke(from)
|
||||
|
||||
override fun convertToT2(from: T1): T2 = t2.invoke(from)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T1, T2> simpleMapper(
|
||||
noinline t1: (T2) -> T1,
|
||||
noinline t2: (T1) -> T2,
|
||||
) = SimpleMapperImpl(t1, t2)
|
||||
|
||||
interface SimpleSuspendableMapper<T1, T2> {
|
||||
suspend fun convertToT1(from: T2): T1
|
||||
suspend fun convertToT2(from: T1): T2
|
||||
}
|
||||
|
||||
@JvmName("convertFromT2")
|
||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
||||
@JvmName("convertFromT1")
|
||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
||||
|
||||
class SimpleSuspendableMapperImpl<T1, T2>(
|
||||
private val t1: suspend (T2) -> T1,
|
||||
private val t2: suspend (T1) -> T2,
|
||||
) : SimpleSuspendableMapper<T1, T2> {
|
||||
override suspend fun convertToT1(from: T2): T1 = t1.invoke(from)
|
||||
|
||||
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T1, T2> simpleSuspendableMapper(
|
||||
noinline t1: suspend (T2) -> T1,
|
||||
noinline t2: suspend (T1) -> T2,
|
||||
) = SimpleSuspendableMapperImpl(t1, t2)
|
@@ -41,6 +41,13 @@ data class Optional<T> internal constructor(
|
||||
inline val <T> T.optional
|
||||
get() = Optional.presented(this)
|
||||
|
||||
inline val <T : Any> T?.optionalOrAbsentIfNull
|
||||
get() = if (this == null) {
|
||||
Optional.absent<T>()
|
||||
} else {
|
||||
Optional.presented(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
|
@@ -32,7 +32,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,8 +54,8 @@ class DiffUtilsTests {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in 0 until oldList.size) {
|
||||
for ((i, v) in withIndex) {
|
||||
for (step in oldList.indices) {
|
||||
for ((i, _) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
val changes = (
|
||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||
@@ -73,4 +73,83 @@ 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, _) 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, _) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
|
||||
val newList = if (step == 0) {
|
||||
i until oldList.size
|
||||
} else {
|
||||
i until oldList.size step step
|
||||
}
|
||||
newList.forEach { 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,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,12 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.Element
|
||||
|
||||
inline val Element.isOverflowWidth
|
||||
get() = scrollWidth > clientWidth
|
||||
|
||||
inline val Element.isOverflowHeight
|
||||
get() = scrollHeight > clientHeight
|
||||
|
||||
inline val Element.isOverflow
|
||||
get() = isOverflowHeight || isOverflowWidth
|
@@ -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,58 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.*
|
||||
import kotlin.js.Json
|
||||
import kotlin.js.json
|
||||
|
||||
external class ResizeObserver(
|
||||
callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit
|
||||
) {
|
||||
fun observe(target: Element, options: Json = definedExternally)
|
||||
|
||||
fun unobserve(target: Element)
|
||||
|
||||
fun disconnect()
|
||||
}
|
||||
|
||||
external interface ResizeObserverSize {
|
||||
val blockSize: Float
|
||||
val inlineSize: Float
|
||||
}
|
||||
|
||||
external interface ResizeObserverEntry {
|
||||
val borderBoxSize: Array<ResizeObserverSize>
|
||||
val contentBoxSize: Array<ResizeObserverSize>
|
||||
val devicePixelContentBoxSize: Array<ResizeObserverSize>
|
||||
val contentRect: DOMRectReadOnly
|
||||
val target: Element
|
||||
}
|
||||
|
||||
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
|
||||
target,
|
||||
json(
|
||||
"box" to options.box ?.name
|
||||
)
|
||||
)
|
||||
|
||||
class ResizeObserverObserveOptions(
|
||||
val box: Box? = null
|
||||
) {
|
||||
sealed interface Box {
|
||||
val name: String
|
||||
|
||||
object Content : Box {
|
||||
override val name: String
|
||||
get() = "content-box"
|
||||
}
|
||||
|
||||
object Border : Box {
|
||||
override val name: String
|
||||
get() = "border-box"
|
||||
}
|
||||
|
||||
object DevicePixelContent : Box {
|
||||
override val name: String
|
||||
get() = "device-pixel-content-box"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
|
@@ -10,12 +10,17 @@ 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,26 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import dev.inmo.micro_utils.common.applyDiff
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
|
||||
scope: CoroutineScope
|
||||
): SnapshotStateList<T> {
|
||||
val state = mutableStateListOf<T>()
|
||||
subscribeSafelyWithoutExceptions(scope) {
|
||||
state.applyDiff(it)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified T> Flow<List<T>>.asComposeList(
|
||||
scope: CoroutineScope
|
||||
): List<T> = asMutableComposeListState(scope)
|
||||
|
@@ -0,0 +1,35 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
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>.asMutableComposeState(
|
||||
initial: T,
|
||||
scope: CoroutineScope
|
||||
): MutableState<T> {
|
||||
val state = mutableStateOf(initial)
|
||||
subscribeSafelyWithoutExceptions(scope) { state.value = it }
|
||||
return state
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> StateFlow<T>.asMutableComposeState(
|
||||
scope: CoroutineScope
|
||||
): MutableState<T> = asMutableComposeState(value, scope)
|
||||
|
||||
fun <T> Flow<T>.asComposeState(
|
||||
initial: T,
|
||||
scope: CoroutineScope
|
||||
): State<T> {
|
||||
val state = asMutableComposeState(initial, scope)
|
||||
return derivedStateOf { state.value }
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> StateFlow<T>.asComposeState(
|
||||
scope: CoroutineScope
|
||||
): State<T> = asComposeState(value, scope)
|
||||
|
@@ -0,0 +1,23 @@
|
||||
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
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
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"/>
|
@@ -6,11 +6,12 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
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
|
||||
private sealed interface AccumulatorFlowStep<T>
|
||||
private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T>
|
||||
private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
||||
private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
||||
|
||||
/**
|
||||
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
||||
@@ -26,12 +27,12 @@ class AccumulatorFlow<T>(
|
||||
private val subscope = scope.LinkedSupervisorScope()
|
||||
private val activeData = ArrayDeque<T>()
|
||||
private val dataMutex = Mutex()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<T>>()
|
||||
private val channelsMutex = Mutex()
|
||||
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
|
||||
private val steps = subscope.actor<AccumulatorFlowStep<T>> { step ->
|
||||
when (step) {
|
||||
is DataRetrievedAccumulatorFlowStep -> {
|
||||
if (activeData.first() === step.data) {
|
||||
if (activeData.firstOrNull() === step.data) {
|
||||
dataMutex.withLock {
|
||||
activeData.removeFirst()
|
||||
}
|
||||
@@ -42,7 +43,7 @@ class AccumulatorFlow<T>(
|
||||
dataMutex.withLock {
|
||||
val dataToSend = activeData.toList()
|
||||
safelyWithoutExceptions {
|
||||
dataToSend.forEach { step.channel.send(it as Any) }
|
||||
dataToSend.forEach { step.channel.send(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,24 +59,29 @@ class AccumulatorFlow<T>(
|
||||
channelsMutex.withLock {
|
||||
channelsForBroadcast.forEach { channel ->
|
||||
safelyWithResult {
|
||||
channel.send(it as Any)
|
||||
channel.send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
val channel = Channel<T>(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))
|
||||
val result = runCatchingSafely {
|
||||
for (data in channel) {
|
||||
val emitResult = runCatchingSafely {
|
||||
collector.emit(data)
|
||||
}
|
||||
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
}
|
||||
emitResult.getOrThrow()
|
||||
}
|
||||
}
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
result.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,15 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
val HEX_ARRAY = "0123456789abcdef".toCharArray()
|
||||
|
||||
fun SourceBytes.hex(): String {
|
||||
val hexChars = CharArray(size * 2)
|
||||
for (j in indices) {
|
||||
val v: Int = this[j].toInt() and 0xFF
|
||||
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
|
||||
}
|
||||
return hexChars.concatToString()
|
||||
}
|
||||
|
||||
fun SourceString.hex(): String = encodeToByteArray().hex()
|
@@ -0,0 +1,4 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
@Deprecated("Deprecated due to incorrect of work sometimes and redundancy. Can be replaced by korlibs krypto")
|
||||
expect fun SourceString.hmacSha256(key: String): String
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
class Hex {
|
||||
@Test
|
||||
fun testSimpleHmacSHA256Message() {
|
||||
val text = "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 resultHex = text.hex()
|
||||
assertEquals(
|
||||
"4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742c2073656420646f20656975736d6f642074656d706f7220696e6369646964756e74207574206c61626f726520657420646f6c6f7265206d61676e6120616c697175612e20557420656e696d206164206d696e696d2076656e69616d2c2071756973206e6f737472756420657865726369746174696f6e20756c6c616d636f206c61626f726973206e69736920757420616c697175697020657820656120636f6d6d6f646f20636f6e7365717561742e2044756973206175746520697275726520646f6c6f7220696e20726570726568656e646572697420696e20766f6c7570746174652076656c697420657373652063696c6c756d20646f6c6f726520657520667567696174206e756c6c612070617269617475722e204578636570746575722073696e74206f6363616563617420637570696461746174206e6f6e2070726f6964656e742c2073756e7420696e2063756c706120717569206f666669636961206465736572756e74206d6f6c6c697420616e696d20696420657374206c61626f72756d2e",
|
||||
resultHex
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
class HmacSHA256 {
|
||||
@Test
|
||||
fun testSimpleHmacSHA256Message() {
|
||||
val text = "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 resultSha = text.hmacSha256("Example")
|
||||
assertEquals("5a481d59329ef862b158eedc95392ebb22492ba3014661a3379d8201db992484", resultSha)
|
||||
}
|
||||
}
|
@@ -7,3 +7,8 @@ external interface CryptoJs {
|
||||
@JsModule("crypto-js")
|
||||
@JsNonModule
|
||||
external val CryptoJS: CryptoJs
|
||||
|
||||
@Deprecated("Deprecated due to incorrect of work sometimes and redundancy. Can be replaced by korlibs krypto")
|
||||
actual fun SourceString.hmacSha256(key: String): String {
|
||||
return CryptoJS.asDynamic().HmacSHA256(this, key).toString().unsafeCast<String>()
|
||||
}
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@Deprecated("Deprecated due to incorrect of work sometimes and redundancy. Can be replaced by korlibs krypto")
|
||||
actual fun SourceString.hmacSha256(key: String): String {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
|
||||
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
|
||||
mac.init(secretKey)
|
||||
|
||||
return mac.doFinal(toByteArray()).hex()
|
||||
}
|
@@ -26,12 +26,12 @@ ext {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ allprojects {
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
|
@@ -3,7 +3,10 @@ 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 dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
||||
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
@@ -13,13 +16,24 @@ import kotlinx.coroutines.sync.withLock
|
||||
* 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>>,
|
||||
onStateHandlingErrorHandler: StateHandlingErrorHandler<T>
|
||||
): T? {
|
||||
return runCatchingSafely {
|
||||
handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
}
|
||||
}.getOrElse {
|
||||
onStateHandlingErrorHandler(state, it)
|
||||
}
|
||||
}
|
||||
suspend fun launchStateHandling(
|
||||
state: T,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
): T? {
|
||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
}
|
||||
return launchStateHandling(state, handlers, defaultStateHandlingErrorHandler())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,8 +52,9 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||
*/
|
||||
operator fun <T: State> invoke(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
) = DefaultStatesMachine(statesManager, handlers)
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
||||
) = DefaultStatesMachine(statesManager, handlers, onStateHandlingErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +67,17 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||
open class DefaultStatesMachine <T: State>(
|
||||
protected val statesManager: StatesManager<T>,
|
||||
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
||||
) : StatesMachine<T> {
|
||||
/**
|
||||
* Will call [launchStateHandling] for state handling
|
||||
*/
|
||||
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
||||
|
||||
override suspend fun launchStateHandling(state: T, handlers: List<CheckableHandlerHolder<in T, T>>): T? {
|
||||
return launchStateHandling(state, handlers, onStateHandlingErrorHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* This
|
||||
*/
|
||||
@@ -66,10 +86,10 @@ open class DefaultStatesMachine <T: State>(
|
||||
|
||||
protected open suspend fun performUpdate(state: T) {
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState != null) {
|
||||
statesManager.update(state, newState)
|
||||
} else {
|
||||
if (newState == null) {
|
||||
statesManager.endChain(state)
|
||||
} else {
|
||||
statesManager.update(state, newState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,15 +119,21 @@ open class DefaultStatesMachine <T: State>(
|
||||
* [StatesManager.endChain].
|
||||
*/
|
||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
|
||||
(statesManager.getActiveStates().asFlow() + 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.getActiveStates().forEach {
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
|
||||
launch {
|
||||
statesJobsMutex.withLock {
|
||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
||||
if (stateInMap === removedState) {
|
||||
statesJobs[stateInMap] ?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
||||
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
@@ -20,15 +22,23 @@ interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
|
||||
open class DefaultUpdatableStatesMachine<T : State>(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
||||
) : DefaultStatesMachine<T>(
|
||||
statesManager,
|
||||
handlers
|
||||
handlers,
|
||||
onStateHandlingErrorHandler
|
||||
), 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)) {
|
||||
if (shouldReplaceJob(previousState, actualState)) {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
}
|
||||
val job = previousState.mapOnPresented {
|
||||
@@ -42,6 +52,7 @@ open class DefaultUpdatableStatesMachine<T : State>(
|
||||
statesJobs.remove(
|
||||
jobsStates[job] ?: return@withLock
|
||||
)
|
||||
jobsStates.remove(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +63,13 @@ open class DefaultUpdatableStatesMachine<T : State>(
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
|
||||
/**
|
||||
* 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)
|
||||
|
@@ -37,36 +37,49 @@ interface DefaultStatesManagerRepo<T : State> {
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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 onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* @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
|
||||
*/
|
||||
class DefaultStatesManager<T : State>(
|
||||
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
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> {
|
||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||
private val _onStartChain = MutableSharedFlow<T>(0)
|
||||
protected val _onStartChain = MutableSharedFlow<T>(0)
|
||||
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
||||
private val _onEndChain = MutableSharedFlow<T>(0)
|
||||
protected val _onEndChain = MutableSharedFlow<T>(0)
|
||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||
|
||||
private val mapMutex = Mutex()
|
||||
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 || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||
repo.removeState(old)
|
||||
repo.set(new)
|
||||
@@ -77,13 +90,17 @@ class DefaultStatesManager<T : State>(
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
||||
if (!repo.contains(state.context)) {
|
||||
val stateOnContext = repo.getContextState(state.context)
|
||||
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
||||
stateOnContext ?.let {
|
||||
endChainWithoutLock(it)
|
||||
}
|
||||
repo.set(state)
|
||||
_onStartChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun endChainWithoutLock(state: T) {
|
||||
protected open suspend fun endChainWithoutLock(state: T) {
|
||||
if (repo.getContextState(state.context) == state) {
|
||||
repo.removeState(state)
|
||||
_onEndChain.emit(state)
|
||||
|
@@ -12,5 +12,6 @@ import kotlinx.coroutines.flow.*
|
||||
*/
|
||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
||||
fun <T: State> InMemoryStatesManager(
|
||||
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)
|
||||
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
|
||||
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.fsm.common.utils
|
||||
|
||||
typealias StateHandlingErrorHandler<T> = suspend (T, Throwable) -> T?
|
||||
val DefaultStateHandlingErrorHandler: StateHandlingErrorHandler<*> = { _, _ -> null }
|
||||
inline fun <T> defaultStateHandlingErrorHandler(): StateHandlingErrorHandler<T> = DefaultStateHandlingErrorHandler as StateHandlingErrorHandler<T>
|
||||
|
@@ -21,7 +21,7 @@ if (new File(projectDir, "secret.gradle").exists()) {
|
||||
owner "InsanusMokrassar"
|
||||
repo "MicroUtils"
|
||||
|
||||
tagName "${project.version}"
|
||||
tagName "v${project.version}"
|
||||
releaseName "${project.version}"
|
||||
targetCommitish "${project.version}"
|
||||
|
||||
|
@@ -7,43 +7,12 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
|
||||
kotlin_version=1.6.10
|
||||
kotlin_coroutines_version=1.6.0
|
||||
kotlin_serialisation_core_version=1.3.2
|
||||
kotlin_exposed_version=0.37.2
|
||||
|
||||
ktor_version=1.6.7
|
||||
|
||||
klockVersion=2.4.12
|
||||
|
||||
github_release_plugin_version=2.2.12
|
||||
|
||||
uuidVersion=0.4.0
|
||||
|
||||
# ANDROID
|
||||
|
||||
core_ktx_version=1.7.0
|
||||
androidx_recycler_version=1.2.1
|
||||
appcompat_version=1.4.0
|
||||
|
||||
android_minSdkVersion=19
|
||||
android_compileSdkVersion=32
|
||||
android_buildToolsVersion=32.0.0
|
||||
dexcount_version=3.0.1
|
||||
junit_version=4.12
|
||||
test_ext_junit_version=1.1.2
|
||||
espresso_core=3.3.0
|
||||
|
||||
# JS NPM
|
||||
|
||||
crypto_js_version=4.1.1
|
||||
|
||||
# Dokka
|
||||
|
||||
dokka_version=1.6.0
|
||||
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.9.3
|
||||
android_code_version=93
|
||||
version=0.11.14
|
||||
android_code_version=138
|
||||
|
87
gradle/libs.versions.toml
Normal file
87
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,87 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.6.21"
|
||||
kt-serialization = "1.3.3"
|
||||
kt-coroutines = "1.6.4"
|
||||
|
||||
jb-compose = "1.2.0-alpha01-dev731"
|
||||
jb-exposed = "0.38.2"
|
||||
jb-dokka = "1.6.21"
|
||||
|
||||
klock = "2.7.0"
|
||||
uuid = "0.4.1"
|
||||
|
||||
ktor = "2.0.3"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
|
||||
android-gradle = "7.0.4"
|
||||
dexcount = "3.1.0"
|
||||
|
||||
android-coreKtx = "1.7.0"
|
||||
android-recyclerView = "1.2.1"
|
||||
android-appCompat = "1.4.1"
|
||||
android-espresso = "3.3.0"
|
||||
android-test = "1.1.2"
|
||||
|
||||
android-props-minSdk = "19"
|
||||
android-props-compileSdk = "32"
|
||||
android-props-buildTools = "32.0.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
|
||||
|
||||
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
|
||||
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
|
||||
|
||||
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
|
||||
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
|
||||
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
|
||||
|
||||
|
||||
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
|
||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
|
||||
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
|
||||
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
|
||||
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
|
||||
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
|
||||
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
|
||||
ktor-server-statusPages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
|
||||
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
|
||||
|
||||
|
||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
||||
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
||||
|
||||
|
||||
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
||||
|
||||
|
||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||
|
||||
|
||||
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
|
||||
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
|
||||
|
||||
|
||||
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
|
||||
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
|
||||
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
|
||||
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
|
||||
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
|
||||
buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
|
||||
|
||||
[plugins]
|
||||
|
||||
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -12,7 +12,7 @@ kotlin {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.ktor.common")
|
||||
api internalProject("micro_utils.coroutines")
|
||||
api "io.ktor:ktor-client-core:$ktor_version"
|
||||
api libs.ktor.client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,18 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.features.websocket.ws
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import io.ktor.http.cio.websocket.readBytes
|
||||
import io.ktor.client.plugins.pluginOrNull
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.ws
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readBytes
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
|
||||
/**
|
||||
@@ -16,42 +21,41 @@ import kotlinx.serialization.DeserializationStrategy
|
||||
*/
|
||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
url: String,
|
||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
||||
): Flow<T> {
|
||||
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
|
||||
|
||||
val correctedUrl = url.asCorrectWebSocketUrl
|
||||
|
||||
return channelFlow {
|
||||
val producerScope = this@channelFlow
|
||||
do {
|
||||
val reconnect = try {
|
||||
safely {
|
||||
ws(correctedUrl) {
|
||||
for (received in incoming) {
|
||||
when (received) {
|
||||
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
||||
else -> {
|
||||
producerScope.close()
|
||||
return@ws
|
||||
}
|
||||
val reconnect = runCatchingSafely {
|
||||
ws(correctedUrl, requestBuilder) {
|
||||
for (received in incoming) {
|
||||
when (received) {
|
||||
is Frame.Binary -> send(conversation(received.data))
|
||||
else -> {
|
||||
close()
|
||||
return@ws
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkReconnection(null)
|
||||
} catch (e: Throwable) {
|
||||
}.getOrElse { e ->
|
||||
checkReconnection(e).also {
|
||||
if (!it) {
|
||||
producerScope.close(e)
|
||||
close(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (reconnect)
|
||||
if (!producerScope.isClosedForSend) {
|
||||
safely(
|
||||
{ it.printStackTrace() }
|
||||
) {
|
||||
producerScope.close()
|
||||
} while (reconnect && isActive)
|
||||
|
||||
if (isActive) {
|
||||
safely {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,12 +67,14 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
*/
|
||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
url: String,
|
||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = createStandardWebsocketFlow(
|
||||
url,
|
||||
checkReconnection
|
||||
checkReconnection,
|
||||
requestBuilder
|
||||
) {
|
||||
serialFormat.decodeDefault(deserializer, it)
|
||||
}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import io.ktor.client.plugins.ClientRequestException
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.isSuccess
|
||||
|
||||
inline fun HttpResponse.throwOnUnsuccess(
|
||||
unsuccessMessage: () -> String
|
||||
) {
|
||||
if (status.isSuccess()) {
|
||||
return
|
||||
}
|
||||
|
||||
throw ClientRequestException(this, unsuccessMessage())
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.Warning
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.pluginOrNull
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.http.URLProtocol
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
/**
|
||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
||||
*/
|
||||
@Warning("This feature is internal and should not be used directly. It is can be changed without any notification and warranty on compile-time or other guaranties")
|
||||
inline fun <reified T : Any> openBaseWebSocketFlow(
|
||||
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
noinline webSocketSessionRequest: suspend SendChannel<T>.() -> Unit
|
||||
): Flow<T> {
|
||||
return channelFlow {
|
||||
do {
|
||||
val reconnect = runCatchingSafely {
|
||||
webSocketSessionRequest()
|
||||
checkReconnection(null)
|
||||
}.getOrElse { e ->
|
||||
checkReconnection(e).also {
|
||||
if (!it) {
|
||||
close(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (reconnect && isActive)
|
||||
|
||||
if (isActive) {
|
||||
safely {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
||||
*/
|
||||
inline fun <reified T : Any> HttpClient.openWebSocketFlow(
|
||||
url: String,
|
||||
useSecureConnection: Boolean,
|
||||
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): Flow<T> {
|
||||
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
|
||||
|
||||
return openBaseWebSocketFlow<T>(checkReconnection) {
|
||||
val block: suspend DefaultClientWebSocketSession.() -> Unit = {
|
||||
while (isActive) {
|
||||
send(receiveDeserialized<T>())
|
||||
}
|
||||
}
|
||||
|
||||
if (useSecureConnection) {
|
||||
wss(url, requestBuilder, block)
|
||||
} else {
|
||||
ws(url, requestBuilder, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
||||
*/
|
||||
inline fun <reified T : Any> HttpClient.openWebSocketFlow(
|
||||
url: String,
|
||||
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): Flow<T> = openWebSocketFlow(url, false, checkReconnection, requestBuilder)
|
||||
|
||||
/**
|
||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
||||
*/
|
||||
inline fun <reified T : Any> HttpClient.openSecureWebSocketFlow(
|
||||
url: String,
|
||||
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): Flow<T> = openWebSocketFlow(url, true, checkReconnection, requestBuilder)
|
||||
|
||||
/**
|
||||
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
|
||||
* connection. Must return true in case if must be reconnected. By default always reconnecting
|
||||
*/
|
||||
inline fun <reified T : Any> HttpClient.createStandardWebsocketFlow(
|
||||
url: String,
|
||||
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): Flow<T> = openWebSocketFlow(url, checkReconnection, requestBuilder)
|
@@ -4,12 +4,15 @@ import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.readBytes
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import kotlinx.serialization.*
|
||||
|
||||
@Deprecated("This class will be removed in next")
|
||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||
|
||||
class UnifiedRequester(
|
||||
@@ -31,7 +34,7 @@ class UnifiedRequester(
|
||||
|
||||
suspend fun <BodyType, ResultType> unipost(
|
||||
url: String,
|
||||
bodyInfo: BodyPair<BodyType>,
|
||||
bodyInfo: Pair<SerializationStrategy<BodyType>, BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
||||
|
||||
@@ -50,7 +53,7 @@ class UnifiedRequester(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
otherData: BodyPair<BodyType>,
|
||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
@@ -73,7 +76,7 @@ class UnifiedRequester(
|
||||
suspend fun <BodyType, ResultType> unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
otherData: BodyPair<BodyType>,
|
||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
@@ -85,9 +88,16 @@ class UnifiedRequester(
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
deserializer: DeserializationStrategy<T>
|
||||
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||
checkReconnection: suspend (Throwable?) -> Boolean,
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = client.createStandardWebsocketFlow(url, deserializer, checkReconnection, serialFormat, requestBuilder)
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
|
||||
}
|
||||
|
||||
val defaultRequester = UnifiedRequester()
|
||||
@@ -96,10 +106,8 @@ suspend fun <ResultType> HttpClient.uniget(
|
||||
url: String,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = get<StandardKtorSerialInputData>(
|
||||
url
|
||||
).let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
) = get(url).let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
|
||||
}
|
||||
|
||||
|
||||
@@ -113,13 +121,15 @@ fun <T> SerializationStrategy<T>.encodeUrlQueryValue(
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||
url: String,
|
||||
bodyInfo: BodyPair<BodyType>,
|
||||
bodyInfo: Pair<SerializationStrategy<BodyType>, BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = post<StandardKtorSerialInputData>(url) {
|
||||
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||
) = post(url) {
|
||||
setBody(
|
||||
serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||
)
|
||||
}.let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
|
||||
}
|
||||
|
||||
suspend fun <ResultType> HttpClient.unimultipart(
|
||||
@@ -132,7 +142,7 @@ suspend fun <ResultType> HttpClient.unimultipart(
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
|
||||
): ResultType = submitFormWithBinaryData(
|
||||
url,
|
||||
formData = formData {
|
||||
append(
|
||||
@@ -148,12 +158,12 @@ suspend fun <ResultType> HttpClient.unimultipart(
|
||||
}
|
||||
) {
|
||||
requestBuilder()
|
||||
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
|
||||
}.let { serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>()) }
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
otherData: BodyPair<BodyType>,
|
||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
@@ -211,7 +221,7 @@ suspend fun <ResultType> HttpClient.unimultipart(
|
||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
otherData: BodyPair<BodyType>,
|
||||
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
expect suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||
): TemporalFileId
|
||||
|
||||
suspend fun UnifiedRequester.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||
): TemporalFileId = client.tempUpload(
|
||||
fullTempUploadDraftPath, file, onUpload
|
||||
)
|
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.*
|
||||
import org.w3c.dom.mediasource.ENDED
|
||||
import org.w3c.dom.mediasource.ReadyState
|
||||
import org.w3c.xhr.*
|
||||
import org.w3c.xhr.XMLHttpRequest.Companion.DONE
|
||||
|
||||
suspend fun tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (Long, Long) -> Unit
|
||||
): TemporalFileId {
|
||||
val formData = FormData()
|
||||
val answer = CompletableDeferred<TemporalFileId>(currentCoroutineContext().job)
|
||||
|
||||
formData.append(
|
||||
"data",
|
||||
file
|
||||
)
|
||||
|
||||
val request = XMLHttpRequest()
|
||||
request.responseType = XMLHttpRequestResponseType.TEXT
|
||||
request.upload.onprogress = {
|
||||
onUpload(it.loaded.toLong(), it.total.toLong())
|
||||
}
|
||||
request.onload = {
|
||||
if (request.status == 200.toShort()) {
|
||||
answer.complete(TemporalFileId(request.responseText))
|
||||
} else {
|
||||
answer.completeExceptionally(Exception("Something went wrong: $it"))
|
||||
}
|
||||
}
|
||||
request.onerror = {
|
||||
answer.completeExceptionally(Exception("Something went wrong: $it"))
|
||||
}
|
||||
request.open("POST", fullTempUploadDraftPath, true)
|
||||
request.send(formData)
|
||||
|
||||
answer.invokeOnCompletion {
|
||||
runCatching {
|
||||
if (request.readyState != DONE) {
|
||||
request.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return answer.await()
|
||||
}
|
||||
|
||||
|
||||
actual suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (uploaded: Long, count: Long) -> Unit
|
||||
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)
|
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.onUpload
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import java.net.URLConnection
|
||||
|
||||
internal val MPPFile.mimeType: String
|
||||
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
|
||||
|
||||
actual suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (Long, Long) -> Unit
|
||||
): TemporalFileId {
|
||||
val inputProvider = file.inputProvider()
|
||||
val fileId = submitFormWithBinaryData(
|
||||
fullTempUploadDraftPath,
|
||||
formData = formData {
|
||||
append(
|
||||
"data",
|
||||
inputProvider,
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, file.mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
onUpload(onUpload)
|
||||
}.bodyAsText()
|
||||
return TemporalFileId(fileId)
|
||||
}
|
@@ -11,8 +11,10 @@ kotlin {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.common")
|
||||
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
|
||||
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
||||
api libs.kt.serialization.cbor
|
||||
api libs.klock
|
||||
api libs.uuid
|
||||
api libs.ktor.io
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.utils.io.core.Input
|
||||
|
||||
expect fun MPPFile.input(): Input
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import kotlin.jvm.JvmInline
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
const val DefaultTemporalFilesSubPath = "temp_upload"
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class TemporalFileId(val string: String)
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import io.ktor.utils.io.core.Input
|
||||
|
||||
actual fun MPPFile.input(): Input = ByteReadPacket(readBytes())
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.utils.io.core.Input
|
||||
import io.ktor.utils.io.streams.asInput
|
||||
|
||||
actual fun MPPFile.input(): Input = inputStream().asInput()
|
@@ -16,10 +16,11 @@ kotlin {
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-server:$ktor_version"
|
||||
api "io.ktor:ktor-server-cio:$ktor_version"
|
||||
api "io.ktor:ktor-server-host-common:$ktor_version"
|
||||
api "io.ktor:ktor-websockets:$ktor_version"
|
||||
api libs.ktor.server
|
||||
api libs.ktor.server.cio
|
||||
api libs.ktor.server.host.common
|
||||
api libs.ktor.server.websockets
|
||||
api libs.ktor.server.statusPages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.response.responseType
|
||||
import io.ktor.util.InternalAPI
|
||||
import io.ktor.util.reflect.TypeInfo
|
||||
|
||||
@InternalAPI
|
||||
suspend fun <T : Any> ApplicationCall.respond(
|
||||
message: T,
|
||||
typeInfo: TypeInfo
|
||||
) {
|
||||
response.responseType = typeInfo
|
||||
response.pipeline.execute(this, message as Any)
|
||||
}
|
@@ -2,29 +2,32 @@ package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.http.cio.websocket.*
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.websocket.webSocket
|
||||
import io.ktor.http.URLProtocol
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.application.pluginOrNull
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.application
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
|
||||
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
||||
if (incoming.tryReceive() != null) {
|
||||
close()
|
||||
throw CorrectCloseException
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("This method will be removed soon")
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
converter: (T) -> StandardKtorSerialInputData
|
||||
protocol: URLProtocol? = null,
|
||||
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
|
||||
) {
|
||||
webSocket(suburl) {
|
||||
application.apply {
|
||||
pluginOrNull(WebSockets) ?: install(WebSockets)
|
||||
}
|
||||
webSocket(suburl, protocol ?.name) {
|
||||
safely {
|
||||
flow.collect {
|
||||
send(converter(it))
|
||||
converter(it) ?.let { data ->
|
||||
send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +37,24 @@ fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
serializer: SerializationStrategy<T>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
protocol: URLProtocol? = null,
|
||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
||||
) = includeWebsocketHandling(
|
||||
suburl,
|
||||
flow
|
||||
) {
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
}
|
||||
flow,
|
||||
protocol,
|
||||
converter = if (filter == null) {
|
||||
{
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
}
|
||||
} else {
|
||||
{
|
||||
if (filter(it)) {
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.http.URLProtocol
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.application.pluginOrNull
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.application
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
|
||||
inline fun <reified T : Any> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
protocol: URLProtocol? = null,
|
||||
noinline dataMapper: suspend WebSocketServerSession.(T) -> T? = { it }
|
||||
) {
|
||||
application.apply {
|
||||
pluginOrNull(WebSockets) ?: install(WebSockets)
|
||||
}
|
||||
webSocket(suburl, protocol ?.name) {
|
||||
safely {
|
||||
flow.collect {
|
||||
sendSerialized(dataMapper(it) ?: return@collect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,25 +3,21 @@ package dev.inmo.micro_utils.ktor.server
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.content.PartData
|
||||
import io.ktor.http.content.forEachPart
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.request.receiveMultipart
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.asStream
|
||||
import io.ktor.util.cio.writeChannel
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.request.receiveMultipart
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondBytes
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.websocket.WebSocketServerSession
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.io.File.createTempFile
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
|
||||
class UnifiedRouter(
|
||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
@@ -30,8 +26,10 @@ class UnifiedRouter(
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
serializer: SerializationStrategy<T>
|
||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
||||
serializer: SerializationStrategy<T>,
|
||||
protocol: URLProtocol? = null,
|
||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
|
||||
|
||||
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||
answerSerializer: SerializationStrategy<T>,
|
||||
@@ -90,6 +88,11 @@ class UnifiedRouter(
|
||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val default
|
||||
get() = defaultUnifiedRouter
|
||||
}
|
||||
}
|
||||
|
||||
val defaultUnifiedRouter = UnifiedRouter()
|
||||
@@ -190,7 +193,9 @@ suspend fun <T> ApplicationCall.uniloadMultipartFile(
|
||||
".${name.extension}"
|
||||
).apply {
|
||||
outputStream().use { fileStream ->
|
||||
it.provider().asStream().copyTo(fileStream)
|
||||
it.streamProvider().use {
|
||||
it.copyTo(fileStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +237,9 @@ suspend fun ApplicationCall.uniloadMultipartFile(
|
||||
".${name.extension}"
|
||||
).apply {
|
||||
outputStream().use { fileStream ->
|
||||
it.provider().asStream().copyTo(fileStream)
|
||||
it.streamProvider().use {
|
||||
it.copyTo(fileStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.cio.CIOApplicationEngine
|
||||
import io.ktor.server.engine.*
|
||||
import kotlin.random.Random
|
||||
|
||||
@@ -10,17 +11,21 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
|
||||
block: Application.() -> Unit
|
||||
): TEngine {
|
||||
val env = applicationEngineEnvironment {
|
||||
): TEngine = embeddedServer(
|
||||
engine,
|
||||
applicationEngineEnvironment {
|
||||
module(block)
|
||||
connector {
|
||||
this@connector.host = host
|
||||
this@connector.port = port
|
||||
this.host = host
|
||||
this.port = port
|
||||
}
|
||||
}
|
||||
return embeddedServer(engine, env)
|
||||
}
|
||||
additionalEngineEnvironmentConfigurator()
|
||||
},
|
||||
additionalConfigurationConfigurator
|
||||
)
|
||||
|
||||
/**
|
||||
* Create server with [CIO] server engine without starting of it
|
||||
@@ -30,18 +35,31 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
||||
fun createKtorServer(
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
|
||||
block: Application.() -> Unit
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, block)
|
||||
): CIOApplicationEngine = createKtorServer(
|
||||
CIO,
|
||||
host,
|
||||
port,
|
||||
additionalEngineEnvironmentConfigurator,
|
||||
additionalConfigurationConfigurator,
|
||||
block
|
||||
)
|
||||
|
||||
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
|
||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
|
||||
configurators: List<KtorApplicationConfigurator>
|
||||
): TEngine = createKtorServer(
|
||||
engine,
|
||||
host,
|
||||
port
|
||||
port,
|
||||
additionalEngineEnvironmentConfigurator,
|
||||
additionalConfigurationConfigurator
|
||||
) {
|
||||
configurators.forEach { it.apply { configure() } }
|
||||
}
|
||||
@@ -54,5 +72,7 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
||||
fun createKtorServer(
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
configurators: List<KtorApplicationConfigurator>
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
|
||||
configurators: List<KtorApplicationConfigurator>,
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators)
|
||||
|
@@ -0,0 +1,133 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.micro_utils.common.FileName
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.content.PartData
|
||||
import io.ktor.http.content.streamProvider
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.request.receiveMultipart
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.post
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.FileTime
|
||||
|
||||
class TemporalFilesRoutingConfigurator(
|
||||
private val subpath: String = DefaultTemporalFilesSubPath,
|
||||
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
|
||||
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
interface TemporalFilesUtilizer {
|
||||
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
|
||||
|
||||
companion object : TemporalFilesUtilizer {
|
||||
class ByTimerUtilizer(
|
||||
private val removeMillis: Long,
|
||||
private val scope: CoroutineScope
|
||||
) : TemporalFilesUtilizer {
|
||||
override fun start(
|
||||
filesMap: MutableMap<TemporalFileId, MPPFile>,
|
||||
filesMutex: Mutex,
|
||||
onNewFileFlow: Flow<TemporalFileId>
|
||||
): Job = scope.launchSafelyWithoutExceptions {
|
||||
while (isActive) {
|
||||
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
|
||||
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
|
||||
}
|
||||
if (filesWithCreationInfo.isEmpty()) {
|
||||
delay(removeMillis)
|
||||
continue
|
||||
}
|
||||
var min = filesWithCreationInfo.first()
|
||||
for (fileWithCreationInfo in filesWithCreationInfo) {
|
||||
if (fileWithCreationInfo.second < min.second) {
|
||||
min = fileWithCreationInfo
|
||||
}
|
||||
}
|
||||
delay(System.currentTimeMillis() - (min.second + removeMillis))
|
||||
filesMutex.withLock {
|
||||
filesMap.remove(min.first)
|
||||
} ?.delete()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun start(
|
||||
filesMap: MutableMap<TemporalFileId, MPPFile>,
|
||||
filesMutex: Mutex,
|
||||
onNewFileFlow: Flow<TemporalFileId>
|
||||
): Job = Job()
|
||||
}
|
||||
}
|
||||
|
||||
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
|
||||
private val temporalFilesMutex = Mutex()
|
||||
private val filesFlow = MutableSharedFlow<TemporalFileId>()
|
||||
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
|
||||
|
||||
override fun Route.invoke() {
|
||||
post(subpath) {
|
||||
unifiedRouter.apply {
|
||||
val multipart = call.receiveMultipart()
|
||||
|
||||
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
|
||||
var part = multipart.readPart()
|
||||
|
||||
while (part != null) {
|
||||
if (part is PartData.FileItem) {
|
||||
break
|
||||
}
|
||||
part = multipart.readPart()
|
||||
}
|
||||
|
||||
part ?.let {
|
||||
if (it is PartData.FileItem) {
|
||||
val fileId = TemporalFileId(uuid4().toString())
|
||||
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
|
||||
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
|
||||
outputStream().use { outputStream ->
|
||||
it.streamProvider().use {
|
||||
it.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileInfo ?.also { (fileId, file) ->
|
||||
temporalFilesMutex.withLock {
|
||||
temporalFilesMap[fileId] = file
|
||||
}
|
||||
call.respondText(fileId.string)
|
||||
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
|
||||
} ?: call.respond(HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
|
||||
temporalFilesMutex.withLock {
|
||||
temporalFilesMap.remove(temporalFileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
|
||||
|
||||
suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
|
||||
temporalFilesMap.remove(temporalFileId)
|
||||
}
|
||||
}
|
@@ -1,14 +1,15 @@
|
||||
package dev.inmo.micro_utils.ktor.server.configurators
|
||||
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.install
|
||||
import io.ktor.features.CachingHeaders
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.cachingheaders.CachingHeaders
|
||||
import io.ktor.server.plugins.cachingheaders.CachingHeadersConfig
|
||||
import kotlinx.serialization.Contextual
|
||||
|
||||
data class ApplicationCachingHeadersConfigurator(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
||||
fun interface Element { operator fun CachingHeadersConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
install(CachingHeaders) {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
package dev.inmo.micro_utils.ktor.server.configurators
|
||||
|
||||
import io.ktor.application.*
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.Routing
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator.Element
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.Routing
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -18,7 +19,7 @@ class ApplicationRoutingConfigurator(
|
||||
}
|
||||
|
||||
override fun Application.configure() {
|
||||
featureOrNull(Routing) ?.apply {
|
||||
pluginOrNull(Routing) ?.apply {
|
||||
rootInstaller.apply { invoke() }
|
||||
} ?: install(Routing) {
|
||||
rootInstaller.apply { invoke() }
|
||||
|
@@ -1,14 +1,15 @@
|
||||
package dev.inmo.micro_utils.ktor.server.configurators
|
||||
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.install
|
||||
import io.ktor.sessions.Sessions
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.sessions.Sessions
|
||||
import io.ktor.server.sessions.SessionsConfig
|
||||
import kotlinx.serialization.Contextual
|
||||
|
||||
class ApplicationSessionsConfigurator(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun Sessions.Configuration.invoke() }
|
||||
fun interface Element { operator fun SessionsConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
install(Sessions) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.server.configurators
|
||||
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.server.application.Application
|
||||
|
||||
interface KtorApplicationConfigurator {
|
||||
fun Application.configure()
|
||||
|
@@ -1,14 +1,15 @@
|
||||
package dev.inmo.micro_utils.ktor.server.configurators
|
||||
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.install
|
||||
import io.ktor.features.StatusPages
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.statuspages.StatusPages
|
||||
import io.ktor.server.plugins.statuspages.StatusPagesConfig
|
||||
import kotlinx.serialization.Contextual
|
||||
|
||||
class StatusPagesConfigurator(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun StatusPages.Configuration.invoke() }
|
||||
fun interface Element { operator fun StatusPagesConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
install(StatusPages) {
|
||||
|
@@ -4,8 +4,8 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
||||
implementation libs.kt.stdlib
|
||||
implementation libs.kt.serialization
|
||||
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-java:$ktor_version"
|
||||
implementation libs.ktor.client
|
||||
implementation libs.ktor.client.java
|
||||
}
|
||||
|
||||
mainClassName="MainKt"
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
@@ -164,7 +165,7 @@ suspend fun main(vararg args: String) {
|
||||
|
||||
val ietfLanguageCodes = json.decodeFromString(
|
||||
ListSerializer(LanguageCode.serializer()),
|
||||
client.get(ietfLanguageCodesLink)
|
||||
client.get(ietfLanguageCodesLink).bodyAsText()
|
||||
).map {
|
||||
it.copy(
|
||||
title = it.title
|
||||
@@ -175,7 +176,7 @@ suspend fun main(vararg args: String) {
|
||||
}
|
||||
val ietfLanguageCodesWithTagsMap = json.decodeFromString(
|
||||
ListSerializer(LanguageCodeWithTag.serializer()),
|
||||
client.get(ietfLanguageCodesAdditionalTagsLink)
|
||||
client.get(ietfLanguageCodesAdditionalTagsLink).bodyAsText()
|
||||
).filter { it.withSubtag != it.tag }.groupBy { it.tag }
|
||||
|
||||
val tags = ietfLanguageCodes.map {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package dev.inmo.micro_utils.mime_types
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
@@ -16,6 +15,7 @@ fun mimeType(raw: String) = mimesCache.getOrPut(raw) {
|
||||
|
||||
internal fun parseMimeType(raw: String): MimeType = CustomMimeType(raw)
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
@Serializer(MimeType::class)
|
||||
object MimeTypeSerializer : KSerializer<MimeType> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("mimeType", PrimitiveKind.STRING)
|
||||
|
@@ -23,7 +23,7 @@ kotlin {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
||||
implementation libs.kt.serialization
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
@@ -46,8 +46,8 @@ kotlin {
|
||||
androidTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
implementation "androidx.test.ext:junit:$test_ext_junit_version"
|
||||
implementation "androidx.test.espresso:espresso-core:$espresso_core"
|
||||
implementation libs.android.test.junit
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
mppProjectWithSerializationAndCompose.gradle
Normal file
72
mppProjectWithSerializationAndCompose.gradle
Normal file
@@ -0,0 +1,72 @@
|
||||
project.version = "$version"
|
||||
project.group = "$group"
|
||||
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
js (IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
android {
|
||||
publishAllLibraryVariants()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
implementation libs.kt.serialization
|
||||
implementation compose.runtime
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-common')
|
||||
implementation kotlin('test-annotations-common')
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation compose.desktop.currentOs
|
||||
}
|
||||
}
|
||||
jvmTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation compose.web.core
|
||||
}
|
||||
}
|
||||
jsTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-js')
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
androidTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
implementation libs.android.test.junit
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
@@ -1,23 +1,60 @@
|
||||
package dev.inmo.micro_utils.pagination
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* @param page Current page number
|
||||
* @param size Current page size. It can be greater than size of [results]
|
||||
* @param results Result objects
|
||||
* @param objectsNumber Count of all objects across all pages
|
||||
*/
|
||||
@Serializable
|
||||
data class PaginationResult<T>(
|
||||
override val page: Int,
|
||||
val pagesNumber: Int,
|
||||
override val size: Int,
|
||||
val results: List<T>,
|
||||
override val size: Int
|
||||
) : Pagination
|
||||
val objectsNumber: Long
|
||||
) : Pagination {
|
||||
/**
|
||||
* Amount of pages for current pagination
|
||||
*/
|
||||
@EncodeDefault
|
||||
val pagesNumber: Int = ceil(objectsNumber / size.toFloat()).toInt()
|
||||
|
||||
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0)
|
||||
constructor(
|
||||
page: Int,
|
||||
results: List<T>,
|
||||
pagesNumber: Int,
|
||||
size: Int
|
||||
) : this(
|
||||
page,
|
||||
size,
|
||||
results,
|
||||
(pagesNumber * size).toLong()
|
||||
)
|
||||
@Deprecated("Replace with The other order of incoming parameters or objectsCount parameter")
|
||||
constructor(
|
||||
page: Int,
|
||||
pagesNumber: Int,
|
||||
results: List<T>,
|
||||
size: Int
|
||||
) : this(
|
||||
page,
|
||||
results,
|
||||
pagesNumber,
|
||||
size
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0L)
|
||||
|
||||
/**
|
||||
* @return New [PaginationResult] with [data] without checking of data sizes equality
|
||||
*/
|
||||
fun <I, O> PaginationResult<I>.changeResultsUnchecked(
|
||||
data: List<O>
|
||||
): PaginationResult<O> = PaginationResult(page, pagesNumber, data, size)
|
||||
): PaginationResult<O> = PaginationResult(page, size, data, objectsNumber)
|
||||
/**
|
||||
* @return New [PaginationResult] with [data] <b>with</b> checking of data sizes equality
|
||||
*/
|
||||
@@ -33,12 +70,9 @@ fun <T> List<T>.createPaginationResult(
|
||||
commonObjectsNumber: Long
|
||||
) = PaginationResult(
|
||||
pagination.page,
|
||||
calculatePagesNumber(
|
||||
commonObjectsNumber,
|
||||
pagination.size
|
||||
),
|
||||
pagination.size,
|
||||
this,
|
||||
pagination.size
|
||||
commonObjectsNumber
|
||||
)
|
||||
|
||||
fun <T> List<T>.createPaginationResult(
|
||||
@@ -46,12 +80,9 @@ fun <T> List<T>.createPaginationResult(
|
||||
commonObjectsNumber: Long
|
||||
) = PaginationResult(
|
||||
calculatePage(firstIndex, size),
|
||||
calculatePagesNumber(
|
||||
commonObjectsNumber,
|
||||
size
|
||||
),
|
||||
size,
|
||||
this,
|
||||
size
|
||||
commonObjectsNumber
|
||||
)
|
||||
|
||||
fun <T> Pair<Long, List<T>>.createPaginationResult(
|
||||
|
@@ -26,6 +26,10 @@ inline fun Pagination.nextPage() =
|
||||
size
|
||||
)
|
||||
|
||||
/**
|
||||
* @param page Current page number
|
||||
* @param size Current page size
|
||||
*/
|
||||
@Serializable
|
||||
data class SimplePagination(
|
||||
override val page: Int,
|
||||
|
@@ -27,4 +27,5 @@ inline fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? =
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()
|
||||
|
@@ -2,19 +2,19 @@ package dev.inmo.micro_utils.pagination.utils
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
|
||||
suspend fun <T> doForAll(
|
||||
inline fun <T> doForAll(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: (PaginationResult<T>) -> Pagination?,
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
) {
|
||||
doWithPagination(initialPagination) {
|
||||
block(it).let(paginationMapper)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> doForAllWithNextPaging(
|
||||
inline fun <T> doForAllWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
) {
|
||||
doForAll(
|
||||
initialPagination,
|
||||
@@ -23,9 +23,9 @@ suspend fun <T> doForAllWithNextPaging(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> doAllWithCurrentPaging(
|
||||
inline fun <T> doAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
) {
|
||||
doForAll(
|
||||
initialPagination,
|
||||
@@ -34,7 +34,7 @@ suspend fun <T> doAllWithCurrentPaging(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> doForAllWithCurrentPaging(
|
||||
inline fun <T> doForAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
) = doAllWithCurrentPaging(initialPagination, block)
|
||||
|
@@ -2,10 +2,10 @@ package dev.inmo.micro_utils.pagination.utils
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
|
||||
suspend fun <T> getAll(
|
||||
inline fun <T> getAll(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: (PaginationResult<T>) -> Pagination?,
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
): List<T> {
|
||||
val results = mutableListOf<T>()
|
||||
doForAll(initialPagination, paginationMapper) {
|
||||
@@ -16,46 +16,45 @@ suspend fun <T> getAll(
|
||||
return results.toList()
|
||||
}
|
||||
|
||||
suspend fun <T, R> R.getAllBy(
|
||||
inline fun <T, R> R.getAllBy(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: R.(PaginationResult<T>) -> Pagination?,
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
block: R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ paginationMapper(it) },
|
||||
{ block(it) }
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithNextPaging(
|
||||
inline fun <T> getAllWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ it.nextPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T, R> R.getAllByWithNextPaging(
|
||||
inline fun <T, R> R.getAllByWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
block: R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithNextPaging(
|
||||
initialPagination,
|
||||
{ block(it) }
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithCurrentPaging(
|
||||
inline fun <T> getAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
block: (Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ it.currentPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T, R> R.getAllByWithCurrentPaging(
|
||||
inline fun <T, R> R.getAllByWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
block: R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithCurrentPaging(
|
||||
initialPagination,
|
||||
{ block(it) }
|
||||
)
|
||||
initialPagination
|
||||
) { block(it) }
|
||||
|
@@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
|
||||
size.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
|
||||
is List<T> -> optionallyReverse(reverse)
|
||||
is Set<T> -> optionallyReverse(reverse)
|
||||
else -> if (reverse) {
|
||||
reversed()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
|
||||
reversed()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
|
||||
reversed().toSet()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
|
||||
Array(size) {
|
||||
get(lastIndex - it)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.*
|
||||
* Example:
|
||||
*
|
||||
* * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|`
|
||||
* * `|__f__l_|` will be transformed to `|__f__l_|`
|
||||
* * `|__f__l_|` will be transformed to `|_f__l__|`
|
||||
*
|
||||
* @return Reversed version of this [Pagination]
|
||||
*/
|
||||
@@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination {
|
||||
* Shortcut for [reverse]
|
||||
*/
|
||||
fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong())
|
||||
|
||||
fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) {
|
||||
reverse(objectsCount)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) {
|
||||
reverse(objectsCount)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ kotlin {
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
|
||||
api libs.jb.exposed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,8 +15,8 @@ kotlin {
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-server:$ktor_version"
|
||||
api "io.ktor:ktor-server-host-common:$ktor_version"
|
||||
api libs.ktor.server
|
||||
api libs.ktor.server.host.common
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.micro_utils.pagination
|
||||
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.http.Parameters
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
|
||||
val Parameters.extractPagination: Pagination
|
||||
get() = SimplePagination(
|
||||
|
2
repos/cache/build.gradle
vendored
2
repos/cache/build.gradle
vendored
@@ -15,4 +15,4 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.cache.cache.KVCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
open class ReadCRUDCacheRepo<ObjectType, IdType>(
|
||||
protected val parentRepo: ReadCRUDRepo<ObjectType, IdType>,
|
||||
protected val kvCache: KVCache<IdType, ObjectType>,
|
||||
protected val idGetter: (ObjectType) -> IdType
|
||||
) : ReadCRUDRepo<ObjectType, IdType> by parentRepo {
|
||||
protected open val parentRepo: ReadCRUDRepo<ObjectType, IdType>,
|
||||
protected open val kvCache: KVCache<IdType, ObjectType>,
|
||||
protected open val idGetter: (ObjectType) -> IdType
|
||||
) : ReadCRUDRepo<ObjectType, IdType> by parentRepo, CacheRepo {
|
||||
override suspend fun getById(id: IdType): ObjectType? = kvCache.get(id) ?: (parentRepo.getById(id) ?.also {
|
||||
kvCache.set(id, it)
|
||||
})
|
||||
@@ -18,8 +18,63 @@ open class ReadCRUDCacheRepo<ObjectType, IdType>(
|
||||
override suspend fun contains(id: IdType): Boolean = kvCache.contains(id) || parentRepo.contains(id)
|
||||
}
|
||||
|
||||
fun <ObjectType, IdType> ReadCRUDRepo<ObjectType, IdType>.cached(
|
||||
kvCache: KVCache<IdType, ObjectType>,
|
||||
idGetter: (ObjectType) -> IdType
|
||||
) = ReadCRUDCacheRepo(this, kvCache, idGetter)
|
||||
|
||||
open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
||||
protected open val parentRepo: WriteCRUDRepo<ObjectType, IdType, InputValueType>,
|
||||
protected open val kvCache: KVCache<IdType, ObjectType>,
|
||||
protected open val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||
protected open val idGetter: (ObjectType) -> IdType
|
||||
) : WriteCRUDRepo<ObjectType, IdType, InputValueType>, CacheRepo {
|
||||
override val newObjectsFlow: Flow<ObjectType> by parentRepo::newObjectsFlow
|
||||
override val updatedObjectsFlow: Flow<ObjectType> by parentRepo::updatedObjectsFlow
|
||||
override val deletedObjectsIdsFlow: Flow<IdType> by parentRepo::deletedObjectsIdsFlow
|
||||
|
||||
val deletedObjectsFlowJob = parentRepo.deletedObjectsIdsFlow.onEach {
|
||||
kvCache.unset(it)
|
||||
}.launchIn(scope)
|
||||
|
||||
override suspend fun deleteById(ids: List<IdType>) = parentRepo.deleteById(ids)
|
||||
|
||||
override suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType> {
|
||||
val updated = parentRepo.update(values)
|
||||
|
||||
kvCache.unset(values.map { it.id })
|
||||
kvCache.set(updated.associateBy { idGetter(it) })
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
|
||||
return parentRepo.update(id, value) ?.also {
|
||||
kvCache.unset(id)
|
||||
kvCache.set(idGetter(it), it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun create(values: List<InputValueType>): List<ObjectType> {
|
||||
val created = parentRepo.create(values)
|
||||
|
||||
kvCache.set(
|
||||
created.associateBy { idGetter(it) }
|
||||
)
|
||||
|
||||
return created
|
||||
}
|
||||
}
|
||||
|
||||
fun <ObjectType, IdType, InputType> WriteCRUDRepo<ObjectType, IdType, InputType>.caching(
|
||||
kvCache: KVCache<IdType, ObjectType>,
|
||||
scope: CoroutineScope,
|
||||
idGetter: (ObjectType) -> IdType
|
||||
) = WriteCRUDCacheRepo(this, kvCache, scope, idGetter)
|
||||
|
||||
|
||||
open class CRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
||||
parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
|
||||
override val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
|
||||
kvCache: KVCache<IdType, ObjectType>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||
idGetter: (ObjectType) -> IdType
|
||||
@@ -27,8 +82,17 @@ open class CRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
||||
parentRepo,
|
||||
kvCache,
|
||||
idGetter
|
||||
), CRUDRepo<ObjectType, IdType, InputValueType>, WriteCRUDRepo<ObjectType, IdType, InputValueType> by parentRepo {
|
||||
protected val onNewJob = parentRepo.newObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
||||
protected val onUpdatedJob = parentRepo.updatedObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
||||
protected val onRemoveJob = parentRepo.deletedObjectsIdsFlow.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||
}
|
||||
),
|
||||
WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo(
|
||||
parentRepo,
|
||||
kvCache,
|
||||
scope,
|
||||
idGetter
|
||||
),
|
||||
CRUDRepo<ObjectType, IdType, InputValueType>
|
||||
|
||||
fun <ObjectType, IdType, InputType> CRUDRepo<ObjectType, IdType, InputType>.cached(
|
||||
kvCache: KVCache<IdType, ObjectType>,
|
||||
scope: CoroutineScope,
|
||||
idGetter: (ObjectType) -> IdType
|
||||
) = CRUDCacheRepo(this, kvCache, scope, idGetter)
|
||||
|
3
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CacheRepo.kt
vendored
Normal file
3
repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CacheRepo.kt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
interface CacheRepo
|
@@ -1,41 +1,6 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
interface KVCache<K, V> : KeyValueRepo<K, V>
|
||||
|
||||
open class SimpleKVCache<K, V>(
|
||||
protected val cachedValuesCount: Int,
|
||||
private val kvParent: KeyValueRepo<K, V> = MapKeyValueRepo<K, V>()
|
||||
) : KVCache<K, V>, KeyValueRepo<K, V> by kvParent {
|
||||
protected open val cacheStack = ArrayList<K>(cachedValuesCount)
|
||||
protected val syncMutex = Mutex()
|
||||
|
||||
protected suspend fun makeUnset(toUnset: List<K>) {
|
||||
cacheStack.removeAll(toUnset)
|
||||
kvParent.unset(toUnset)
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<K, V>) {
|
||||
syncMutex.withLock {
|
||||
if (toSet.size > cachedValuesCount) {
|
||||
cacheStack.clear()
|
||||
|
||||
kvParent.unset(getAllWithNextPaging { kvParent.keys(it) })
|
||||
val keysToInclude = toSet.keys.drop(toSet.size - cachedValuesCount)
|
||||
|
||||
cacheStack.addAll(keysToInclude)
|
||||
kvParent.set(keysToInclude.associateWith { toSet.getValue(it) })
|
||||
} else {
|
||||
makeUnset(cacheStack.take(toSet.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unset(toUnset: List<K>) {
|
||||
syncMutex.withLock { makeUnset(toUnset) }
|
||||
}
|
||||
}
|
||||
@Deprecated("Replaced", ReplaceWith("KVCache", "dev.inmo.micro_utils.repos.cache.cache.KVCache"))
|
||||
typealias KVCache<K, V> = dev.inmo.micro_utils.repos.cache.cache.KVCache<K, V>
|
||||
@Deprecated("Replaced", ReplaceWith("SimpleKVCache", "dev.inmo.micro_utils.repos.cache.cache.SimpleKVCache"))
|
||||
typealias SimpleKVCache<K, V> = dev.inmo.micro_utils.repos.cache.cache.SimpleKVCache<K, V>
|
||||
|
@@ -1,25 +1,35 @@
|
||||
package dev.inmo.micro_utils.repos.cache
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.cache.cache.KVCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
open class ReadKeyValueCacheRepo<Key,Value>(
|
||||
protected val parentRepo: ReadKeyValueRepo<Key, Value>,
|
||||
protected val kvCache: KVCache<Key, Value>,
|
||||
) : ReadKeyValueRepo<Key,Value> by parentRepo {
|
||||
protected open val parentRepo: ReadKeyValueRepo<Key, Value>,
|
||||
protected open val kvCache: KVCache<Key, Value>,
|
||||
) : ReadKeyValueRepo<Key,Value> by parentRepo, CacheRepo {
|
||||
override suspend fun get(k: Key): Value? = kvCache.get(k) ?: parentRepo.get(k) ?.also { kvCache.set(k, it) }
|
||||
override suspend fun contains(key: Key): Boolean = kvCache.contains(key) || parentRepo.contains(key)
|
||||
}
|
||||
|
||||
fun <Key, Value> ReadKeyValueRepo<Key, Value>.cached(
|
||||
kvCache: KVCache<Key, Value>
|
||||
) = ReadKeyValueCacheRepo(this, kvCache)
|
||||
|
||||
open class KeyValueCacheRepo<Key,Value>(
|
||||
parentRepo: KeyValueRepo<Key, Value>,
|
||||
kvCache: KVCache<Key, Value>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) : ReadKeyValueCacheRepo<Key,Value>(parentRepo, kvCache), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by parentRepo {
|
||||
) : ReadKeyValueCacheRepo<Key,Value>(parentRepo, kvCache), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by parentRepo, CacheRepo {
|
||||
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
|
||||
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||
}
|
||||
|
||||
fun <Key, Value> KeyValueRepo<Key, Value>.cached(
|
||||
kvCache: KVCache<Key, Value>,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) = KeyValueCacheRepo(this, kvCache, scope)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user