mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
a1bf43def9 | |||
15e9254e00 | |||
afe5a72c6f | |||
750a8b9ecf | |||
27fc3f93e0 | |||
8166d4b99b | |||
b61d2ae2eb | |||
4790fe0aea | |||
bc37b11cee | |||
223fed910f | |||
b85ab7b061 | |||
888dc299c9 | |||
e113dc28ed | |||
31e55d2307 | |||
e90645f248 | |||
4bb7ba2571 | |||
8d31c25bf8 | |||
c7ee1c28b2 | |||
99b09c8b28 | |||
a328c4425a | |||
c0f61ca896 | |||
86e70c0961 | |||
d87a3a039f | |||
6279a2c40a | |||
f377ebea88 | |||
7373fef964 | |||
41ef86dbda | |||
7bc2b2336d | |||
0a615e6d78 | |||
8f928e16e1 | |||
72bc8da1e7 | |||
51e349a5db | |||
3cbb19ba2c | |||
ae3a5bf45d | |||
9c667f4b78 | |||
21195e1bcb | |||
03117ac565 | |||
d13fbdf176 | |||
7cecc0e0b6 | |||
203e781f5d | |||
3eb6cd77cd | |||
51855b2405 | |||
00cc26d874 | |||
02520636ad | |||
76813fae8e | |||
4693483c2b | |||
6dbd12df59 | |||
9e84dc5031 | |||
8f790360bc | |||
808375cea6 |
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 -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
|
||||||
env:
|
env:
|
||||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,5 +1,88 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## 0.9.11
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
18
common/compose/build.gradle
Normal file
18
common/compose/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
alias(libs.plugins.jb.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.DisposableEffectResult
|
||||||
|
|
||||||
|
class DefaultDisposableEffectResult(
|
||||||
|
private val onDispose: () -> Unit
|
||||||
|
) : DisposableEffectResult {
|
||||||
|
override fun dispose() {
|
||||||
|
onDispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DoNothing = DefaultDisposableEffectResult {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composition
|
||||||
|
import dev.inmo.micro_utils.common.onRemoved
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
fun Composition.linkWithElement(element: Element) {
|
||||||
|
element.onRemoved { dispose() }
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.attributes.ATarget
|
||||||
|
|
||||||
|
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
||||||
|
link,
|
||||||
|
mode.targetStr,
|
||||||
|
features
|
||||||
|
)
|
||||||
|
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.dom.DOMScope
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
fun <TElement : Element> renderComposableAndLinkToRoot(
|
||||||
|
root: TElement,
|
||||||
|
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||||
|
content: @Composable DOMScope<TElement>.() -> Unit
|
||||||
|
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||||
|
linkWithElement(root)
|
||||||
|
}
|
1
common/compose/src/main/AndroidManifest.xml
Normal file
1
common/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
@@ -51,7 +51,6 @@ 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
|
||||||
@@ -83,7 +82,6 @@ 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) {
|
||||||
|
@@ -0,0 +1,61 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import org.w3c.dom.*
|
||||||
|
|
||||||
|
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
||||||
|
lateinit var observer: MutationObserver
|
||||||
|
|
||||||
|
observer = MutationObserver { _, _ ->
|
||||||
|
fun checkIfRemoved(node: Node): Boolean {
|
||||||
|
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkIfRemoved(this)) {
|
||||||
|
observer.disconnect()
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
|
||||||
|
return observer
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
|
||||||
|
var previousIntersectionRatio = -1f
|
||||||
|
val observer = IntersectionObserver { entries, observer ->
|
||||||
|
entries.forEach {
|
||||||
|
if (previousIntersectionRatio != it.intersectionRatio) {
|
||||||
|
previousIntersectionRatio = it.intersectionRatio.toFloat()
|
||||||
|
it.block(previousIntersectionRatio, observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.observe(this)
|
||||||
|
return observer
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
|
||||||
|
var previous = -1f
|
||||||
|
onVisibilityChanged { intersectionRatio, observer ->
|
||||||
|
if (previous != intersectionRatio) {
|
||||||
|
if (intersectionRatio > 0 && previous == 0f) {
|
||||||
|
block(observer)
|
||||||
|
}
|
||||||
|
previous = intersectionRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
|
||||||
|
var previous = -1f
|
||||||
|
return onVisibilityChanged { intersectionRatio, observer ->
|
||||||
|
if (previous != intersectionRatio) {
|
||||||
|
if (intersectionRatio == 0f && previous != 0f) {
|
||||||
|
block(observer)
|
||||||
|
}
|
||||||
|
previous = intersectionRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import org.w3c.dom.*
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
import org.w3c.dom.events.EventListener
|
||||||
|
|
||||||
|
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
|
||||||
|
lateinit var observer: MutationObserver
|
||||||
|
val listener = EventListener {
|
||||||
|
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
|
||||||
|
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
|
||||||
|
val childrenGettingElement = elementsToCheck.removeFirst()
|
||||||
|
for (i in 0 until childrenGettingElement.childElementCount) {
|
||||||
|
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (elementsToCheck.isEmpty()) {
|
||||||
|
callback(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options == null) {
|
||||||
|
document.addEventListener(type, listener)
|
||||||
|
} else {
|
||||||
|
document.addEventListener(type, listener, options)
|
||||||
|
}
|
||||||
|
observer = onRemoved {
|
||||||
|
if (options == null) {
|
||||||
|
document.removeEventListener(type, listener)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener(type, listener, options)
|
||||||
|
}
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
|
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
|
||||||
|
fun openLink(link: String, target: String = "_blank", features: String = "") {
|
||||||
|
window.open(link, target, features) ?.focus()
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.dom.createElement
|
||||||
|
import org.w3c.dom.HTMLElement
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.files.get
|
||||||
|
|
||||||
|
fun selectFile(
|
||||||
|
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||||
|
onFailure: (Throwable) -> Unit = {},
|
||||||
|
onFile: (MPPFile) -> Unit
|
||||||
|
) {
|
||||||
|
(document.createElement("input") {
|
||||||
|
(this as HTMLInputElement).apply {
|
||||||
|
type = "file"
|
||||||
|
onchange = {
|
||||||
|
runCatching {
|
||||||
|
files ?.get(0) ?: error("File must not be null")
|
||||||
|
}.onSuccess {
|
||||||
|
onFile(it)
|
||||||
|
}.onFailure {
|
||||||
|
onFailure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputSetup(this)
|
||||||
|
}
|
||||||
|
} as HTMLElement).click()
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import org.w3c.dom.HTMLAnchorElement
|
||||||
|
|
||||||
|
fun triggerDownloadFile(filename: String, fileLink: String) {
|
||||||
|
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||||
|
|
||||||
|
hiddenElement.href = fileLink
|
||||||
|
hiddenElement.target = "_blank"
|
||||||
|
hiddenElement.download = filename
|
||||||
|
hiddenElement.click()
|
||||||
|
}
|
||||||
|
|
@@ -13,6 +13,11 @@ kotlin {
|
|||||||
api libs.kt.coroutines
|
api libs.kt.coroutines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines.android
|
api libs.kt.coroutines.android
|
||||||
|
@@ -12,6 +12,8 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api libs.kt.coroutines
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
api project(":micro_utils.common.compose")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
fun <T> Flow<T>.toMutableState(
|
||||||
|
initial: T,
|
||||||
|
scope: CoroutineScope
|
||||||
|
): MutableState<T> {
|
||||||
|
val state = mutableStateOf(initial)
|
||||||
|
subscribeSafelyWithoutExceptions(scope) { state.value = it }
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> StateFlow<T>.toMutableState(
|
||||||
|
scope: CoroutineScope
|
||||||
|
): MutableState<T> = toMutableState(value, scope)
|
||||||
|
|
@@ -1,6 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.coroutines.compose
|
package dev.inmo.micro_utils.coroutines.compose
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import dev.inmo.micro_utils.common.compose.linkWithElement
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.jetbrains.compose.web.dom.DOMScope
|
import org.jetbrains.compose.web.dom.DOMScope
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
@@ -14,3 +15,12 @@ suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
|||||||
currentCoroutineContext()
|
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)
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.onRemoved
|
||||||
|
import dev.inmo.micro_utils.common.onVisibilityChanged
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
|
||||||
|
var previousData: Boolean? = null
|
||||||
|
|
||||||
|
val observer = onVisibilityChanged { intersectionRatio, _ ->
|
||||||
|
val currentData = intersectionRatio > 0
|
||||||
|
if (currentData != previousData) {
|
||||||
|
trySend(currentData)
|
||||||
|
}
|
||||||
|
previousData = currentData
|
||||||
|
}
|
||||||
|
|
||||||
|
val removeObserver = onRemoved {
|
||||||
|
observer.disconnect()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeOnClose {
|
||||||
|
observer.disconnect()
|
||||||
|
removeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.selectFile
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
|
||||||
|
suspend fun selectFileOrThrow(
|
||||||
|
inputSetup: (HTMLInputElement) -> Unit = {}
|
||||||
|
): MPPFile {
|
||||||
|
val result = CompletableDeferred<MPPFile>()
|
||||||
|
|
||||||
|
selectFile(
|
||||||
|
inputSetup,
|
||||||
|
{
|
||||||
|
result.completeExceptionally(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
result.complete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectFileOrNull(
|
||||||
|
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||||
|
onFailure: (Throwable) -> Unit = {}
|
||||||
|
): MPPFile? {
|
||||||
|
val result = CompletableDeferred<MPPFile?>()
|
||||||
|
|
||||||
|
selectFile(
|
||||||
|
inputSetup,
|
||||||
|
{
|
||||||
|
result.complete(null)
|
||||||
|
onFailure(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
result.complete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.await()
|
||||||
|
}
|
@@ -105,6 +105,16 @@ 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,6 +26,12 @@ 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)) {
|
||||||
@@ -52,7 +58,13 @@ open class DefaultUpdatableStatesMachine<T : State>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
|
/**
|
||||||
|
* Compare if [previous] potentially lead to the same behaviour with [new]
|
||||||
|
*/
|
||||||
|
protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
|
||||||
|
|
||||||
|
@Deprecated("Overwrite shouldReplaceJob instead")
|
||||||
|
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new)
|
||||||
|
|
||||||
override suspend fun updateChain(currentState: T, newState: T) {
|
override suspend fun updateChain(currentState: T, newState: T) {
|
||||||
statesManager.update(currentState, newState)
|
statesManager.update(currentState, newState)
|
||||||
|
@@ -37,36 +37,49 @@ interface DefaultStatesManagerRepo<T : State> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
|
* @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 onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
|
||||||
|
* this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
|
||||||
|
* with [endChain] with current state
|
||||||
|
* @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
* 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
|
||||||
*/
|
*/
|
||||||
class DefaultStatesManager<T : State>(
|
open class DefaultStatesManager<T : State>(
|
||||||
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||||
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
||||||
|
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||||
) : StatesManager<T> {
|
) : StatesManager<T> {
|
||||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||||
private val _onStartChain = MutableSharedFlow<T>(0)
|
protected val _onStartChain = MutableSharedFlow<T>(0)
|
||||||
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
||||||
private val _onEndChain = MutableSharedFlow<T>(0)
|
protected val _onEndChain = MutableSharedFlow<T>(0)
|
||||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
private val mapMutex = Mutex()
|
protected val mapMutex = Mutex()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
repo: DefaultStatesManagerRepo<T>,
|
||||||
|
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
|
||||||
|
) : this (
|
||||||
|
repo,
|
||||||
|
onUpdateContextsConflictResolver = onContextsConflictResolver
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun update(old: T, new: T) = mapMutex.withLock {
|
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 || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||||
repo.removeState(old)
|
repo.removeState(old)
|
||||||
repo.set(new)
|
repo.set(new)
|
||||||
@@ -77,13 +90,17 @@ class DefaultStatesManager<T : State>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
override suspend fun startChain(state: T) = mapMutex.withLock {
|
||||||
if (!repo.contains(state.context)) {
|
val stateOnContext = repo.getContextState(state.context)
|
||||||
|
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
||||||
|
stateOnContext ?.let {
|
||||||
|
endChainWithoutLock(it)
|
||||||
|
}
|
||||||
repo.set(state)
|
repo.set(state)
|
||||||
_onStartChain.emit(state)
|
_onStartChain.emit(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun endChainWithoutLock(state: T) {
|
protected open 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,5 +12,6 @@ import kotlinx.coroutines.flow.*
|
|||||||
*/
|
*/
|
||||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
||||||
fun <T: State> InMemoryStatesManager(
|
fun <T: State> InMemoryStatesManager(
|
||||||
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
|
||||||
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)
|
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||||
|
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)
|
||||||
|
@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.9.11
|
version=0.9.20
|
||||||
android_code_version=101
|
android_code_version=110
|
||||||
|
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
kt = "1.6.10"
|
kt = "1.6.10"
|
||||||
kt-serialization = "1.3.2"
|
kt-serialization = "1.3.2"
|
||||||
kt-coroutines = "1.6.0"
|
kt-coroutines = "1.6.1"
|
||||||
|
|
||||||
jb-compose = "1.1.0"
|
jb-compose = "1.1.1"
|
||||||
jb-exposed = "0.37.3"
|
jb-exposed = "0.37.3"
|
||||||
jb-dokka = "1.6.10"
|
jb-dokka = "1.6.10"
|
||||||
|
|
||||||
klock = "2.6.2"
|
klock = "2.7.0"
|
||||||
uuid = "0.4.0"
|
uuid = "0.4.0"
|
||||||
|
|
||||||
ktor = "1.6.7"
|
ktor = "1.6.8"
|
||||||
|
|
||||||
gh-release = "2.2.12"
|
gh-release = "2.2.12"
|
||||||
|
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
|
expect suspend fun HttpClient.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||||
|
): TemporalFileId
|
||||||
|
|
||||||
|
suspend fun UnifiedRequester.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||||
|
): TemporalFileId = client.tempUpload(
|
||||||
|
fullTempUploadDraftPath, file, onUpload
|
||||||
|
)
|
@@ -0,0 +1,58 @@
|
|||||||
|
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)
|
@@ -0,0 +1,39 @@
|
|||||||
|
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)
|
||||||
|
}
|
@@ -13,6 +13,7 @@ kotlin {
|
|||||||
api internalProject("micro_utils.common")
|
api internalProject("micro_utils.common")
|
||||||
api libs.kt.serialization.cbor
|
api libs.kt.serialization.cbor
|
||||||
api libs.klock
|
api libs.klock
|
||||||
|
api libs.uuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.common
|
||||||
|
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
const val DefaultTemporalFilesSubPath = "temp_upload"
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@JvmInline
|
||||||
|
value class TemporalFileId(val string: String)
|
@@ -92,6 +92,11 @@ 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()
|
||||||
|
@@ -0,0 +1,132 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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]
|
||||||
*/
|
*/
|
||||||
|
@@ -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] and version of table will be set up to [version]
|
* In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less
|
||||||
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
|
* than parameter [version] or null, it will increase version one-by-one until database version will be equal to
|
||||||
* until database version will be equal to [version]
|
* [version]
|
||||||
*
|
*
|
||||||
* @param version Current version of table
|
* @param version Current version of table
|
||||||
* @param onCreate This callback will be called in case when table have no information about table
|
* @param onCreate This callback will be called in case when repo 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"
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
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,7 +143,12 @@ 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(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
|
selectDistinct(
|
||||||
|
tableName,
|
||||||
|
columns = valueColumnArray,
|
||||||
|
selection = "$idColumnName=?",
|
||||||
|
selectionArgs = arrayOf(k.keyAsString())
|
||||||
|
).use {
|
||||||
it.count
|
it.count
|
||||||
}
|
}
|
||||||
}.toLong()
|
}.toLong()
|
||||||
|
@@ -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("tableName")
|
val tableVersionColumn = integer("tableVersion")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initTable()
|
initTable()
|
||||||
|
@@ -2,6 +2,7 @@ rootProject.name='micro_utils'
|
|||||||
|
|
||||||
String[] includes = [
|
String[] includes = [
|
||||||
":common",
|
":common",
|
||||||
|
":common:compose",
|
||||||
":matrix",
|
":matrix",
|
||||||
":crypto",
|
":crypto",
|
||||||
":selector:common",
|
":selector:common",
|
||||||
|
Reference in New Issue
Block a user