mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-10-12 19:00:28 +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 }}
|
||||||
|
146
CHANGELOG.md
146
CHANGELOG.md
@@ -1,151 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.9.24
|
|
||||||
|
|
||||||
* `Ktor`:
|
|
||||||
* `Common`:
|
|
||||||
* New extension fun `MPPFile#input`
|
|
||||||
|
|
||||||
## 0.9.23
|
|
||||||
|
|
||||||
* `Repos`:
|
|
||||||
* `Exposed`:
|
|
||||||
* New property `ExposedRepo#selectAll` to retrieve all the rows in the table
|
|
||||||
|
|
||||||
## 0.9.22
|
|
||||||
|
|
||||||
* `Ktor`:
|
|
||||||
* `Server`:
|
|
||||||
* Now `createKtorServer` fun is fully customizable
|
|
||||||
|
|
||||||
## 0.9.21
|
|
||||||
|
|
||||||
* `Repos`:
|
|
||||||
* `Exposed`:
|
|
||||||
* fixes in `AbstractExposedWriteCRUDRepo`
|
|
||||||
|
|
||||||
## 0.9.20
|
|
||||||
|
|
||||||
* `Repos`:
|
|
||||||
* `Common`:
|
|
||||||
* Fixes in `OneToManyAndroidRepo`
|
|
||||||
* New `CursorIterator`
|
|
||||||
|
|
||||||
## 0.9.19
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Coroutines`: `1.6.0` -> `1.6.1`
|
|
||||||
* `Repos`:
|
|
||||||
* `Exposed`:
|
|
||||||
* Fixes in `ExposedStandardVersionsRepoProxy`
|
|
||||||
|
|
||||||
## 0.9.18
|
|
||||||
|
|
||||||
* `Common`
|
|
||||||
* New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
|
|
||||||
|
|
||||||
## 0.9.17
|
|
||||||
|
|
||||||
* `Common`:
|
|
||||||
* New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
|
|
||||||
* `Coroutines`:
|
|
||||||
* New extension `Element.visibilityFlow()`
|
|
||||||
* `FSM`:
|
|
||||||
* Now it is possible to resolve conflicts on `startChain`
|
|
||||||
|
|
||||||
## 0.9.16
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Klock`: `2.6.3` -> `2.7.0`
|
|
||||||
* `Common`:
|
|
||||||
* New extension `Node#onRemoved`
|
|
||||||
* `Compose`:
|
|
||||||
* New extension `Composition#linkWithRoot` for removing of composition with root element
|
|
||||||
* `Coroutines`:
|
|
||||||
* `Compose`:
|
|
||||||
* New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
|
|
||||||
|
|
||||||
## 0.9.15
|
|
||||||
|
|
||||||
* `FSM`:
|
|
||||||
* Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
|
|
||||||
* `DefaultStatesManager` now is extendable
|
|
||||||
* `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
|
|
||||||
|
|
||||||
## 0.9.14
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Klock`: `2.6.2` -> `2.6.3`
|
|
||||||
* `Ktor`: `1.6.7` -> `1.6.8`
|
|
||||||
* `Ktor`:
|
|
||||||
* Add temporal files uploading functionality (for clients to upload and for server to receive)
|
|
||||||
|
|
||||||
## 0.9.13
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Compose`: `1.1.0` -> `1.1.1`
|
|
||||||
|
|
||||||
## 0.9.12
|
|
||||||
|
|
||||||
* `Common`:
|
|
||||||
* `JS`:
|
|
||||||
* New function `openLink`
|
|
||||||
* New function `selectFile`
|
|
||||||
* New function `triggerDownloadFile`
|
|
||||||
* `Compose`:
|
|
||||||
* Created :)
|
|
||||||
* `Common`:
|
|
||||||
* `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
|
|
||||||
* `JS`:
|
|
||||||
* `openLink` on top of `openLink` with `String` target from common
|
|
||||||
* `Coroutines`:
|
|
||||||
* `Compose`:
|
|
||||||
* `Common`:
|
|
||||||
* New extension `Flow.toMutableState`
|
|
||||||
* New extension `StateFlow.toMutableState`
|
|
||||||
* `JS`:
|
|
||||||
* New function `selectFileOrThrow` on top of `selectFile` from `common`
|
|
||||||
* New function `selectFileOrNull` on top of `selectFile` from `common`
|
|
||||||
|
|
||||||
## 0.9.11
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Klock`: `2.6.1` -> `2.6.2`
|
|
||||||
* `Coroutines`:
|
|
||||||
* `Compose`:
|
|
||||||
* Created :)
|
|
||||||
* New extensions and function:
|
|
||||||
* `Composition#linkWithJob`
|
|
||||||
* `Composition#linkWithContext`
|
|
||||||
* `renderComposableAndLinkToContext`
|
|
||||||
|
|
||||||
## 0.9.10
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Klock`: `2.5.2` -> `2.6.1`
|
|
||||||
* Ktor:
|
|
||||||
* Client:
|
|
||||||
* New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
|
|
||||||
* Server:
|
|
||||||
* Now it is possible to filter data in `Route#includeWebsocketHandling`
|
|
||||||
* Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
|
|
||||||
* Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
|
|
||||||
|
|
||||||
## 0.9.9
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Klock`: `2.5.1` -> `2.5.2`
|
|
||||||
* `Common`:
|
|
||||||
* Add new diff tool - `applyDiff`
|
|
||||||
* Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
|
|
||||||
|
|
||||||
## 0.9.8
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Exposed`: `0.37.2` -> `0.37.3`
|
|
||||||
* `Klock`: `2.4.13` -> `2.5.1`
|
|
||||||
* `AppCompat`: `1.4.0` -> `1.4.1`
|
|
||||||
|
|
||||||
## 0.9.7
|
## 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
build.gradle
12
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"/>
|
|
@@ -153,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -51,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
|
||||||
@@ -82,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) {
|
||||||
|
@@ -54,7 +54,7 @@ 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, v) in withIndex) {
|
for ((i, v) in withIndex) {
|
||||||
val mutable = oldList.toMutableList()
|
val mutable = oldList.toMutableList()
|
||||||
val changes = (
|
val changes = (
|
||||||
@@ -73,78 +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, v) in withIndex) {
|
|
||||||
if (i + count > oldList.lastIndex) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
|
||||||
val mutable = oldList.toMutableList()
|
|
||||||
mutable.addAll(i, addedSublist)
|
|
||||||
val mutableOldList = oldList.toMutableList()
|
|
||||||
|
|
||||||
mutableOldList.applyDiff(mutable)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
mutable,
|
|
||||||
mutableOldList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testThatSimpleChangesApplyWorks() {
|
|
||||||
val oldList = (0 until 10).map { it.toString() }
|
|
||||||
val withIndex = oldList.withIndex()
|
|
||||||
|
|
||||||
for (step in oldList.indices) {
|
|
||||||
for ((i, v) in withIndex) {
|
|
||||||
val mutable = oldList.toMutableList()
|
|
||||||
val changes = (
|
|
||||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
|
||||||
).map { index ->
|
|
||||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
|
||||||
mutable[index] = it.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val mutableOldList = oldList.toMutableList()
|
|
||||||
mutableOldList.applyDiff(mutable)
|
|
||||||
assertEquals(
|
|
||||||
mutable,
|
|
||||||
mutableOldList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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,22 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
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.9.24
|
version=0.9.7
|
||||||
android_code_version=114
|
android_code_version=97
|
||||||
|
@@ -1,78 +0,0 @@
|
|||||||
[versions]
|
|
||||||
|
|
||||||
kt = "1.6.10"
|
|
||||||
kt-serialization = "1.3.2"
|
|
||||||
kt-coroutines = "1.6.1"
|
|
||||||
|
|
||||||
jb-compose = "1.1.1"
|
|
||||||
jb-exposed = "0.37.3"
|
|
||||||
jb-dokka = "1.6.10"
|
|
||||||
|
|
||||||
klock = "2.7.0"
|
|
||||||
uuid = "0.4.0"
|
|
||||||
|
|
||||||
ktor = "1.6.8"
|
|
||||||
|
|
||||||
gh-release = "2.2.12"
|
|
||||||
|
|
||||||
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" }
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,6 @@ 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.features.websocket.ws
|
import io.ktor.client.features.websocket.ws
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
|
||||||
import io.ktor.http.cio.websocket.Frame
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.http.cio.websocket.readBytes
|
import io.ktor.http.cio.websocket.readBytes
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -18,7 +17,6 @@ import kotlinx.serialization.DeserializationStrategy
|
|||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: (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> {
|
||||||
val correctedUrl = url.asCorrectWebSocketUrl
|
val correctedUrl = url.asCorrectWebSocketUrl
|
||||||
@@ -28,7 +26,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
do {
|
do {
|
||||||
val reconnect = try {
|
val reconnect = try {
|
||||||
safely {
|
safely {
|
||||||
ws(correctedUrl, requestBuilder) {
|
ws(correctedUrl) {
|
||||||
for (received in incoming) {
|
for (received in incoming) {
|
||||||
when (received) {
|
when (received) {
|
||||||
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
||||||
@@ -67,12 +65,10 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>,
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@@ -85,16 +85,9 @@ class UnifiedRequester(
|
|||||||
|
|
||||||
fun <T> createStandardWebsocketFlow(
|
fun <T> createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
checkReconnection: (Throwable?) -> Boolean,
|
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||||
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder)
|
|
||||||
|
|
||||||
fun <T> createStandardWebsocketFlow(
|
|
||||||
url: String,
|
|
||||||
deserializer: DeserializationStrategy<T>,
|
|
||||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultRequester = UnifiedRequester()
|
val defaultRequester = UnifiedRequester()
|
||||||
|
@@ -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,39 +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.features.onUpload
|
|
||||||
import io.ktor.client.request.forms.formData
|
|
||||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
|
||||||
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<String>(
|
|
||||||
fullTempUploadDraftPath,
|
|
||||||
formData = formData {
|
|
||||||
append(
|
|
||||||
"data",
|
|
||||||
inputProvider,
|
|
||||||
Headers.build {
|
|
||||||
append(HttpHeaders.ContentType, file.mimeType)
|
|
||||||
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
onUpload(onUpload)
|
|
||||||
}
|
|
||||||
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,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,10 +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.websockets
|
api "io.ktor:ktor-websockets:$ktor_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,29 +4,25 @@ import dev.inmo.micro_utils.coroutines.safely
|
|||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.application.featureOrNull
|
import io.ktor.application.featureOrNull
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.http.URLProtocol
|
|
||||||
import io.ktor.http.cio.websocket.*
|
import io.ktor.http.cio.websocket.*
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.routing.application
|
import io.ktor.routing.application
|
||||||
import io.ktor.websocket.*
|
import io.ktor.websocket.webSocket
|
||||||
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 = URLProtocol.WS,
|
converter: (T) -> StandardKtorSerialInputData
|
||||||
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
|
|
||||||
) {
|
) {
|
||||||
application.apply {
|
application.apply {
|
||||||
featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.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 = URLProtocol.WS,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
@@ -5,7 +5,8 @@ import dev.inmo.micro_utils.coroutines.safely
|
|||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.application.ApplicationCall
|
import io.ktor.application.ApplicationCall
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.http.*
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.content.PartData
|
import io.ktor.http.content.PartData
|
||||||
import io.ktor.http.content.forEachPart
|
import io.ktor.http.content.forEachPart
|
||||||
import io.ktor.request.receive
|
import io.ktor.request.receive
|
||||||
@@ -17,7 +18,6 @@ import io.ktor.util.asStream
|
|||||||
import io.ktor.util.cio.writeChannel
|
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 io.ktor.websocket.WebSocketServerSession
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -30,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 = URLProtocol.WS,
|
) = 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>,
|
||||||
@@ -92,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()
|
||||||
|
@@ -3,7 +3,6 @@ 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.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.application.call
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.http.content.PartData
|
|
||||||
import io.ktor.http.content.streamProvider
|
|
||||||
import io.ktor.request.receiveMultipart
|
|
||||||
import io.ktor.response.respond
|
|
||||||
import io.ktor.routing.Route
|
|
||||||
import io.ktor.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)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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"
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
||||||
*/
|
*/
|
||||||
|
@@ -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)
|
|
@@ -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()
|
||||||
|
@@ -10,8 +10,8 @@ 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