mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-10-11 02:10:31 +00:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
ccc1dd7857 | |||
5d450e0632 | |||
cd3838f321 |
2
.github/workflows/packages_push.yml
vendored
2
.github/workflows/packages_push.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
run: ./gradlew build
|
run: ./gradlew build
|
||||||
- name: Publish
|
- name: Publish
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
|
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||||
env:
|
env:
|
||||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
155
CHANGELOG.md
155
CHANGELOG.md
@@ -1,160 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 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
|
## 0.9.7
|
||||||
|
|
||||||
* `Repos`:
|
* `Repos`:
|
||||||
|
@@ -10,7 +10,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.appCompat.resources
|
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,13 +10,13 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.recyclerView
|
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
build.gradle
13
build.gradle
@@ -7,12 +7,12 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath libs.buildscript.kt.gradle
|
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||||
classpath libs.buildscript.kt.serialization
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath libs.buildscript.jb.dokka
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
classpath libs.buildscript.gh.release
|
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||||
classpath libs.buildscript.android.gradle
|
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||||
classpath libs.buildscript.android.dexcount
|
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,6 @@ allprojects {
|
|||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
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
|
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,9 +0,0 @@
|
|||||||
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() }
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
|
|
@@ -1,13 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
|
@@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
|
|
||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
|
@@ -43,7 +43,6 @@ private inline fun <T> performChanges(
|
|||||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||||
changedList.addAll(
|
changedList.addAll(
|
||||||
potentialChanges.take(i).mapNotNull {
|
potentialChanges.take(i).mapNotNull {
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -122,10 +121,7 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
||||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
|
||||||
})
|
|
||||||
potentiallyChangedObjects.clear()
|
potentiallyChangedObjects.clear()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -157,22 +153,3 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
|||||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||||
other: Iterable<T>
|
other: Iterable<T>
|
||||||
) = calculateDiff(other, strictComparison = true)
|
) = 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -27,13 +27,20 @@ sealed interface Either<T1, T2> {
|
|||||||
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
|
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
|
||||||
val t2: T2?
|
val t2: T2?
|
||||||
get() = optionalT2.dataOrNull()
|
get() = optionalT2.dataOrNull()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T1, T2> serializer(
|
||||||
|
t1Serializer: KSerializer<T1>,
|
||||||
|
t2Serializer: KSerializer<T2>,
|
||||||
|
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EitherSerializer<T1, T2>(
|
class EitherSerializer<T1, T2>(
|
||||||
t1Serializer: KSerializer<T1>,
|
t1Serializer: KSerializer<T1>,
|
||||||
t2Serializer: KSerializer<T2>,
|
t2Serializer: KSerializer<T2>,
|
||||||
) : KSerializer<Either<T1, T2>> {
|
) : KSerializer<Either<T1, T2>> {
|
||||||
@OptIn(InternalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||||
"TypedSerializer",
|
"TypedSerializer",
|
||||||
SerialKind.CONTEXTUAL
|
SerialKind.CONTEXTUAL
|
||||||
@@ -44,6 +51,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||||
return decoder.decodeStructure(descriptor) {
|
return decoder.decodeStructure(descriptor) {
|
||||||
var type: String? = null
|
var type: String? = null
|
||||||
@@ -75,6 +83,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||||
encoder.encodeStructure(descriptor) {
|
encoder.encodeStructure(descriptor) {
|
||||||
when (value) {
|
when (value) {
|
||||||
|
@@ -32,7 +32,7 @@ class DiffUtilsTests {
|
|||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
for ((i, _) in withIndex) {
|
for ((i, v) in withIndex) {
|
||||||
if (i + count > oldList.lastIndex) {
|
if (i + count > oldList.lastIndex) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ class DiffUtilsTests {
|
|||||||
val oldList = (0 until 10).map { it.toString() }
|
val oldList = (0 until 10).map { it.toString() }
|
||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (step in oldList.indices) {
|
for (step in 0 until oldList.size) {
|
||||||
for ((i, _) in withIndex) {
|
for ((i, v) in withIndex) {
|
||||||
val mutable = oldList.toMutableList()
|
val mutable = oldList.toMutableList()
|
||||||
val changes = (
|
val changes = (
|
||||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||||
@@ -73,83 +73,4 @@ 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,61 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@@ -1,38 +0,0 @@
|
|||||||
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)
|
|
@@ -1,8 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
|
|
@@ -1,30 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
|
|
@@ -1,14 +0,0 @@
|
|||||||
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,17 +10,12 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
}
|
|
||||||
}
|
|
||||||
jsMain {
|
|
||||||
dependencies {
|
|
||||||
api project(":micro_utils.common")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines.android
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
@@ -1,14 +0,0 @@
|
|||||||
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)
|
|
@@ -1,26 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
|
@@ -1,28 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,42 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@@ -26,12 +26,12 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
buildToolsVersion "$android_buildToolsVersion"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||||
versionCode "${android_code_version}".toInteger()
|
versionCode "${android_code_version}".toInteger()
|
||||||
versionName "$version"
|
versionName "$version"
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,6 @@ allprojects {
|
|||||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||||
|
|
||||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
|
||||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||||
|
|
||||||
|
@@ -105,16 +105,6 @@ open class DefaultStatesMachine <T: State>(
|
|||||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||||
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
|
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
|
||||||
}
|
}
|
||||||
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
|
|
||||||
launch {
|
|
||||||
statesJobsMutex.withLock {
|
|
||||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
|
||||||
if (stateInMap === removedState) {
|
|
||||||
statesJobs[stateInMap] ?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statesManager.getActiveStates().forEach {
|
statesManager.getActiveStates().forEach {
|
||||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||||
|
@@ -26,12 +26,6 @@ open class DefaultUpdatableStatesMachine<T : State>(
|
|||||||
), UpdatableStatesMachine<T> {
|
), UpdatableStatesMachine<T> {
|
||||||
protected val jobsStates = mutableMapOf<Job, 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) {
|
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||||
statesJobsMutex.withLock {
|
statesJobsMutex.withLock {
|
||||||
if (compare(previousState, actualState)) {
|
if (compare(previousState, actualState)) {
|
||||||
@@ -58,13 +52,7 @@ 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) {
|
override suspend fun updateChain(currentState: T, newState: T) {
|
||||||
statesManager.update(currentState, newState)
|
statesManager.update(currentState, newState)
|
||||||
|
@@ -37,49 +37,36 @@ interface DefaultStatesManagerRepo<T : State> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
|
* @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
|
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
|
||||||
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
|
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||||
* 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
|
* 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
|
* new state by using [endChain] with that state
|
||||||
*/
|
*/
|
||||||
open class DefaultStatesManager<T : State>(
|
class DefaultStatesManager<T : State>(
|
||||||
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
|
||||||
) : StatesManager<T> {
|
) : StatesManager<T> {
|
||||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||||
protected val _onStartChain = MutableSharedFlow<T>(0)
|
private val _onStartChain = MutableSharedFlow<T>(0)
|
||||||
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
||||||
protected val _onEndChain = MutableSharedFlow<T>(0)
|
private val _onEndChain = MutableSharedFlow<T>(0)
|
||||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
protected val mapMutex = Mutex()
|
private 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 {
|
override suspend fun update(old: T, new: T) = mapMutex.withLock {
|
||||||
val stateByOldContext: T? = repo.getContextState(old.context)
|
val stateByOldContext: T? = repo.getContextState(old.context)
|
||||||
when {
|
when {
|
||||||
stateByOldContext != old -> return@withLock
|
stateByOldContext != old -> return@withLock
|
||||||
stateByOldContext == null || old.context == new.context -> {
|
stateByOldContext == null || old.context == new.context -> {
|
||||||
repo.removeState(old)
|
|
||||||
repo.set(new)
|
repo.set(new)
|
||||||
_onChainStateUpdated.emit(old to new)
|
_onChainStateUpdated.emit(old to new)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val stateOnNewOneContext = repo.getContextState(new.context)
|
val stateOnNewOneContext = repo.getContextState(new.context)
|
||||||
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||||
repo.removeState(old)
|
repo.removeState(old)
|
||||||
repo.set(new)
|
repo.set(new)
|
||||||
@@ -90,17 +77,13 @@ open class DefaultStatesManager<T : State>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
override suspend fun startChain(state: T) = mapMutex.withLock {
|
||||||
val stateOnContext = repo.getContextState(state.context)
|
if (!repo.contains(state.context)) {
|
||||||
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
|
||||||
stateOnContext ?.let {
|
|
||||||
endChainWithoutLock(it)
|
|
||||||
}
|
|
||||||
repo.set(state)
|
repo.set(state)
|
||||||
_onStartChain.emit(state)
|
_onStartChain.emit(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open suspend fun endChainWithoutLock(state: T) {
|
private suspend fun endChainWithoutLock(state: T) {
|
||||||
if (repo.getContextState(state.context) == state) {
|
if (repo.getContextState(state.context) == state) {
|
||||||
repo.removeState(state)
|
repo.removeState(state)
|
||||||
_onEndChain.emit(state)
|
_onEndChain.emit(state)
|
||||||
|
@@ -12,6 +12,5 @@ import kotlinx.coroutines.flow.*
|
|||||||
*/
|
*/
|
||||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
||||||
fun <T: State> InMemoryStatesManager(
|
fun <T: State> InMemoryStatesManager(
|
||||||
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
|
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||||
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)
|
||||||
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)
|
|
||||||
|
@@ -7,12 +7,43 @@ android.useAndroidX=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx2g
|
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.13
|
||||||
|
|
||||||
|
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
|
# JS NPM
|
||||||
|
|
||||||
crypto_js_version=4.1.1
|
crypto_js_version=4.1.1
|
||||||
|
|
||||||
|
# Dokka
|
||||||
|
|
||||||
|
dokka_version=1.6.10
|
||||||
|
|
||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.10.0
|
version=0.9.7
|
||||||
android_code_version=115
|
android_code_version=97
|
||||||
|
@@ -1,80 +0,0 @@
|
|||||||
[versions]
|
|
||||||
|
|
||||||
kt = "1.6.21"
|
|
||||||
kt-serialization = "1.3.2"
|
|
||||||
kt-coroutines = "1.6.1"
|
|
||||||
|
|
||||||
jb-compose = "1.2.0-alpha01-dev675"
|
|
||||||
jb-exposed = "0.38.2"
|
|
||||||
jb-dokka = "1.6.21"
|
|
||||||
|
|
||||||
klock = "2.7.0"
|
|
||||||
uuid = "0.4.0"
|
|
||||||
|
|
||||||
ktor = "2.0.0"
|
|
||||||
|
|
||||||
gh-release = "2.3.7"
|
|
||||||
|
|
||||||
android-gradle = "7.0.4"
|
|
||||||
dexcount = "3.0.1"
|
|
||||||
|
|
||||||
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" }
|
|
||||||
|
|
||||||
|
|
||||||
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
|
|
||||||
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
|
||||||
ktor-client-java = { module = "io.ktor:ktor-client-java", 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" }
|
|
||||||
|
|
||||||
|
|
||||||
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -12,7 +12,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api internalProject("micro_utils.ktor.common")
|
api internalProject("micro_utils.ktor.common")
|
||||||
api internalProject("micro_utils.coroutines")
|
api internalProject("micro_utils.coroutines")
|
||||||
api libs.ktor.client
|
api "io.ktor:ktor-client-core:$ktor_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,13 @@
|
|||||||
package dev.inmo.micro_utils.ktor.client
|
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.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.plugins.pluginOrNull
|
import io.ktor.client.features.websocket.ws
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.client.plugins.websocket.ws
|
import io.ktor.http.cio.websocket.readBytes
|
||||||
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.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import kotlinx.serialization.DeserializationStrategy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,41 +16,42 @@ import kotlinx.serialization.DeserializationStrategy
|
|||||||
*/
|
*/
|
||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
||||||
): Flow<T> {
|
): Flow<T> {
|
||||||
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
|
|
||||||
|
|
||||||
val correctedUrl = url.asCorrectWebSocketUrl
|
val correctedUrl = url.asCorrectWebSocketUrl
|
||||||
|
|
||||||
return channelFlow {
|
return channelFlow {
|
||||||
|
val producerScope = this@channelFlow
|
||||||
do {
|
do {
|
||||||
val reconnect = runCatchingSafely {
|
val reconnect = try {
|
||||||
ws(correctedUrl, requestBuilder) {
|
safely {
|
||||||
|
ws(correctedUrl) {
|
||||||
for (received in incoming) {
|
for (received in incoming) {
|
||||||
when (received) {
|
when (received) {
|
||||||
is Frame.Binary -> send(conversation(received.data))
|
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
||||||
else -> {
|
else -> {
|
||||||
close()
|
producerScope.close()
|
||||||
return@ws
|
return@ws
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
checkReconnection(null)
|
checkReconnection(null)
|
||||||
}.getOrElse { e ->
|
} catch (e: Throwable) {
|
||||||
checkReconnection(e).also {
|
checkReconnection(e).also {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
close(e)
|
producerScope.close(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (reconnect && isActive)
|
} while (reconnect)
|
||||||
|
if (!producerScope.isClosedForSend) {
|
||||||
if (isActive) {
|
safely(
|
||||||
safely {
|
{ it.printStackTrace() }
|
||||||
close()
|
) {
|
||||||
|
producerScope.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,14 +63,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
*/
|
*/
|
||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>,
|
||||||
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
|
||||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
) = createStandardWebsocketFlow(
|
) = createStandardWebsocketFlow(
|
||||||
url,
|
url,
|
||||||
checkReconnection,
|
checkReconnection
|
||||||
requestBuilder
|
|
||||||
) {
|
) {
|
||||||
serialFormat.decodeDefault(deserializer, it)
|
serialFormat.decodeDefault(deserializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -4,10 +4,8 @@ import dev.inmo.micro_utils.common.MPPFile
|
|||||||
import dev.inmo.micro_utils.common.filename
|
import dev.inmo.micro_utils.common.filename
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.forms.*
|
import io.ktor.client.request.forms.*
|
||||||
import io.ktor.client.statement.readBytes
|
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.utils.io.core.ByteReadPacket
|
import io.ktor.utils.io.core.ByteReadPacket
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
@@ -87,16 +85,9 @@ class UnifiedRequester(
|
|||||||
|
|
||||||
fun <T> createStandardWebsocketFlow(
|
fun <T> createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
checkReconnection: suspend (Throwable?) -> Boolean,
|
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||||
) = 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()
|
val defaultRequester = UnifiedRequester()
|
||||||
@@ -105,8 +96,10 @@ suspend fun <ResultType> HttpClient.uniget(
|
|||||||
url: String,
|
url: String,
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = get(url).let {
|
) = get<StandardKtorSerialInputData>(
|
||||||
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
|
url
|
||||||
|
).let {
|
||||||
|
serialFormat.decodeDefault(resultDeserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -123,12 +116,10 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
|
|||||||
bodyInfo: BodyPair<BodyType>,
|
bodyInfo: BodyPair<BodyType>,
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = post(url) {
|
) = post<StandardKtorSerialInputData>(url) {
|
||||||
setBody(
|
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||||
serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
|
||||||
)
|
|
||||||
}.let {
|
}.let {
|
||||||
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
|
serialFormat.decodeDefault(resultDeserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <ResultType> HttpClient.unimultipart(
|
suspend fun <ResultType> HttpClient.unimultipart(
|
||||||
@@ -141,7 +132,7 @@ suspend fun <ResultType> HttpClient.unimultipart(
|
|||||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
): ResultType = submitFormWithBinaryData(
|
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
|
||||||
url,
|
url,
|
||||||
formData = formData {
|
formData = formData {
|
||||||
append(
|
append(
|
||||||
@@ -157,7 +148,7 @@ suspend fun <ResultType> HttpClient.unimultipart(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
requestBuilder()
|
requestBuilder()
|
||||||
}.let { serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>()) }
|
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
|
||||||
|
|
||||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||||
url: String,
|
url: String,
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@@ -1,58 +0,0 @@
|
|||||||
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.xhr.*
|
|
||||||
|
|
||||||
suspend fun tempUpload(
|
|
||||||
fullTempUploadDraftPath: String,
|
|
||||||
file: MPPFile,
|
|
||||||
onUpload: (Long, Long) -> Unit
|
|
||||||
): TemporalFileId {
|
|
||||||
val formData = FormData()
|
|
||||||
val answer = CompletableDeferred<TemporalFileId>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
val handle = currentCoroutineContext().job.invokeOnCompletion {
|
|
||||||
runCatching {
|
|
||||||
request.abort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return runCatching {
|
|
||||||
answer.await()
|
|
||||||
}.also {
|
|
||||||
handle.dispose()
|
|
||||||
}.getOrThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
@@ -1,40 +0,0 @@
|
|||||||
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,10 +11,8 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api internalProject("micro_utils.common")
|
api internalProject("micro_utils.common")
|
||||||
api libs.kt.serialization.cbor
|
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
|
||||||
api libs.klock
|
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
||||||
api libs.uuid
|
|
||||||
api libs.ktor.io
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
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,5 +1,3 @@
|
|||||||
@file:Suppress("NOTHING_TO_INLINE")
|
|
||||||
|
|
||||||
package dev.inmo.micro_utils.ktor.common
|
package dev.inmo.micro_utils.ktor.common
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
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)
|
|
@@ -1,7 +0,0 @@
|
|||||||
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())
|
|
@@ -1,7 +0,0 @@
|
|||||||
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,11 +16,10 @@ kotlin {
|
|||||||
|
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.ktor.server
|
api "io.ktor:ktor-server:$ktor_version"
|
||||||
api libs.ktor.server.cio
|
api "io.ktor:ktor-server-cio:$ktor_version"
|
||||||
api libs.ktor.server.host.common
|
api "io.ktor:ktor-server-host-common:$ktor_version"
|
||||||
api libs.ktor.server.websockets
|
api "io.ktor:ktor-websockets:$ktor_version"
|
||||||
api libs.ktor.server.statusPages
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,31 +2,27 @@ package dev.inmo.micro_utils.ktor.server
|
|||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.http.URLProtocol
|
import io.ktor.application.featureOrNull
|
||||||
import io.ktor.server.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.server.application.pluginOrNull
|
import io.ktor.http.cio.websocket.*
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.application
|
import io.ktor.routing.application
|
||||||
import io.ktor.server.websocket.*
|
import io.ktor.websocket.webSocket
|
||||||
import io.ktor.websocket.send
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.SerializationStrategy
|
import kotlinx.serialization.SerializationStrategy
|
||||||
|
|
||||||
fun <T> Route.includeWebsocketHandling(
|
fun <T> Route.includeWebsocketHandling(
|
||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
protocol: URLProtocol? = null,
|
converter: (T) -> StandardKtorSerialInputData
|
||||||
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
|
|
||||||
) {
|
) {
|
||||||
application.apply {
|
application.apply {
|
||||||
pluginOrNull(WebSockets) ?: install(WebSockets)
|
featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets)
|
||||||
}
|
}
|
||||||
webSocket(suburl, protocol ?.name) {
|
webSocket(suburl) {
|
||||||
safely {
|
safely {
|
||||||
flow.collect {
|
flow.collect {
|
||||||
converter(it) ?.let { data ->
|
send(converter(it))
|
||||||
send(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,24 +32,10 @@ fun <T> Route.includeWebsocketHandling(
|
|||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
serializer: SerializationStrategy<T>,
|
serializer: SerializationStrategy<T>,
|
||||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
protocol: URLProtocol? = null,
|
|
||||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
|
||||||
) = includeWebsocketHandling(
|
) = includeWebsocketHandling(
|
||||||
suburl,
|
suburl,
|
||||||
flow,
|
flow
|
||||||
protocol,
|
) {
|
||||||
converter = if (filter == null) {
|
|
||||||
{
|
|
||||||
serialFormat.encodeDefault(serializer, it)
|
serialFormat.encodeDefault(serializer, it)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
{
|
|
||||||
if (filter(it)) {
|
|
||||||
serialFormat.encodeDefault(serializer, it)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
@@ -3,21 +3,25 @@ package dev.inmo.micro_utils.ktor.server
|
|||||||
import dev.inmo.micro_utils.common.*
|
import dev.inmo.micro_utils.common.*
|
||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.http.*
|
import io.ktor.application.ApplicationCall
|
||||||
import io.ktor.http.content.*
|
import io.ktor.application.call
|
||||||
import io.ktor.server.application.ApplicationCall
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.application.call
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.request.receive
|
import io.ktor.http.content.PartData
|
||||||
import io.ktor.server.request.receiveMultipart
|
import io.ktor.http.content.forEachPart
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.request.receive
|
||||||
import io.ktor.server.response.respondBytes
|
import io.ktor.request.receiveMultipart
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.response.respond
|
||||||
import io.ktor.server.websocket.WebSocketServerSession
|
import io.ktor.response.respondBytes
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.util.asStream
|
||||||
|
import io.ktor.util.cio.writeChannel
|
||||||
import io.ktor.util.pipeline.PipelineContext
|
import io.ktor.util.pipeline.PipelineContext
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.SerializationStrategy
|
import java.io.File
|
||||||
|
import java.io.File.createTempFile
|
||||||
|
|
||||||
class UnifiedRouter(
|
class UnifiedRouter(
|
||||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||||
@@ -26,10 +30,8 @@ class UnifiedRouter(
|
|||||||
fun <T> Route.includeWebsocketHandling(
|
fun <T> Route.includeWebsocketHandling(
|
||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
serializer: SerializationStrategy<T>,
|
serializer: SerializationStrategy<T>
|
||||||
protocol: URLProtocol? = null,
|
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
||||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
|
||||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
|
|
||||||
|
|
||||||
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||||
answerSerializer: SerializationStrategy<T>,
|
answerSerializer: SerializationStrategy<T>,
|
||||||
@@ -88,11 +90,6 @@ class UnifiedRouter(
|
|||||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
val default
|
|
||||||
get() = defaultUnifiedRouter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultUnifiedRouter = UnifiedRouter()
|
val defaultUnifiedRouter = UnifiedRouter()
|
||||||
@@ -193,9 +190,7 @@ suspend fun <T> ApplicationCall.uniloadMultipartFile(
|
|||||||
".${name.extension}"
|
".${name.extension}"
|
||||||
).apply {
|
).apply {
|
||||||
outputStream().use { fileStream ->
|
outputStream().use { fileStream ->
|
||||||
it.streamProvider().use {
|
it.provider().asStream().copyTo(fileStream)
|
||||||
it.copyTo(fileStream)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,9 +232,7 @@ suspend fun ApplicationCall.uniloadMultipartFile(
|
|||||||
".${name.extension}"
|
".${name.extension}"
|
||||||
).apply {
|
).apply {
|
||||||
outputStream().use { fileStream ->
|
outputStream().use { fileStream ->
|
||||||
it.streamProvider().use {
|
it.provider().asStream().copyTo(fileStream)
|
||||||
it.copyTo(fileStream)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server
|
package dev.inmo.micro_utils.ktor.server
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.server.cio.CIO
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.server.cio.CIOApplicationEngine
|
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -11,21 +10,17 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
|||||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||||
host: String = "localhost",
|
host: String = "localhost",
|
||||||
port: Int = Random.nextInt(1024, 65535),
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
|
||||||
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
|
|
||||||
block: Application.() -> Unit
|
block: Application.() -> Unit
|
||||||
): TEngine = embeddedServer(
|
): TEngine {
|
||||||
engine,
|
val env = applicationEngineEnvironment {
|
||||||
applicationEngineEnvironment {
|
|
||||||
module(block)
|
module(block)
|
||||||
connector {
|
connector {
|
||||||
this.host = host
|
this@connector.host = host
|
||||||
this.port = port
|
this@connector.port = port
|
||||||
}
|
}
|
||||||
additionalEngineEnvironmentConfigurator()
|
}
|
||||||
},
|
return embeddedServer(engine, env)
|
||||||
additionalConfigurationConfigurator
|
}
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create server with [CIO] server engine without starting of it
|
* Create server with [CIO] server engine without starting of it
|
||||||
@@ -35,31 +30,18 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
|||||||
fun createKtorServer(
|
fun createKtorServer(
|
||||||
host: String = "localhost",
|
host: String = "localhost",
|
||||||
port: Int = Random.nextInt(1024, 65535),
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
|
||||||
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
|
|
||||||
block: Application.() -> Unit
|
block: Application.() -> Unit
|
||||||
): CIOApplicationEngine = createKtorServer(
|
): ApplicationEngine = createKtorServer(CIO, host, port, block)
|
||||||
CIO,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
additionalEngineEnvironmentConfigurator,
|
|
||||||
additionalConfigurationConfigurator,
|
|
||||||
block
|
|
||||||
)
|
|
||||||
|
|
||||||
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
|
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
|
||||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||||
host: String = "localhost",
|
host: String = "localhost",
|
||||||
port: Int = Random.nextInt(1024, 65535),
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
|
||||||
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
|
|
||||||
configurators: List<KtorApplicationConfigurator>
|
configurators: List<KtorApplicationConfigurator>
|
||||||
): TEngine = createKtorServer(
|
): TEngine = createKtorServer(
|
||||||
engine,
|
engine,
|
||||||
host,
|
host,
|
||||||
port,
|
port
|
||||||
additionalEngineEnvironmentConfigurator,
|
|
||||||
additionalConfigurationConfigurator
|
|
||||||
) {
|
) {
|
||||||
configurators.forEach { it.apply { configure() } }
|
configurators.forEach { it.apply { configure() } }
|
||||||
}
|
}
|
||||||
@@ -72,7 +54,5 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
|||||||
fun createKtorServer(
|
fun createKtorServer(
|
||||||
host: String = "localhost",
|
host: String = "localhost",
|
||||||
port: Int = Random.nextInt(1024, 65535),
|
port: Int = Random.nextInt(1024, 65535),
|
||||||
configurators: List<KtorApplicationConfigurator>,
|
configurators: List<KtorApplicationConfigurator>
|
||||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
|
||||||
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
|
|
||||||
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators)
|
|
||||||
|
@@ -1,132 +0,0 @@
|
|||||||
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.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.respond(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,15 +1,14 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server.configurators
|
package dev.inmo.micro_utils.ktor.server.configurators
|
||||||
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.server.plugins.cachingheaders.CachingHeaders
|
import io.ktor.features.CachingHeaders
|
||||||
import io.ktor.server.plugins.cachingheaders.CachingHeadersConfig
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
|
|
||||||
data class ApplicationCachingHeadersConfigurator(
|
data class ApplicationCachingHeadersConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
fun interface Element { operator fun CachingHeadersConfig.invoke() }
|
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(CachingHeaders) {
|
install(CachingHeaders) {
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server.configurators
|
package dev.inmo.micro_utils.ktor.server.configurators
|
||||||
|
|
||||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator.Element
|
import io.ktor.application.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Routing
|
||||||
import io.ktor.server.routing.Routing
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ class ApplicationRoutingConfigurator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
pluginOrNull(Routing) ?.apply {
|
featureOrNull(Routing) ?.apply {
|
||||||
rootInstaller.apply { invoke() }
|
rootInstaller.apply { invoke() }
|
||||||
} ?: install(Routing) {
|
} ?: install(Routing) {
|
||||||
rootInstaller.apply { invoke() }
|
rootInstaller.apply { invoke() }
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server.configurators
|
package dev.inmo.micro_utils.ktor.server.configurators
|
||||||
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.server.sessions.Sessions
|
import io.ktor.sessions.Sessions
|
||||||
import io.ktor.server.sessions.SessionsConfig
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
|
|
||||||
class ApplicationSessionsConfigurator(
|
class ApplicationSessionsConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
fun interface Element { operator fun SessionsConfig.invoke() }
|
fun interface Element { operator fun Sessions.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(Sessions) {
|
install(Sessions) {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server.configurators
|
package dev.inmo.micro_utils.ktor.server.configurators
|
||||||
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.application.Application
|
||||||
|
|
||||||
interface KtorApplicationConfigurator {
|
interface KtorApplicationConfigurator {
|
||||||
fun Application.configure()
|
fun Application.configure()
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
package dev.inmo.micro_utils.ktor.server.configurators
|
package dev.inmo.micro_utils.ktor.server.configurators
|
||||||
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.server.plugins.statuspages.StatusPages
|
import io.ktor.features.StatusPages
|
||||||
import io.ktor.server.plugins.statuspages.StatusPagesConfig
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
|
|
||||||
class StatusPagesConfigurator(
|
class StatusPagesConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
fun interface Element { operator fun StatusPagesConfig.invoke() }
|
fun interface Element { operator fun StatusPages.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(StatusPages) {
|
install(StatusPages) {
|
||||||
|
@@ -4,8 +4,8 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath libs.buildscript.kt.gradle
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath libs.buildscript.kt.serialization
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.kt.stdlib
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation libs.kt.serialization
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
||||||
|
|
||||||
implementation libs.ktor.client
|
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||||
implementation libs.ktor.client.java
|
implementation "io.ktor:ktor-client-java:$ktor_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
mainClassName="MainKt"
|
mainClassName="MainKt"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
@@ -165,7 +164,7 @@ suspend fun main(vararg args: String) {
|
|||||||
|
|
||||||
val ietfLanguageCodes = json.decodeFromString(
|
val ietfLanguageCodes = json.decodeFromString(
|
||||||
ListSerializer(LanguageCode.serializer()),
|
ListSerializer(LanguageCode.serializer()),
|
||||||
client.get(ietfLanguageCodesLink).bodyAsText()
|
client.get(ietfLanguageCodesLink)
|
||||||
).map {
|
).map {
|
||||||
it.copy(
|
it.copy(
|
||||||
title = it.title
|
title = it.title
|
||||||
@@ -176,7 +175,7 @@ suspend fun main(vararg args: String) {
|
|||||||
}
|
}
|
||||||
val ietfLanguageCodesWithTagsMap = json.decodeFromString(
|
val ietfLanguageCodesWithTagsMap = json.decodeFromString(
|
||||||
ListSerializer(LanguageCodeWithTag.serializer()),
|
ListSerializer(LanguageCodeWithTag.serializer()),
|
||||||
client.get(ietfLanguageCodesAdditionalTagsLink).bodyAsText()
|
client.get(ietfLanguageCodesAdditionalTagsLink)
|
||||||
).filter { it.withSubtag != it.tag }.groupBy { it.tag }
|
).filter { it.withSubtag != it.tag }.groupBy { it.tag }
|
||||||
|
|
||||||
val tags = ietfLanguageCodes.map {
|
val tags = ietfLanguageCodes.map {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.mime_types
|
package dev.inmo.micro_utils.mime_types
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
import kotlinx.serialization.descriptors.*
|
import kotlinx.serialization.descriptors.*
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
@@ -15,7 +16,6 @@ fun mimeType(raw: String) = mimesCache.getOrPut(raw) {
|
|||||||
|
|
||||||
internal fun parseMimeType(raw: String): MimeType = CustomMimeType(raw)
|
internal fun parseMimeType(raw: String): MimeType = CustomMimeType(raw)
|
||||||
|
|
||||||
@Suppress("OPT_IN_USAGE")
|
|
||||||
@Serializer(MimeType::class)
|
@Serializer(MimeType::class)
|
||||||
object MimeTypeSerializer : KSerializer<MimeType> {
|
object MimeTypeSerializer : KSerializer<MimeType> {
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("mimeType", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("mimeType", PrimitiveKind.STRING)
|
||||||
|
@@ -23,7 +23,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('stdlib')
|
implementation kotlin('stdlib')
|
||||||
implementation libs.kt.serialization
|
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commonTest {
|
commonTest {
|
||||||
@@ -46,8 +46,8 @@ kotlin {
|
|||||||
androidTest {
|
androidTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-junit')
|
implementation kotlin('test-junit')
|
||||||
implementation libs.android.test.junit
|
implementation "androidx.test.ext:junit:$test_ext_junit_version"
|
||||||
implementation libs.android.espresso
|
implementation "androidx.test.espresso:espresso-core:$espresso_core"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,72 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@@ -27,5 +27,4 @@ inline fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? =
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()
|
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()
|
||||||
|
@@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.*
|
|||||||
* Example:
|
* 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__|`
|
* * `|__f__l_|` will be transformed to `|__f__l_|`
|
||||||
*
|
*
|
||||||
* @return Reversed version of this [Pagination]
|
* @return Reversed version of this [Pagination]
|
||||||
*/
|
*/
|
||||||
|
@@ -14,7 +14,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.jb.exposed
|
api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,8 +15,8 @@ kotlin {
|
|||||||
|
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.ktor.server
|
api "io.ktor:ktor-server:$ktor_version"
|
||||||
api libs.ktor.server.host.common
|
api "io.ktor:ktor-server-host-common:$ktor_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.pagination
|
package dev.inmo.micro_utils.pagination
|
||||||
|
|
||||||
|
import io.ktor.application.ApplicationCall
|
||||||
import io.ktor.http.Parameters
|
import io.ktor.http.Parameters
|
||||||
import io.ktor.server.application.ApplicationCall
|
|
||||||
|
|
||||||
val Parameters.extractPagination: Pagination
|
val Parameters.extractPagination: Pagination
|
||||||
get() = SimplePagination(
|
get() = SimplePagination(
|
||||||
|
@@ -10,10 +10,10 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
api internalProject("micro_utils.pagination.common")
|
api internalProject("micro_utils.pagination.common")
|
||||||
|
|
||||||
api libs.uuid
|
api "com.benasher44:uuid:$uuidVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.coreKtx
|
api "androidx.core:core-ktx:$core_ktx_version"
|
||||||
api internalProject("micro_utils.common")
|
api internalProject("micro_utils.common")
|
||||||
api internalProject("micro_utils.coroutines")
|
api internalProject("micro_utils.coroutines")
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
interface MapperRepo<FromKey, FromValue, ToKey, ToValue> {
|
interface MapperRepo<FromKey, FromValue, ToKey, ToValue> {
|
||||||
suspend fun FromKey.toOutKey() = this as ToKey
|
suspend fun FromKey.toOutKey() = this as ToKey
|
||||||
suspend fun FromValue.toOutValue() = this as ToValue
|
suspend fun FromValue.toOutValue() = this as ToValue
|
||||||
|
@@ -126,10 +126,9 @@ inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue>
|
|||||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
|
|
||||||
open class MapperStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
open class MapperStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||||
private val to: StandardKeyValueRepo<ToKey, ToValue>,
|
private val to: StandardKeyValueRepo<ToKey, ToValue>,
|
||||||
private val mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||||
) : StandardKeyValueRepo<FromKey, FromValue>,
|
) : StandardKeyValueRepo<FromKey, FromValue>,
|
||||||
MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper,
|
MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper,
|
||||||
ReadStandardKeyValueRepo<FromKey, FromValue> by MapperReadStandardKeyValueRepo(to, mapper),
|
ReadStandardKeyValueRepo<FromKey, FromValue> by MapperReadStandardKeyValueRepo(to, mapper),
|
||||||
|
@@ -132,7 +132,6 @@ inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue>
|
|||||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
|
|
||||||
open class MapperOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
open class MapperOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||||
private val to: OneToManyKeyValueRepo<ToKey, ToValue>,
|
private val to: OneToManyKeyValueRepo<ToKey, ToValue>,
|
||||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||||
|
@@ -13,12 +13,12 @@ interface VersionsRepo<T> : Repo {
|
|||||||
* By default, instance of this interface will check that version of table with name [tableName] is less than
|
* By default, instance of this interface will check that version of table with name [tableName] is less than
|
||||||
* [version] or is absent
|
* [version] or is absent
|
||||||
*
|
*
|
||||||
* In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less
|
* * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
|
||||||
* than parameter [version] or null, it will increase version one-by-one until database version will be equal to
|
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
|
||||||
* [version]
|
* until database version will be equal to [version]
|
||||||
*
|
*
|
||||||
* @param version Current version of table
|
* @param version Current version of table
|
||||||
* @param onCreate This callback will be called in case when repo have no information about table
|
* @param onCreate This callback will be called in case when table have no information about table
|
||||||
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
|
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
|
||||||
* "to" will always be greater than "from"
|
* "to" will always be greater than "from"
|
||||||
*/
|
*/
|
||||||
|
@@ -175,11 +175,9 @@ class FileWriteStandardKeyValueRepo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
|
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
|
||||||
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
|
|
||||||
class FileStandardKeyValueRepo(
|
class FileStandardKeyValueRepo(
|
||||||
folder: File,
|
folder: File,
|
||||||
filesChangedProcessingScope: CoroutineScope? = null
|
filesChangedProcessingScope: CoroutineScope? = null
|
||||||
) : StandardKeyValueRepo<String, File>,
|
) : StandardKeyValueRepo<String, File>,
|
||||||
WriteStandardKeyValueRepo<String, File> by FileWriteStandardKeyValueRepo(folder, filesChangedProcessingScope),
|
WriteStandardKeyValueRepo<String, File> by FileWriteStandardKeyValueRepo(folder, filesChangedProcessingScope),
|
||||||
ReadStandardKeyValueRepo<String, File> by FileReadStandardKeyValueRepo(folder) {
|
ReadStandardKeyValueRepo<String, File> by FileReadStandardKeyValueRepo(folder)
|
||||||
}
|
|
@@ -1,27 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
|
|
||||||
class CursorIterator(
|
|
||||||
private val c: Cursor
|
|
||||||
) : Iterator<Cursor> {
|
|
||||||
private var i = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
c.moveToFirst()
|
|
||||||
}
|
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return i < c.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): Cursor {
|
|
||||||
i++
|
|
||||||
return if (c.moveToNext()) {
|
|
||||||
c
|
|
||||||
} else {
|
|
||||||
throw NoSuchElementException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun Cursor.iterator(): CursorIterator = CursorIterator(this)
|
|
@@ -16,7 +16,6 @@ fun <T : Any> Context.keyValueStore(
|
|||||||
name: String = "default",
|
name: String = "default",
|
||||||
cacheValues: Boolean = false
|
cacheValues: Boolean = false
|
||||||
): StandardKeyValueRepo<String, T> {
|
): StandardKeyValueRepo<String, T> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return cache.getOrPut(name) {
|
return cache.getOrPut(name) {
|
||||||
KeyValueStore<T>(this, name, cacheValues)
|
KeyValueStore<T>(this, name, cacheValues)
|
||||||
} as KeyValueStore<T>
|
} as KeyValueStore<T>
|
||||||
@@ -63,7 +62,6 @@ class KeyValueStore<T : Any> internal constructor (
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(k: String): T? {
|
override suspend fun get(k: String): T? {
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T
|
return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,10 +73,7 @@ class KeyValueStore<T : Any> internal constructor (
|
|||||||
PaginationResult(
|
PaginationResult(
|
||||||
it.page,
|
it.page,
|
||||||
it.pagesNumber,
|
it.pagesNumber,
|
||||||
it.results.map {
|
it.results.map { it as T }.let { if (reversed) it.reversed() else it },
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
it as T
|
|
||||||
}.let { if (reversed) it.reversed() else it },
|
|
||||||
it.size
|
it.size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -143,12 +143,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}.toLong()
|
}.toLong()
|
||||||
|
|
||||||
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
|
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
|
||||||
selectDistinct(
|
selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
|
||||||
tableName,
|
|
||||||
columns = valueColumnArray,
|
|
||||||
selection = "$idColumnName=?",
|
|
||||||
selectionArgs = arrayOf(k.keyAsString())
|
|
||||||
).use {
|
|
||||||
it.count
|
it.count
|
||||||
}
|
}
|
||||||
}.toLong()
|
}.toLong()
|
||||||
|
@@ -17,19 +17,13 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
ExposedCRUDRepo<ObjectType, IdType>,
|
ExposedCRUDRepo<ObjectType, IdType>,
|
||||||
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||||
{
|
{
|
||||||
protected val _newObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
|
protected val newObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
|
||||||
protected val _updatedObjectsFlow = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
|
protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(replyCacheInFlows, flowsChannelsSize)
|
||||||
protected val _deletedObjectsIdsFlow = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
|
protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(replyCacheInFlows, flowsChannelsSize)
|
||||||
@Deprecated("Renamed", ReplaceWith("_newObjectsFlow"))
|
|
||||||
protected val newObjectsChannel = _newObjectsFlow
|
|
||||||
@Deprecated("Renamed", ReplaceWith("_updatedObjectsFlow"))
|
|
||||||
protected val updateObjectsChannel = _updatedObjectsFlow
|
|
||||||
@Deprecated("Renamed", ReplaceWith("_deletedObjectsIdsFlow"))
|
|
||||||
protected val deleteObjectsIdsChannel = _deletedObjectsIdsFlow
|
|
||||||
|
|
||||||
override val newObjectsFlow: Flow<ObjectType> = _newObjectsFlow.asSharedFlow()
|
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
|
||||||
override val updatedObjectsFlow: Flow<ObjectType> = _updatedObjectsFlow.asSharedFlow()
|
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
|
||||||
override val deletedObjectsIdsFlow: Flow<IdType> = _deletedObjectsIdsFlow.asSharedFlow()
|
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
|
||||||
|
|
||||||
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
|
protected abstract fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType
|
||||||
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
|
abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
|
||||||
@@ -49,7 +43,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
return transaction(db = database) {
|
return transaction(db = database) {
|
||||||
values.map { value -> createWithoutNotification(value) }
|
values.map { value -> createWithoutNotification(value) }
|
||||||
}.onEach {
|
}.onEach {
|
||||||
_newObjectsFlow.emit(it)
|
newObjectsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +74,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
onBeforeUpdate(listOf(id to value))
|
onBeforeUpdate(listOf(id to value))
|
||||||
return updateWithoutNotification(id, value).also {
|
return updateWithoutNotification(id, value).also {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
_updatedObjectsFlow.emit(it)
|
updateObjectsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,25 +85,16 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
|
|||||||
values.map { (id, value) -> updateWithoutNotification(id, value) }
|
values.map { (id, value) -> updateWithoutNotification(id, value) }
|
||||||
}.filterNotNull()
|
}.filterNotNull()
|
||||||
).onEach {
|
).onEach {
|
||||||
_updatedObjectsFlow.emit(it)
|
updateObjectsChannel.emit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
|
protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
|
||||||
override suspend fun deleteById(ids: List<IdType>) {
|
override suspend fun deleteById(ids: List<IdType>) {
|
||||||
onBeforeDelete(ids)
|
onBeforeDelete(ids)
|
||||||
transaction(db = database) {
|
transaction(db = database) {
|
||||||
val deleted = deleteWhere(null, null) {
|
deleteWhere(null, null) {
|
||||||
selectByIds(ids)
|
selectByIds(ids)
|
||||||
}
|
}
|
||||||
if (deleted == ids.size) {
|
|
||||||
ids
|
|
||||||
} else {
|
|
||||||
ids.filter {
|
|
||||||
select { selectById(it) }.limit(1).none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
_deletedObjectsIdsFlow.emit(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.repos.exposed
|
package dev.inmo.micro_utils.repos.exposed
|
||||||
|
|
||||||
import dev.inmo.micro_utils.repos.Repo
|
import dev.inmo.micro_utils.repos.Repo
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
|
||||||
interface ExposedRepo : Repo, FieldSet {
|
interface ExposedRepo : Repo {
|
||||||
val database: Database
|
val database: Database
|
||||||
val selectAll: Transaction.() -> Query
|
|
||||||
get() = { (this@ExposedRepo as FieldSet).selectAll() }
|
|
||||||
}
|
}
|
@@ -19,7 +19,7 @@ class ExposedStandardVersionsRepoProxy(
|
|||||||
override val database: Database
|
override val database: Database
|
||||||
) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo {
|
) : StandardVersionsRepoProxy<Database>, Table("ExposedVersionsProxy"), ExposedRepo {
|
||||||
val tableNameColumn = text("tableName")
|
val tableNameColumn = text("tableName")
|
||||||
val tableVersionColumn = integer("tableVersion")
|
val tableVersionColumn = integer("tableName")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initTable()
|
initTable()
|
||||||
|
@@ -7,10 +7,10 @@ import dev.inmo.micro_utils.pagination.PaginationResult
|
|||||||
import dev.inmo.micro_utils.pagination.extractPagination
|
import dev.inmo.micro_utils.pagination.extractPagination
|
||||||
import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo
|
import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.crud.*
|
import dev.inmo.micro_utils.repos.ktor.common.crud.*
|
||||||
|
import io.ktor.application.call
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.application.call
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.get
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.UnifiedRouter
|
|||||||
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
||||||
import dev.inmo.micro_utils.repos.StandardCRUDRepo
|
import dev.inmo.micro_utils.repos.StandardCRUDRepo
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.route
|
import io.ktor.routing.route
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
|
||||||
fun <ObjectType, IdType, InputValue> Route.configureStandardCrudRepoRoutes(
|
fun <ObjectType, IdType, InputValue> Route.configureStandardCrudRepoRoutes(
|
||||||
|
@@ -5,9 +5,10 @@ import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|||||||
import dev.inmo.micro_utils.ktor.server.*
|
import dev.inmo.micro_utils.ktor.server.*
|
||||||
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
|
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.crud.*
|
import dev.inmo.micro_utils.repos.ktor.common.crud.*
|
||||||
|
import io.ktor.application.call
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.routing.post
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.*
|
import kotlinx.serialization.builtins.*
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.UnifiedRouter
|
|||||||
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
||||||
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.route
|
import io.ktor.routing.route
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
|
||||||
fun <K, V> Route.configureStandardKeyValueRepoRoutes(
|
fun <K, V> Route.configureStandardKeyValueRepoRoutes(
|
||||||
|
@@ -8,10 +8,10 @@ import dev.inmo.micro_utils.pagination.extractPagination
|
|||||||
import dev.inmo.micro_utils.repos.ReadStandardKeyValueRepo
|
import dev.inmo.micro_utils.repos.ReadStandardKeyValueRepo
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
|
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
|
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
|
||||||
|
import io.ktor.application.call
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.application.call
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.get
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.*
|
|||||||
import dev.inmo.micro_utils.repos.WriteStandardKeyValueRepo
|
import dev.inmo.micro_utils.repos.WriteStandardKeyValueRepo
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
|
import dev.inmo.micro_utils.repos.ktor.common.key_value.*
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.routing.post
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.*
|
import kotlinx.serialization.builtins.*
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ import dev.inmo.micro_utils.ktor.server.UnifiedRouter
|
|||||||
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
import dev.inmo.micro_utils.ktor.server.standardKtorSerialFormatContentType
|
||||||
import dev.inmo.micro_utils.repos.OneToManyKeyValueRepo
|
import dev.inmo.micro_utils.repos.OneToManyKeyValueRepo
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.route
|
import io.ktor.routing.route
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
|
||||||
fun <Key, Value> Route.configureOneToManyKeyValueRepoRoutes(
|
fun <Key, Value> Route.configureOneToManyKeyValueRepoRoutes(
|
||||||
|
@@ -6,14 +6,15 @@ import dev.inmo.micro_utils.ktor.server.*
|
|||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||||
import dev.inmo.micro_utils.pagination.extractPagination
|
import dev.inmo.micro_utils.pagination.extractPagination
|
||||||
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
|
import dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo
|
||||||
|
import dev.inmo.micro_utils.repos.ktor.common.*
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.keyParameterName
|
import dev.inmo.micro_utils.repos.ktor.common.keyParameterName
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
|
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
|
import dev.inmo.micro_utils.repos.ktor.common.valueParameterName
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.reversedParameterName
|
import dev.inmo.micro_utils.repos.ktor.common.reversedParameterName
|
||||||
|
import io.ktor.application.call
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.application.call
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.get
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
|
||||||
|
@@ -5,9 +5,10 @@ import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat
|
|||||||
import dev.inmo.micro_utils.ktor.server.*
|
import dev.inmo.micro_utils.ktor.server.*
|
||||||
import dev.inmo.micro_utils.repos.WriteOneToManyKeyValueRepo
|
import dev.inmo.micro_utils.repos.WriteOneToManyKeyValueRepo
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
|
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.*
|
||||||
|
import io.ktor.application.call
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.routing.post
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.*
|
import kotlinx.serialization.builtins.*
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ rootProject.name='micro_utils'
|
|||||||
|
|
||||||
String[] includes = [
|
String[] includes = [
|
||||||
":common",
|
":common",
|
||||||
":common:compose",
|
|
||||||
":matrix",
|
":matrix",
|
||||||
":crypto",
|
":crypto",
|
||||||
":selector:common",
|
":selector:common",
|
||||||
@@ -24,7 +23,6 @@ String[] includes = [
|
|||||||
":ktor:common",
|
":ktor:common",
|
||||||
":ktor:client",
|
":ktor:client",
|
||||||
":coroutines",
|
":coroutines",
|
||||||
":coroutines:compose",
|
|
||||||
":android:recyclerview",
|
":android:recyclerview",
|
||||||
":android:alerts:common",
|
":android:alerts:common",
|
||||||
":android:alerts:recyclerview",
|
":android:alerts:recyclerview",
|
||||||
@@ -40,13 +38,11 @@ String[] includes = [
|
|||||||
|
|
||||||
|
|
||||||
includes.each { originalName ->
|
includes.each { originalName ->
|
||||||
String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replace(":", File.separator)}"
|
String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}"
|
||||||
String projectName = "${rootProject.name}${originalName.replace(":", ".")}"
|
String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}"
|
||||||
String projectIdentifier = ":${projectName}"
|
String projectIdentifier = ":${projectName}"
|
||||||
include projectIdentifier
|
include projectIdentifier
|
||||||
ProjectDescriptor project = project(projectIdentifier)
|
ProjectDescriptor project = project(projectIdentifier)
|
||||||
project.name = projectName
|
project.name = projectName
|
||||||
project.projectDir = new File(projectDirectory)
|
project.projectDir = new File(projectDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
enableFeaturePreview("VERSION_CATALOGS")
|
|
||||||
|
Reference in New Issue
Block a user