mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-18 14:59:24 +00:00
Compare commits
67 Commits
Author | SHA1 | Date | |
---|---|---|---|
f118ebce6e | |||
59fc90e556 | |||
fb9e4d57fb | |||
960c38b696 | |||
39895e58a6 | |||
b420d85be5 | |||
19ea2f340a | |||
11b0d059bf | |||
c8a25ce544 | |||
509583ea2e | |||
1c86f3f4bf | |||
6d999be590 | |||
e715772dbf | |||
63eb7b7ea8 | |||
b07683b815 | |||
96e97d1691 | |||
261d8827e3 | |||
c3156f2e41 | |||
8c08801460 | |||
aaf1299da7 | |||
a411355b4f | |||
eba41066b4 | |||
f295dff8a2 | |||
a16815143c | |||
6ff3f6ae42 | |||
84071881af | |||
7cccf7e56e | |||
2516d5e381 | |||
cdec8bac75 | |||
fa30aae194 | |||
eb959a3135 | |||
24033e0cac | |||
71f9a505e0 | |||
979b8f017b | |||
af78f01682 | |||
0b16d5c826 | |||
597e14bc7e | |||
04a95867e2 | |||
e0d5eb45b7 | |||
b90cab318e | |||
3252b61abe | |||
2a2da21ff3 | |||
04ef371337 | |||
623e0cd369 | |||
1f466747f0 | |||
2215462f99 | |||
ac4c0a2e4c | |||
f7496db5ac | |||
3028fe975d | |||
23a5034493 | |||
65e339f811 | |||
2020e48659 | |||
9566d6f81f | |||
a00d734712 | |||
27a3e8706a | |||
e601efcfc0 | |||
2bfad9f885 | |||
e78e984943 | |||
242f4b02d0 | |||
041be5a1d1 | |||
976ce056c1 | |||
00c23c73a8 | |||
9dd1848337 | |||
9b30efd9a2 | |||
5853f7cc49 | |||
7b00a06f3e | |||
9ef9be0f37 |
12
.github/workflows/build.yml
vendored
Normal file
12
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Regular build
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew build
|
116
CHANGELOG.md
116
CHANGELOG.md
@@ -1,5 +1,121 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.14
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Kotlin`: `1.5.10` -> `1.5.20`
|
||||||
|
* `Coroutines`
|
||||||
|
* `subscribeSafelyWithoutExceptions` got new parameter `onException` by analogue with `safelyWithoutExceptions`
|
||||||
|
* New extensions `Flow#subscribeAsync` and subsequent analogs of `subscribe` with opportunity to set up custom marker
|
||||||
|
|
||||||
|
## 0.5.13
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Add functionality for multiplatform working with files:
|
||||||
|
* Main class for files `MPPFile`
|
||||||
|
* Inline class for filenames work encapsulation `FileName`
|
||||||
|
* `FSM`
|
||||||
|
* Module inited and in preview state
|
||||||
|
|
||||||
|
## 0.5.12
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `Android`
|
||||||
|
* Extension `View#changeVisibility` has been fixed
|
||||||
|
* `Android`
|
||||||
|
* `RecyclerView`
|
||||||
|
* Default adapter got `dataCountFlow` property
|
||||||
|
* New subtype of adapter based on `StateFlow`: `StateFlowBasedRecyclerViewAdapter`
|
||||||
|
|
||||||
|
## 0.5.11
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* Fixes in `WriteOneToManyRepo#add`
|
||||||
|
* `Exposed`:
|
||||||
|
* Fixes in `ExposedOneToManyKeyValueRepo#add`
|
||||||
|
|
||||||
|
## 0.5.10
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Core KTX`: `1.3.2` -> `1.5.0`
|
||||||
|
* `AndroidX Recycler`: `1.2.0` -> `1.2.1`
|
||||||
|
* `AppCompat`: `1.2.0` -> `1.3.0`
|
||||||
|
* `Android`
|
||||||
|
* `RecyclerView`:
|
||||||
|
* `data` of `RecyclerViewAdapter` became an abstract field
|
||||||
|
* New function `RecyclerViewAdapter`
|
||||||
|
* `Common`:
|
||||||
|
* New extension `View#changeVisibility`
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* `WriteOneToManyRepo` got new function `clearWithValue`
|
||||||
|
* `Android`:
|
||||||
|
* New extension `SQLiteDatabase#selectDistinct`
|
||||||
|
* Fixes in `OneToManyAndroidRepo`
|
||||||
|
* `Ktor`
|
||||||
|
* `Server`
|
||||||
|
* All elements in configurators became a `fun interface`
|
||||||
|
* `Pagination`
|
||||||
|
* New function `doForAllWithCurrentPaging`
|
||||||
|
|
||||||
|
## 0.5.9
|
||||||
|
|
||||||
|
* `Repos`
|
||||||
|
* `Common`
|
||||||
|
* `OneToManyAndroidRepo` got new primary constructor
|
||||||
|
|
||||||
|
## 0.5.8
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* New extension `Iterable#firstNotNull`
|
||||||
|
* `Coroutines`
|
||||||
|
* New extension `Flow#firstNotNull`
|
||||||
|
* New extensions `CoroutineContext#LinkedSupervisorJob`, `CoroutineScope#LinkedSupervisorJob` and
|
||||||
|
`CoroutineScope#LinkedSupervisorScope`
|
||||||
|
|
||||||
|
## 0.5.7
|
||||||
|
|
||||||
|
* `Pagination`
|
||||||
|
* `Ktor`
|
||||||
|
* `Server`
|
||||||
|
* Fixes in extension `extractPagination`
|
||||||
|
* `Repos`
|
||||||
|
* `Cache`
|
||||||
|
* All standard cache repos have been separated to read and read/write repos
|
||||||
|
|
||||||
|
## 0.5.6
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Exposed`: `0.31.1` -> `0.32.1`
|
||||||
|
* `Coroutines`
|
||||||
|
* `JVM`
|
||||||
|
* `launchSynchronously` and subsequent functions got improved mechanism
|
||||||
|
* New method `safelyWithResult`
|
||||||
|
|
||||||
|
## 0.5.5
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Ktor`: `1.5.4` -> `1.6.0`
|
||||||
|
|
||||||
|
## 0.5.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.1.0` -> `2.1.2`
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.5.0` -> `1.5.10`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Extensions `doInUI` and `doInDefault` were replaced in common and available on any supported platform
|
||||||
|
* Extension `doInIO` replaced into `jvm` and available on any `JVM` platform
|
||||||
|
* Old extension `safelyWithouException` without `onException` has been replaced by its copy with `onException` and
|
||||||
|
default value
|
||||||
|
* New value `defaultSafelyWithoutExceptionHandlerWithNull` which is used in all `*WithoutExceptions` by default
|
||||||
|
* Analogs of `launch` and `async` for `safely` and `safelyWithoutExceptions` were added
|
||||||
|
* Analogs of `runCatching` for `safely` and `safelyWithoutExceptions` were added
|
||||||
|
|
||||||
## 0.5.2
|
## 0.5.2
|
||||||
|
|
||||||
* `Ktor`:
|
* `Ktor`:
|
||||||
|
@@ -35,9 +35,9 @@ class ActionViewHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ActionsRecyclerViewAdapter(
|
class ActionsRecyclerViewAdapter(
|
||||||
data: List<AlertAction>,
|
override val data: List<AlertAction>,
|
||||||
private val dialogInterfaceGetter: () -> DialogInterface
|
private val dialogInterfaceGetter: () -> DialogInterface
|
||||||
) : RecyclerViewAdapter<AlertAction>(data) {
|
) : RecyclerViewAdapter<AlertAction>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||||
parent, dialogInterfaceGetter
|
parent, dialogInterfaceGetter
|
||||||
)
|
)
|
||||||
|
@@ -11,6 +11,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
|
@@ -1,12 +1,21 @@
|
|||||||
package dev.inmo.micro_utils.android.recyclerview
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
|
||||||
abstract class RecyclerViewAdapter<T>(
|
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||||
val data: List<T>
|
protected abstract val data: List<T>
|
||||||
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
|
||||||
|
private val _dataCountState by lazy {
|
||||||
|
MutableStateFlow<Int>(data.size)
|
||||||
|
}
|
||||||
|
val dataCountState: StateFlow<Int> by lazy {
|
||||||
|
_dataCountState.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
var emptyView: View? = null
|
var emptyView: View? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
object : RecyclerView.AdapterDataObserver() {
|
object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount)
|
super.onItemRangeChanged(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
super.onChanged()
|
super.onChanged()
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
super.onItemRangeRemoved(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
super.onItemRangeInserted(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
|
|
||||||
private fun checkEmpty() {
|
private fun checkEmpty() {
|
||||||
emptyView ?. let {
|
emptyView ?. let {
|
||||||
if (data.isEmpty()) {
|
if (dataCountState.value == 0) {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
@@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> RecyclerViewAdapter(
|
||||||
|
data: List<T>,
|
||||||
|
onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T>
|
||||||
|
) = object : RecyclerViewAdapter<T>() {
|
||||||
|
override val data: List<T> = data
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType)
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.Diff
|
||||||
|
import dev.inmo.micro_utils.common.PreviewFeature
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
@PreviewFeature("This feature in preview state and may contains different bugs. " +
|
||||||
|
"Besides, this feature can be changed in future in non-compatible way")
|
||||||
|
abstract class StateFlowBasedRecyclerViewAdapter<T>(
|
||||||
|
listeningScope: CoroutineScope,
|
||||||
|
dataState: StateFlow<List<T>>
|
||||||
|
) : RecyclerViewAdapter<T>() {
|
||||||
|
override var data: List<T> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataState.onEach {
|
||||||
|
try {
|
||||||
|
val diffForRemoves = Diff(data, it)
|
||||||
|
val removedIndexes = diffForRemoves.removed.map { it.index }
|
||||||
|
val leftRemove = removedIndexes.toMutableList()
|
||||||
|
data = data.filterIndexed { i, _ ->
|
||||||
|
if (i in leftRemove) {
|
||||||
|
leftRemove.remove(i)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
removedIndexes.sortedDescending().forEach {
|
||||||
|
notifyItemRemoved(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val diffAddsAndReplaces = Diff(data, it)
|
||||||
|
data = it
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
diffAddsAndReplaces.replaced.forEach { (from, to) ->
|
||||||
|
notifyItemMoved(from.index, to.index)
|
||||||
|
}
|
||||||
|
diffAddsAndReplaces.added.forEach {
|
||||||
|
notifyItemInserted(it.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// currently do nothing
|
||||||
|
}
|
||||||
|
}.launchIn(listeningScope)
|
||||||
|
}
|
||||||
|
}
|
@@ -5,3 +5,18 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -16,7 +16,7 @@ package dev.inmo.micro_utils.common
|
|||||||
AnnotationTarget.TYPEALIAS,
|
AnnotationTarget.TYPEALIAS,
|
||||||
AnnotationTarget.TYPE_PARAMETER
|
AnnotationTarget.TYPE_PARAMETER
|
||||||
)
|
)
|
||||||
annotation class PreviewFeature
|
annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases")
|
||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
"This thing is marked as warned. See message of warn to get more info",
|
"This thing is marked as warned. See message of warn to get more info",
|
||||||
|
@@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder
|
|||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
typealias ByteArrayAllocator = () -> ByteArray
|
typealias ByteArrayAllocator = () -> ByteArray
|
||||||
|
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
|
||||||
|
|
||||||
val ByteArray.asAllocator: ByteArrayAllocator
|
val ByteArray.asAllocator: ByteArrayAllocator
|
||||||
get() = { this }
|
get() = { this }
|
||||||
|
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = { this }
|
||||||
|
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = { this() }
|
||||||
|
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
|
||||||
|
return invoke().asAllocator
|
||||||
|
}
|
||||||
|
|
||||||
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||||
private val realSerializer = ByteArraySerializer()
|
private val realSerializer = ByteArraySerializer()
|
||||||
@@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
|||||||
|
|
||||||
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
||||||
val bytes = realSerializer.deserialize(decoder)
|
val bytes = realSerializer.deserialize(decoder)
|
||||||
return { bytes }
|
return bytes.asAllocator
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!
|
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@JvmInline
|
||||||
|
value class FileName(val string: String) {
|
||||||
|
val name: String
|
||||||
|
get() = string.takeLastWhile { it != '/' }
|
||||||
|
val extension: String
|
||||||
|
get() = name.takeLastWhile { it != '.' }
|
||||||
|
val nameWithoutExtension: String
|
||||||
|
get() {
|
||||||
|
val filename = name
|
||||||
|
return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let {
|
||||||
|
filename.substring(0, it)
|
||||||
|
} ?: filename
|
||||||
|
}
|
||||||
|
override fun toString(): String = string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
expect class MPPFile
|
||||||
|
|
||||||
|
expect val MPPFile.filename: FileName
|
||||||
|
expect val MPPFile.filesize: Long
|
||||||
|
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
suspend fun MPPFile.bytes() = bytesAllocator()
|
||||||
|
|
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.khronos.webgl.ArrayBuffer
|
||||||
|
import org.w3c.dom.ErrorEvent
|
||||||
|
import org.w3c.files.File
|
||||||
|
import org.w3c.files.FileReader
|
||||||
|
import kotlin.js.Promise
|
||||||
|
|
||||||
|
actual typealias MPPFile = File
|
||||||
|
|
||||||
|
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||||
|
val reader = FileReader()
|
||||||
|
reader.onload = {
|
||||||
|
success((reader.result as ArrayBuffer).toByteArray())
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
reader.onerror = {
|
||||||
|
failure(Exception((it as ErrorEvent).message))
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
|
||||||
|
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(name)
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = size.toLong()
|
||||||
|
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = ::dirtyReadBytes
|
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
import kotlin.js.Promise
|
||||||
|
|
||||||
|
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||||
|
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.doInIO
|
||||||
|
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
actual typealias MPPFile = File
|
||||||
|
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(name)
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = length()
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
doInIO {
|
||||||
|
doOutsideOfCoroutine {
|
||||||
|
readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
|||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
if (goneOnHide) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -19,4 +19,4 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,7 @@ inline fun CoroutineScope.createSafeActionsActor(
|
|||||||
suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending(
|
suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending(
|
||||||
action: ActorAction<T>
|
action: ActorAction<T>
|
||||||
) = suspendCoroutine<T> {
|
) = suspendCoroutine<T> {
|
||||||
offer {
|
trySend {
|
||||||
safely({ e -> it.resumeWithException(e) }) {
|
safely({ e -> it.resumeWithException(e) }) {
|
||||||
it.resume(action())
|
it.resume(action())
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
inline val UI
|
||||||
|
get() = Dispatchers.Main
|
||||||
|
inline val Default
|
||||||
|
get() = Dispatchers.Default
|
||||||
|
|
||||||
|
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||||
|
context,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
UI,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
Default,
|
||||||
|
block
|
||||||
|
)
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!
|
@@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
||||||
@@ -29,9 +31,10 @@ inline fun <T> Flow<T>.subscribeSafely(
|
|||||||
*/
|
*/
|
||||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
noinline block: suspend (T) -> Unit
|
noinline block: suspend (T) -> Unit
|
||||||
) = subscribe(scope) {
|
) = subscribe(scope) {
|
||||||
safelyWithoutExceptions {
|
safelyWithoutExceptions(onException) {
|
||||||
block(it)
|
block(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,118 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
private class SubscribeAsyncReceiver<T>(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
|
||||||
|
) {
|
||||||
|
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
|
||||||
|
val channel: SendChannel<T>
|
||||||
|
get() = dataChannel
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launchSafelyWithoutExceptions {
|
||||||
|
for (data in dataChannel) {
|
||||||
|
output(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean = dataChannel.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface AsyncSubscriptionCommand<T, M> {
|
||||||
|
suspend operator fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>)
|
||||||
|
}
|
||||||
|
private data class AsyncSubscriptionCommandData<T, M>(
|
||||||
|
val data: T,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
val markerFactory: suspend (T) -> M,
|
||||||
|
val block: suspend (T) -> Unit,
|
||||||
|
val onEmpty: suspend (M) -> Unit
|
||||||
|
) : AsyncSubscriptionCommand<T, M> {
|
||||||
|
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||||
|
val marker = markerFactory(data)
|
||||||
|
markersMap.getOrPut(marker) {
|
||||||
|
SubscribeAsyncReceiver(scope) {
|
||||||
|
safelyWithoutExceptions { block(it) }
|
||||||
|
if (isEmpty()) {
|
||||||
|
onEmpty(marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.channel.send(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AsyncSubscriptionCommandClearReceiver<T, M>(
|
||||||
|
val marker: M
|
||||||
|
) : AsyncSubscriptionCommand<T, M> {
|
||||||
|
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||||
|
val receiver = markersMap[marker]
|
||||||
|
if (receiver ?.isEmpty() == true) {
|
||||||
|
markersMap.remove(marker)
|
||||||
|
receiver.scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, M> Flow<T>.subscribeAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
markerFactory: suspend (T) -> M,
|
||||||
|
block: suspend (T) -> Unit
|
||||||
|
): Job {
|
||||||
|
val subscope = scope.LinkedSupervisorScope()
|
||||||
|
val markersMap = mutableMapOf<M, SubscribeAsyncReceiver<T>>()
|
||||||
|
val actor = subscope.actor<AsyncSubscriptionCommand<T, M>>(Channel.UNLIMITED) {
|
||||||
|
it.invoke(markersMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
|
||||||
|
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
||||||
|
actor.send(
|
||||||
|
AsyncSubscriptionCommandClearReceiver(marker)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actor.send(dataCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.invokeOnCompletion { subscope.cancel() }
|
||||||
|
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline markerFactory: suspend (T) -> M,
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
|
safely(onException) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline markerFactory: suspend (T) -> M,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
|
safelyWithoutExceptions(onException) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline markerFactory: suspend (T) -> M,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
|
safelyWithoutExceptions({ /* do nothing */}) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
@@ -86,6 +86,9 @@ suspend fun <T> safelyWithContextExceptionHandler(
|
|||||||
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||||
* * [defaultSafelyExceptionHandler]
|
* * [defaultSafelyExceptionHandler]
|
||||||
*
|
*
|
||||||
|
* Remember, that [ExceptionHandler] from [CoroutineContext.get] will be used anyway if it is available. After it will
|
||||||
|
* be called [onException]
|
||||||
|
*
|
||||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||||
* exception will be available for catching
|
* exception will be available for catching
|
||||||
*
|
*
|
||||||
@@ -105,24 +108,42 @@ suspend inline fun <T> safely(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend inline fun <T> runCatchingSafely(
|
||||||
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
): Result<T> = runCatching {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <T> safelyWithResult(
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and
|
||||||
|
* returning null at one time
|
||||||
|
*
|
||||||
|
* @see safelyWithoutExceptions
|
||||||
|
* @see launchSafelyWithoutExceptions
|
||||||
|
* @see asyncSafelyWithoutExceptions
|
||||||
|
*/
|
||||||
|
val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
|
||||||
|
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||||
* result from exception (instead of throwing it)
|
* result from exception (instead of throwing it, by default always returns null)
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> safelyWithoutExceptions(
|
suspend inline fun <T> safelyWithoutExceptions(
|
||||||
noinline onException: ExceptionHandler<T?>,
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T? = safely(onException, block)
|
): T? = safely(onException, block)
|
||||||
|
|
||||||
/**
|
suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||||
* Shortcut for [safely] without exception handler (instead of this you will always receive null as a result)
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
*/
|
|
||||||
suspend inline fun <T> safelyWithoutExceptions(
|
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T? = safelyWithoutExceptions(
|
): Result<T?> = runCatching {
|
||||||
{
|
safelyWithoutExceptions(onException, block)
|
||||||
defaultSafelyWithoutExceptionHandler.invoke(it)
|
}
|
||||||
null
|
|
||||||
},
|
|
||||||
block
|
|
||||||
)
|
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
inline fun CoroutineScope.launchSafely(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.asyncSafely(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
fun CoroutineContext.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
|
||||||
|
fun CoroutineScope.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = coroutineContext.LinkedSupervisorJob(additionalContext)
|
||||||
|
|
||||||
|
fun CoroutineScope.LinkedSupervisorScope(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = CoroutineScope(
|
||||||
|
coroutineContext + LinkedSupervisorJob(additionalContext)
|
||||||
|
)
|
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
val IO
|
||||||
|
get() = Dispatchers.IO
|
||||||
|
|
||||||
|
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
IO,
|
||||||
|
block
|
||||||
|
)
|
@@ -3,27 +3,21 @@ package dev.inmo.micro_utils.coroutines
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||||
val deferred = CompletableDeferred<T>()
|
var result: Result<T>? = null
|
||||||
val objectToSynchronize = java.lang.Object()
|
val objectToSynchronize = Object()
|
||||||
val launchCallback = {
|
synchronized(objectToSynchronize) {
|
||||||
launch {
|
launch {
|
||||||
safely(
|
result = safelyWithResult(block)
|
||||||
{
|
}.invokeOnCompletion {
|
||||||
deferred.completeExceptionally(it)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
deferred.complete(block())
|
|
||||||
}
|
|
||||||
synchronized(objectToSynchronize) {
|
synchronized(objectToSynchronize) {
|
||||||
objectToSynchronize.notifyAll()
|
objectToSynchronize.notifyAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
while (result == null) {
|
||||||
|
objectToSynchronize.wait()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
synchronized(objectToSynchronize) {
|
return result!!.getOrThrow()
|
||||||
launchCallback()
|
|
||||||
objectToSynchronize.wait()
|
|
||||||
}
|
|
||||||
return deferred.getCompleted()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.asDeferred
|
|
||||||
import dev.inmo.micro_utils.coroutines.launchSynchronously
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlin.test.*
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class DoWithFirstTests {
|
class DoWithFirstTests {
|
||||||
@Test
|
@Test
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
|
||||||
Dispatchers.Main,
|
|
||||||
block
|
|
||||||
)
|
|
||||||
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
|
||||||
Dispatchers.Default,
|
|
||||||
block
|
|
||||||
)
|
|
||||||
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
|
||||||
Dispatchers.IO,
|
|
||||||
block
|
|
||||||
)
|
|
17
fsm/common/build.gradle
Normal file
17
fsm/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
val context: Any
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class StateHandlerHolder<I : State>(
|
||||||
|
private val inputKlass: KClass<I>,
|
||||||
|
private val strict: Boolean = false,
|
||||||
|
private val delegateTo: StatesHandler<I>
|
||||||
|
) : StatesHandler<State> {
|
||||||
|
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
|
||||||
|
|
||||||
|
override suspend fun StatesMachine.handleState(state: State): State? {
|
||||||
|
return delegateTo.run { handleState(state as I) }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
fun interface StatesHandler<I : State> {
|
||||||
|
suspend fun StatesMachine.handleState(state: I): State?
|
||||||
|
}
|
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
|
||||||
|
private suspend fun <I : State> StatesMachine.launchStateHandling(
|
||||||
|
state: State,
|
||||||
|
handlers: List<StateHandlerHolder<out I>>
|
||||||
|
): State? {
|
||||||
|
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||||
|
handleState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatesMachine (
|
||||||
|
private val statesManager: StatesManager,
|
||||||
|
private val handlers: List<StateHandlerHolder<*>>
|
||||||
|
) : StatesHandler<State> {
|
||||||
|
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
|
||||||
|
|
||||||
|
fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||||
|
val statePerformer: suspend (State) -> Unit = { state: State ->
|
||||||
|
val newState = launchStateHandling(state, handlers)
|
||||||
|
if (newState != null) {
|
||||||
|
statesManager.update(state, newState)
|
||||||
|
} else {
|
||||||
|
statesManager.endChain(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
launch { statePerformer(it) }
|
||||||
|
}
|
||||||
|
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
launch { statePerformer(it.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
statesManager.getActiveStates().forEach {
|
||||||
|
launch { statePerformer(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun startChain(state: State) {
|
||||||
|
statesManager.startChain(state)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
interface StatesManager {
|
||||||
|
val onChainStateUpdated: Flow<Pair<State, State>>
|
||||||
|
val onStartChain: Flow<State>
|
||||||
|
val onEndChain: Flow<State>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must set current set using [State.context]
|
||||||
|
*/
|
||||||
|
suspend fun update(old: State, new: State)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
|
||||||
|
* busy by the other [State]
|
||||||
|
*/
|
||||||
|
suspend fun startChain(state: State)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
|
||||||
|
* ignored
|
||||||
|
*/
|
||||||
|
suspend fun endChain(state: State)
|
||||||
|
|
||||||
|
suspend fun getActiveStates(): List<State>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||||
|
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||||
|
* new state by using [endChain] with that state
|
||||||
|
*/
|
||||||
|
class InMemoryStatesManager(
|
||||||
|
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||||
|
) : StatesManager {
|
||||||
|
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||||
|
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
private val _onStartChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
|
||||||
|
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
|
private val contextsToStates = mutableMapOf<Any, State>()
|
||||||
|
private val mapMutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun update(old: State, new: State) = mapMutex.withLock {
|
||||||
|
when {
|
||||||
|
contextsToStates[old.context] != old -> return@withLock
|
||||||
|
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
|
||||||
|
contextsToStates[old.context] = new
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val stateOnNewOneContext = contextsToStates.getValue(new.context)
|
||||||
|
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
|
endChainWithoutLock(stateOnNewOneContext)
|
||||||
|
contextsToStates.remove(old.context)
|
||||||
|
contextsToStates[new.context] = new
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun startChain(state: State) = mapMutex.withLock {
|
||||||
|
if (!contextsToStates.containsKey(state.context)) {
|
||||||
|
contextsToStates[state.context] = state
|
||||||
|
_onStartChain.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun endChainWithoutLock(state: State) {
|
||||||
|
if (contextsToStates[state.context] == state) {
|
||||||
|
contextsToStates.remove(state.context)
|
||||||
|
_onEndChain.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun endChain(state: State) {
|
||||||
|
mapMutex.withLock {
|
||||||
|
endChainWithoutLock(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common.dsl
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.fsm.common.*
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class FSMBuilder(
|
||||||
|
var statesManager: StatesManager = InMemoryStatesManager()
|
||||||
|
) {
|
||||||
|
private var states = mutableListOf<StateHandlerHolder<*>>()
|
||||||
|
|
||||||
|
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||||
|
states.add(StateHandlerHolder(kClass, false, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||||
|
states.add(StateHandlerHolder(kClass, true, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build() = StatesMachine(
|
||||||
|
statesManager,
|
||||||
|
states.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
|
||||||
|
add(I::class, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
|
||||||
|
addStrict(I::class, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildFSM(
|
||||||
|
block: FSMBuilder.() -> Unit
|
||||||
|
): StatesMachine = FSMBuilder().apply(block).build()
|
53
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
53
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import dev.inmo.micro_utils.fsm.common.*
|
||||||
|
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
|
||||||
|
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
sealed interface TrafficLightState : State {
|
||||||
|
val trafficLightNumber: Int
|
||||||
|
override val context: Int
|
||||||
|
get() = trafficLightNumber
|
||||||
|
}
|
||||||
|
data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||||
|
data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||||
|
data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||||
|
|
||||||
|
class PlayableMain {
|
||||||
|
// @Test
|
||||||
|
fun test() {
|
||||||
|
runBlocking {
|
||||||
|
val countOfTrafficLights = 10
|
||||||
|
val initialStates = (0 until countOfTrafficLights).map {
|
||||||
|
when (0/*Random.nextInt(3)*/) {
|
||||||
|
0 -> GreenCommon(it)
|
||||||
|
1 -> YellowCommon(it)
|
||||||
|
else -> RedCommon(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val statesManager = InMemoryStatesManager()
|
||||||
|
|
||||||
|
val machine = buildFSM {
|
||||||
|
strictlyOn<GreenCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
YellowCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
strictlyOn<YellowCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
RedCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
strictlyOn<RedCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
GreenCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
this.statesManager = statesManager
|
||||||
|
}
|
||||||
|
|
||||||
|
initialStates.forEach { machine.startChain(it) }
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
machine.start(scope).join()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
fsm/common/src/main/AndroidManifest.xml
Normal file
1
fsm/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
18
fsm/repos/common/build.gradle
Normal file
18
fsm/repos/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.fsm.common")
|
||||||
|
api project(":micro_utils.repos.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.repos.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.fsm.common.State
|
||||||
|
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||||
|
import dev.inmo.micro_utils.repos.*
|
||||||
|
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||||
|
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
class KeyValueBasedStatesManager(
|
||||||
|
private val keyValueRepo: KeyValueRepo<Any, State>,
|
||||||
|
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||||
|
) : StatesManager {
|
||||||
|
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||||
|
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
|
override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun update(old: State, new: State) {
|
||||||
|
mutex.withLock {
|
||||||
|
when {
|
||||||
|
keyValueRepo.get(old.context) != old -> return@withLock
|
||||||
|
old.context == new.context || !keyValueRepo.contains(new.context) -> {
|
||||||
|
keyValueRepo.set(old.context, new)
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val stateOnNewOneContext = keyValueRepo.get(new.context)!!
|
||||||
|
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
|
endChainWithoutLock(stateOnNewOneContext)
|
||||||
|
keyValueRepo.unset(old.context)
|
||||||
|
keyValueRepo.set(new.context, new)
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun startChain(state: State) {
|
||||||
|
if (!keyValueRepo.contains(state.context)) {
|
||||||
|
keyValueRepo.set(state.context, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun endChainWithoutLock(state: State) {
|
||||||
|
if (keyValueRepo.get(state.context) == state) {
|
||||||
|
keyValueRepo.unset(state.context)
|
||||||
|
_onEndChain.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun endChain(state: State) {
|
||||||
|
mutex.withLock { endChainWithoutLock(state) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getActiveStates(): List<State> {
|
||||||
|
return keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
|
||||||
|
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
|
||||||
|
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
|
||||||
|
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
|
||||||
|
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
|
||||||
|
noinline outToStateTransformer: suspend TargetStateType.() -> State,
|
||||||
|
) = KeyValueBasedStatesManager(
|
||||||
|
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
|
||||||
|
contextToOutTransformer,
|
||||||
|
stateToOutTransformer,
|
||||||
|
outToContextTransformer,
|
||||||
|
outToStateTransformer
|
||||||
|
)
|
||||||
|
)
|
1
fsm/repos/common/src/main/AndroidManifest.xml
Normal file
1
fsm/repos/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
@@ -7,14 +7,14 @@ android.useAndroidX=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx2g
|
org.gradle.jvmargs=-Xmx2g
|
||||||
|
|
||||||
kotlin_version=1.5.0
|
kotlin_version=1.5.20
|
||||||
kotlin_coroutines_version=1.5.0
|
kotlin_coroutines_version=1.5.0
|
||||||
kotlin_serialisation_core_version=1.2.1
|
kotlin_serialisation_core_version=1.2.1
|
||||||
kotlin_exposed_version=0.31.1
|
kotlin_exposed_version=0.32.1
|
||||||
|
|
||||||
ktor_version=1.5.4
|
ktor_version=1.6.0
|
||||||
|
|
||||||
klockVersion=2.1.0
|
klockVersion=2.1.2
|
||||||
|
|
||||||
github_release_plugin_version=2.2.12
|
github_release_plugin_version=2.2.12
|
||||||
|
|
||||||
@@ -22,9 +22,9 @@ uuidVersion=0.3.0
|
|||||||
|
|
||||||
# ANDROID
|
# ANDROID
|
||||||
|
|
||||||
core_ktx_version=1.3.2
|
core_ktx_version=1.5.0
|
||||||
androidx_recycler_version=1.2.0
|
androidx_recycler_version=1.2.1
|
||||||
appcompat_version=1.2.0
|
appcompat_version=1.3.0
|
||||||
|
|
||||||
android_minSdkVersion=19
|
android_minSdkVersion=19
|
||||||
android_compileSdkVersion=30
|
android_compileSdkVersion=30
|
||||||
@@ -45,5 +45,5 @@ dokka_version=1.4.32
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.5.2
|
version=0.5.14
|
||||||
android_code_version=43
|
android_code_version=55
|
||||||
|
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.serialization.SerializationStrategy
|
import kotlinx.serialization.SerializationStrategy
|
||||||
|
|
||||||
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
||||||
if (incoming.poll() != null) {
|
if (incoming.tryReceive() != null) {
|
||||||
close()
|
close()
|
||||||
throw CorrectCloseException
|
throw CorrectCloseException
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
data class ApplicationCachingHeadersConfigurator(
|
data class ApplicationCachingHeadersConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(CachingHeaders) {
|
install(CachingHeaders) {
|
||||||
|
@@ -10,17 +10,18 @@ import kotlinx.serialization.Serializable
|
|||||||
class ApplicationRoutingConfigurator(
|
class ApplicationRoutingConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun Route.invoke() }
|
fun interface Element { operator fun Route.invoke() }
|
||||||
|
private val rootInstaller = Element {
|
||||||
|
elements.forEach {
|
||||||
|
it.apply { invoke() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
try {
|
featureOrNull(Routing) ?.apply {
|
||||||
feature(Routing)
|
rootInstaller.apply { invoke() }
|
||||||
} catch (e: IllegalStateException) {
|
} ?: install(Routing) {
|
||||||
install(Routing) {
|
rootInstaller.apply { invoke() }
|
||||||
elements.forEach {
|
|
||||||
it.apply { invoke() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
class ApplicationSessionsConfigurator(
|
class ApplicationSessionsConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun Sessions.Configuration.invoke() }
|
fun interface Element { operator fun Sessions.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(Sessions) {
|
install(Sessions) {
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
class StatusPagesConfigurator(
|
class StatusPagesConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun StatusPages.Configuration.invoke() }
|
fun interface Element { operator fun StatusPages.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(StatusPages) {
|
install(StatusPages) {
|
||||||
|
@@ -33,3 +33,8 @@ suspend fun <T> doAllWithCurrentPaging(
|
|||||||
block
|
block
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> doForAllWithCurrentPaging(
|
||||||
|
initialPagination: Pagination = FirstPagePagination(),
|
||||||
|
block: suspend (Pagination) -> PaginationResult<T>
|
||||||
|
) = doAllWithCurrentPaging(initialPagination, block)
|
||||||
|
@@ -5,8 +5,8 @@ import io.ktor.http.Parameters
|
|||||||
|
|
||||||
val Parameters.extractPagination: Pagination
|
val Parameters.extractPagination: Pagination
|
||||||
get() = SimplePagination(
|
get() = SimplePagination(
|
||||||
get("page") ?.toIntOrNull() ?: 0,
|
get(paginationPageKey) ?.toIntOrNull() ?: 0,
|
||||||
get("size") ?.toIntOrNull() ?: defaultPaginationPageSize
|
get(paginationSizeKey) ?.toIntOrNull() ?: defaultPaginationPageSize
|
||||||
)
|
)
|
||||||
|
|
||||||
val ApplicationCall.extractPagination: Pagination
|
val ApplicationCall.extractPagination: Pagination
|
||||||
|
@@ -6,19 +6,29 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
open class CRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
open class ReadCRUDCacheRepo<ObjectType, IdType>(
|
||||||
protected val parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
|
protected val parentRepo: ReadCRUDRepo<ObjectType, IdType>,
|
||||||
protected val kvCache: KVCache<IdType, ObjectType>,
|
protected val kvCache: KVCache<IdType, ObjectType>,
|
||||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
|
||||||
protected val idGetter: (ObjectType) -> IdType
|
protected val idGetter: (ObjectType) -> IdType
|
||||||
) : CRUDRepo<ObjectType, IdType, InputValueType> by parentRepo {
|
) : ReadCRUDRepo<ObjectType, IdType> by parentRepo {
|
||||||
protected val onNewJob = parentRepo.newObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
|
||||||
protected val onUpdatedJob = parentRepo.updatedObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
|
||||||
protected val onRemoveJob = parentRepo.deletedObjectsIdsFlow.onEach { kvCache.unset(it) }.launchIn(scope)
|
|
||||||
|
|
||||||
override suspend fun getById(id: IdType): ObjectType? = kvCache.get(id) ?: (parentRepo.getById(id) ?.also {
|
override suspend fun getById(id: IdType): ObjectType? = kvCache.get(id) ?: (parentRepo.getById(id) ?.also {
|
||||||
kvCache.set(id, it)
|
kvCache.set(id, it)
|
||||||
})
|
})
|
||||||
|
|
||||||
override suspend fun contains(id: IdType): Boolean = kvCache.contains(id) || parentRepo.contains(id)
|
override suspend fun contains(id: IdType): Boolean = kvCache.contains(id) || parentRepo.contains(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class CRUDCacheRepo<ObjectType, IdType, InputValueType>(
|
||||||
|
parentRepo: CRUDRepo<ObjectType, IdType, InputValueType>,
|
||||||
|
kvCache: KVCache<IdType, ObjectType>,
|
||||||
|
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||||
|
idGetter: (ObjectType) -> IdType
|
||||||
|
) : ReadCRUDCacheRepo<ObjectType, IdType>(
|
||||||
|
parentRepo,
|
||||||
|
kvCache,
|
||||||
|
idGetter
|
||||||
|
), CRUDRepo<ObjectType, IdType, InputValueType>, WriteCRUDRepo<ObjectType, IdType, InputValueType> by parentRepo {
|
||||||
|
protected val onNewJob = parentRepo.newObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
||||||
|
protected val onUpdatedJob = parentRepo.updatedObjectsFlow.onEach { kvCache.set(idGetter(it), it) }.launchIn(scope)
|
||||||
|
protected val onRemoveJob = parentRepo.deletedObjectsIdsFlow.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||||
|
}
|
||||||
|
@@ -7,14 +7,19 @@ import kotlinx.coroutines.flow.*
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
open class KeyValueCacheRepo<Key,Value>(
|
open class ReadKeyValueCacheRepo<Key,Value>(
|
||||||
protected val parentRepo: KeyValueRepo<Key, Value>,
|
protected val parentRepo: ReadKeyValueRepo<Key, Value>,
|
||||||
protected val kvCache: KVCache<Key, Value>,
|
protected val kvCache: KVCache<Key, Value>,
|
||||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
) : ReadKeyValueRepo<Key,Value> by parentRepo {
|
||||||
) : KeyValueRepo<Key,Value> by parentRepo {
|
|
||||||
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
|
|
||||||
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
|
|
||||||
|
|
||||||
override suspend fun get(k: Key): Value? = kvCache.get(k) ?: parentRepo.get(k) ?.also { kvCache.set(k, it) }
|
override suspend fun get(k: Key): Value? = kvCache.get(k) ?: parentRepo.get(k) ?.also { kvCache.set(k, it) }
|
||||||
override suspend fun contains(key: Key): Boolean = kvCache.contains(key) || parentRepo.contains(key)
|
override suspend fun contains(key: Key): Boolean = kvCache.contains(key) || parentRepo.contains(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class KeyValueCacheRepo<Key,Value>(
|
||||||
|
parentRepo: KeyValueRepo<Key, Value>,
|
||||||
|
kvCache: KVCache<Key, Value>,
|
||||||
|
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
) : ReadKeyValueCacheRepo<Key,Value>(parentRepo, kvCache), KeyValueRepo<Key,Value>, WriteKeyValueRepo<Key, Value> by parentRepo {
|
||||||
|
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, it.second) }.launchIn(scope)
|
||||||
|
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||||
|
}
|
||||||
|
@@ -11,15 +11,10 @@ import kotlinx.coroutines.flow.*
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
open class KeyValuesCacheRepo<Key,Value>(
|
open class ReadKeyValuesCacheRepo<Key,Value>(
|
||||||
protected val parentRepo: KeyValuesRepo<Key, Value>,
|
protected val parentRepo: ReadKeyValuesRepo<Key, Value>,
|
||||||
protected val kvCache: KVCache<Key, List<Value>>,
|
protected val kvCache: KVCache<Key, List<Value>>
|
||||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
) : ReadKeyValuesRepo<Key,Value> by parentRepo {
|
||||||
) : KeyValuesRepo<Key,Value> by parentRepo {
|
|
||||||
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.plus(it.second) ?: listOf(it.second)) }.launchIn(scope)
|
|
||||||
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.minus(it.second) ?: return@onEach) }.launchIn(scope)
|
|
||||||
protected val onDataClearedJob = parentRepo.onDataCleared.onEach { kvCache.unset(it) }.launchIn(scope)
|
|
||||||
|
|
||||||
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
|
override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult<Value> {
|
||||||
return kvCache.get(k) ?.paginate(
|
return kvCache.get(k) ?.paginate(
|
||||||
pagination.let { if (reversed) it.reverse(count(k)) else it }
|
pagination.let { if (reversed) it.reverse(count(k)) else it }
|
||||||
@@ -35,3 +30,13 @@ open class KeyValuesCacheRepo<Key,Value>(
|
|||||||
override suspend fun contains(k: Key, v: Value): Boolean = kvCache.get(k) ?.contains(v) ?: parentRepo.contains(k, v)
|
override suspend fun contains(k: Key, v: Value): Boolean = kvCache.get(k) ?.contains(v) ?: parentRepo.contains(k, v)
|
||||||
override suspend fun contains(k: Key): Boolean = kvCache.contains(k) || parentRepo.contains(k)
|
override suspend fun contains(k: Key): Boolean = kvCache.contains(k) || parentRepo.contains(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class KeyValuesCacheRepo<Key,Value>(
|
||||||
|
parentRepo: KeyValuesRepo<Key, Value>,
|
||||||
|
kvCache: KVCache<Key, List<Value>>,
|
||||||
|
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
) : ReadKeyValuesCacheRepo<Key,Value>(parentRepo, kvCache), KeyValuesRepo<Key,Value>, WriteKeyValuesRepo<Key,Value> by parentRepo {
|
||||||
|
protected val onNewJob = parentRepo.onNewValue.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.plus(it.second) ?: listOf(it.second)) }.launchIn(scope)
|
||||||
|
protected val onRemoveJob = parentRepo.onValueRemoved.onEach { kvCache.set(it.first, kvCache.get(it.first) ?.minus(it.second) ?: return@onEach) }.launchIn(scope)
|
||||||
|
protected val onDataClearedJob = parentRepo.onDataCleared.onEach { kvCache.unset(it) }.launchIn(scope)
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.*
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import dev.inmo.micro_utils.pagination.utils.doForAllWithCurrentPaging
|
||||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
|
|||||||
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
||||||
|
|
||||||
suspend fun clear(k: Key)
|
suspend fun clear(k: Key)
|
||||||
|
suspend fun clearWithValue(v: Value)
|
||||||
|
|
||||||
suspend fun set(toSet: Map<Key, List<Value>>) {
|
suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||||
toSet.keys.forEach { key -> clear(key) }
|
toSet.keys.forEach { key -> clear(key) }
|
||||||
@@ -87,7 +89,19 @@ suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
|
|||||||
k: Key, vararg v: Value
|
k: Key, vararg v: Value
|
||||||
) = set(k, v.toList())
|
) = set(k, v.toList())
|
||||||
|
|
||||||
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value>
|
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value> {
|
||||||
|
override suspend fun clearWithValue(v: Value) {
|
||||||
|
doWithPagination {
|
||||||
|
val keysResult = keys(v, it)
|
||||||
|
|
||||||
|
if (keysResult.results.isNotEmpty()) {
|
||||||
|
remove(keysResult.results.map { it to listOf(v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
keysResult.currentPageIfNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
typealias KeyValuesRepo<Key,Value> = OneToManyKeyValueRepo<Key, Value>
|
typealias KeyValuesRepo<Key,Value> = OneToManyKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||||
|
@@ -114,6 +114,7 @@ open class MapperWriteOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
||||||
|
override suspend fun clearWithValue(v: FromValue) = to.clearWithValue(v.toOutValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@@ -74,6 +74,19 @@ fun SQLiteDatabase.select(
|
|||||||
table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun SQLiteDatabase.selectDistinct(
|
||||||
|
table: String,
|
||||||
|
columns: Array<String>? = null,
|
||||||
|
selection: String? = null,
|
||||||
|
selectionArgs: Array<String>? = null,
|
||||||
|
groupBy: String? = null,
|
||||||
|
having: String? = null,
|
||||||
|
orderBy: String? = null,
|
||||||
|
limit: String? = null
|
||||||
|
) = query(
|
||||||
|
true, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
||||||
|
)
|
||||||
|
|
||||||
fun makePlaceholders(count: Int): String {
|
fun makePlaceholders(count: Int): String {
|
||||||
return (0 until count).joinToString { "?" }
|
return (0 until count).joinToString { "?" }
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,7 @@ package dev.inmo.micro_utils.repos.onetomany
|
|||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import dev.inmo.micro_utils.common.mapNotNullA
|
import dev.inmo.micro_utils.common.mapNotNullA
|
||||||
import dev.inmo.micro_utils.pagination.FirstPagePagination
|
import dev.inmo.micro_utils.pagination.*
|
||||||
import dev.inmo.micro_utils.pagination.Pagination
|
|
||||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
|
||||||
import dev.inmo.micro_utils.pagination.createPaginationResult
|
|
||||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||||
import dev.inmo.micro_utils.repos.*
|
import dev.inmo.micro_utils.repos.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -20,10 +17,14 @@ private val internalSerialFormat = Json {
|
|||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typealias KeyValuesAndroidRepo<Key, Value> = OneToManyAndroidRepo<Key, Value>
|
||||||
|
|
||||||
class OneToManyAndroidRepo<Key, Value>(
|
class OneToManyAndroidRepo<Key, Value>(
|
||||||
private val tableName: String,
|
private val tableName: String,
|
||||||
private val keySerializer: KSerializer<Key>,
|
private val keyAsString: Key.() -> String,
|
||||||
private val valueSerializer: KSerializer<Value>,
|
private val valueAsString: Value.() -> String,
|
||||||
|
private val keyFromString: String.() -> Key,
|
||||||
|
private val valueFromString: String.() -> Value,
|
||||||
private val helper: SQLiteOpenHelper
|
private val helper: SQLiteOpenHelper
|
||||||
) : OneToManyKeyValueRepo<Key, Value> {
|
) : OneToManyKeyValueRepo<Key, Value> {
|
||||||
private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
|
private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
|
||||||
@@ -34,12 +35,9 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
|
override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
|
||||||
|
|
||||||
private val idColumnName = "id"
|
private val idColumnName = "id"
|
||||||
|
private val idColumnArray = arrayOf(idColumnName)
|
||||||
private val valueColumnName = "value"
|
private val valueColumnName = "value"
|
||||||
|
private val valueColumnArray = arrayOf(valueColumnName)
|
||||||
private fun Key.asId() = internalSerialFormat.encodeToString(keySerializer, this)
|
|
||||||
private fun Value.asValue() = internalSerialFormat.encodeToString(valueSerializer, this)
|
|
||||||
private fun String.asValue(): Value = internalSerialFormat.decodeFromString(valueSerializer, this)
|
|
||||||
private fun String.asKey(): Key = internalSerialFormat.decodeFromString(keySerializer, this)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
@@ -57,12 +55,23 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
for ((k, values) in toAdd) {
|
for ((k, values) in toAdd) {
|
||||||
values.forEach { v ->
|
values.forEach { v ->
|
||||||
|
val kAsString = k.keyAsString()
|
||||||
|
val vAsString = v.valueAsString()
|
||||||
|
val isThere = select(tableName,
|
||||||
|
null,
|
||||||
|
"$idColumnName=? AND $valueColumnName=?",
|
||||||
|
arrayOf(kAsString, vAsString),
|
||||||
|
limit = limitClause(1)
|
||||||
|
).use { it.moveToFirst() }
|
||||||
|
if (isThere) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
insert(
|
insert(
|
||||||
tableName,
|
tableName,
|
||||||
null,
|
null,
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
idColumnName to k.asId(),
|
idColumnName to k.keyAsString(),
|
||||||
valueColumnName to v.asValue()
|
valueColumnName to v.valueAsString()
|
||||||
)
|
)
|
||||||
).also {
|
).also {
|
||||||
if (it != -1L) {
|
if (it != -1L) {
|
||||||
@@ -77,7 +86,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
|
|
||||||
override suspend fun clear(k: Key) {
|
override suspend fun clear(k: Key) {
|
||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
delete(tableName, "$idColumnName=?", arrayOf(k.asId()))
|
delete(tableName, "$idColumnName=?", arrayOf(k.keyAsString()))
|
||||||
}.also {
|
}.also {
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
_onDataCleared.emit(k)
|
_onDataCleared.emit(k)
|
||||||
@@ -88,7 +97,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
override suspend fun set(toSet: Map<Key, List<Value>>) {
|
override suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||||
val (clearedKeys, inserted) = helper.blockingWritableTransaction {
|
val (clearedKeys, inserted) = helper.blockingWritableTransaction {
|
||||||
toSet.mapNotNull { (k, _) ->
|
toSet.mapNotNull { (k, _) ->
|
||||||
if (delete(tableName, "$idColumnName=?", arrayOf(k.asId())) > 0) {
|
if (delete(tableName, "$idColumnName=?", arrayOf(k.keyAsString())) > 0) {
|
||||||
k
|
k
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -98,7 +107,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
insert(
|
insert(
|
||||||
tableName,
|
tableName,
|
||||||
null,
|
null,
|
||||||
contentValuesOf(idColumnName to k.asId(), valueColumnName to v.asValue())
|
contentValuesOf(idColumnName to k.keyAsString(), valueColumnName to v.valueAsString())
|
||||||
)
|
)
|
||||||
k to v
|
k to v
|
||||||
}
|
}
|
||||||
@@ -109,7 +118,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
|
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
|
||||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = firstPageWithOneElementPagination.limitClause()).use {
|
||||||
it.count > 0
|
it.count > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,14 +127,14 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
select(
|
select(
|
||||||
tableName,
|
tableName,
|
||||||
selection = "$idColumnName=? AND $valueColumnName=?",
|
selection = "$idColumnName=? AND $valueColumnName=?",
|
||||||
selectionArgs = arrayOf(k.asId(), v.asValue()),
|
selectionArgs = arrayOf(k.keyAsString(), v.valueAsString()),
|
||||||
limit = FirstPagePagination(1).limitClause()
|
limit = FirstPagePagination(1).limitClause()
|
||||||
).use {
|
).use {
|
||||||
it.count > 0
|
it.count > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun count(): Long =helper.blockingReadableTransaction {
|
override suspend fun count(): Long = helper.blockingReadableTransaction {
|
||||||
select(
|
select(
|
||||||
tableName
|
tableName
|
||||||
).use {
|
).use {
|
||||||
@@ -134,7 +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 {
|
||||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
|
||||||
it.count
|
it.count
|
||||||
}
|
}
|
||||||
}.toLong()
|
}.toLong()
|
||||||
@@ -144,18 +153,25 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
reversed: Boolean
|
reversed: Boolean
|
||||||
): PaginationResult<Value> = count(k).let { count ->
|
): PaginationResult<Value> = count(k).let { count ->
|
||||||
|
if (pagination.firstIndex >= count) {
|
||||||
|
return@let emptyList<Value>().createPaginationResult(
|
||||||
|
pagination,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||||
helper.blockingReadableTransaction {
|
helper.blockingReadableTransaction {
|
||||||
select(
|
select(
|
||||||
tableName,
|
tableName,
|
||||||
|
valueColumnArray,
|
||||||
selection = "$idColumnName=?",
|
selection = "$idColumnName=?",
|
||||||
selectionArgs = arrayOf(k.asId()),
|
selectionArgs = arrayOf(k.keyAsString()),
|
||||||
limit = resultPagination.limitClause()
|
limit = resultPagination.limitClause()
|
||||||
).use { c ->
|
).use { c ->
|
||||||
mutableListOf<Value>().also {
|
mutableListOf<Value>().also {
|
||||||
if (c.moveToFirst()) {
|
if (c.moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
it.add(c.getString(valueColumnName).asValue())
|
it.add(c.getString(valueColumnName).valueFromString())
|
||||||
} while (c.moveToNext())
|
} while (c.moveToNext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,16 +186,23 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
reversed: Boolean
|
reversed: Boolean
|
||||||
): PaginationResult<Key> = count().let { count ->
|
): PaginationResult<Key> = count().let { count ->
|
||||||
|
if (pagination.firstIndex >= count) {
|
||||||
|
return@let emptyList<Key>().createPaginationResult(
|
||||||
|
pagination,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||||
helper.blockingReadableTransaction {
|
helper.blockingReadableTransaction {
|
||||||
select(
|
selectDistinct(
|
||||||
tableName,
|
tableName,
|
||||||
|
idColumnArray,
|
||||||
limit = resultPagination.limitClause()
|
limit = resultPagination.limitClause()
|
||||||
).use { c ->
|
).use { c ->
|
||||||
mutableListOf<Key>().also {
|
mutableListOf<Key>().also {
|
||||||
if (c.moveToFirst()) {
|
if (c.moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
it.add(c.getString(idColumnName).asKey())
|
it.add(c.getString(idColumnName).keyFromString())
|
||||||
} while (c.moveToNext())
|
} while (c.moveToNext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,16 +220,17 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
): PaginationResult<Key> = count().let { count ->
|
): PaginationResult<Key> = count().let { count ->
|
||||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||||
helper.blockingReadableTransaction {
|
helper.blockingReadableTransaction {
|
||||||
select(
|
selectDistinct(
|
||||||
tableName,
|
tableName,
|
||||||
|
idColumnArray,
|
||||||
selection = "$valueColumnName=?",
|
selection = "$valueColumnName=?",
|
||||||
selectionArgs = arrayOf(v.asValue()),
|
selectionArgs = arrayOf(v.valueAsString()),
|
||||||
limit = resultPagination.limitClause()
|
limit = resultPagination.limitClause()
|
||||||
).use { c ->
|
).use { c ->
|
||||||
mutableListOf<Key>().also {
|
mutableListOf<Key>().also {
|
||||||
if (c.moveToFirst()) {
|
if (c.moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
it.add(c.getString(idColumnName).asKey())
|
it.add(c.getString(idColumnName).keyFromString())
|
||||||
} while (c.moveToNext())
|
} while (c.moveToNext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +245,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
toRemove.flatMap { (k, vs) ->
|
toRemove.flatMap { (k, vs) ->
|
||||||
vs.mapNotNullA { v ->
|
vs.mapNotNullA { v ->
|
||||||
if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.asId(), v.asValue())) > 0) {
|
if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.keyAsString(), v.valueAsString())) > 0) {
|
||||||
k to v
|
k to v
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -233,3 +257,24 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <Key, Value> OneToManyAndroidRepo(
|
||||||
|
tableName: String,
|
||||||
|
keySerializer: KSerializer<Key>,
|
||||||
|
valueSerializer: KSerializer<Value>,
|
||||||
|
helper: SQLiteOpenHelper
|
||||||
|
) = OneToManyAndroidRepo(
|
||||||
|
tableName,
|
||||||
|
{ internalSerialFormat.encodeToString(keySerializer, this) },
|
||||||
|
{ internalSerialFormat.encodeToString(valueSerializer, this) },
|
||||||
|
{ internalSerialFormat.decodeFromString(keySerializer, this) },
|
||||||
|
{ internalSerialFormat.decodeFromString(valueSerializer, this) },
|
||||||
|
helper
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <Key, Value> KeyValuesAndroidRepo(
|
||||||
|
tableName: String,
|
||||||
|
keySerializer: KSerializer<Key>,
|
||||||
|
valueSerializer: KSerializer<Value>,
|
||||||
|
helper: SQLiteOpenHelper
|
||||||
|
) = OneToManyAndroidRepo(tableName, keySerializer, valueSerializer, helper)
|
||||||
|
@@ -31,6 +31,9 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>(
|
|||||||
transaction(database) {
|
transaction(database) {
|
||||||
toAdd.keys.flatMap { k ->
|
toAdd.keys.flatMap { k ->
|
||||||
toAdd[k] ?.mapNotNull { v ->
|
toAdd[k] ?.mapNotNull { v ->
|
||||||
|
if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
insertIgnore {
|
insertIgnore {
|
||||||
it[keyColumn] = k
|
it[keyColumn] = k
|
||||||
it[valueColumn] = v
|
it[valueColumn] = v
|
||||||
|
@@ -86,6 +86,12 @@ class MapWriteOneToManyKeyValueRepo<Key, Value>(
|
|||||||
override suspend fun clear(k: Key) {
|
override suspend fun clear(k: Key) {
|
||||||
map.remove(k) ?.also { _onDataCleared.emit(k) }
|
map.remove(k) ?.also { _onDataCleared.emit(k) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun clearWithValue(v: Value) {
|
||||||
|
map.forEach { (k, values) ->
|
||||||
|
if (values.remove(v)) _onValueRemoved.emit(k to v)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapOneToManyKeyValueRepo<Key, Value>(
|
class MapOneToManyKeyValueRepo<Key, Value>(
|
||||||
|
@@ -67,6 +67,15 @@ class KtorWriteOneToManyKeyValueRepo<Key, Value> (
|
|||||||
Unit.serializer(),
|
Unit.serializer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun clearWithValue(v: Value) = unifiedRequester.unipost(
|
||||||
|
buildStandardUrl(
|
||||||
|
baseUrl,
|
||||||
|
clearWithValueRoute,
|
||||||
|
),
|
||||||
|
BodyPair(valueSerializer, v),
|
||||||
|
Unit.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun set(toSet: Map<Key, List<Value>>) = unifiedRequester.unipost(
|
override suspend fun set(toSet: Map<Key, List<Value>>) = unifiedRequester.unipost(
|
||||||
buildStandardUrl(
|
buildStandardUrl(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@@ -75,4 +84,4 @@ class KtorWriteOneToManyKeyValueRepo<Key, Value> (
|
|||||||
BodyPair(keyValueMapSerializer, toSet),
|
BodyPair(keyValueMapSerializer, toSet),
|
||||||
Unit.serializer(),
|
Unit.serializer(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -14,4 +14,5 @@ const val onDataClearedRoute = "onDataCleared"
|
|||||||
const val addRoute = "add"
|
const val addRoute = "add"
|
||||||
const val removeRoute = "remove"
|
const val removeRoute = "remove"
|
||||||
const val clearRoute = "clear"
|
const val clearRoute = "clear"
|
||||||
const val setRoute = "set"
|
const val clearWithValueRoute = "clearWithValue"
|
||||||
|
const val setRoute = "set"
|
||||||
|
@@ -72,6 +72,17 @@ fun <Key, Value> Route.configureOneToManyWriteKeyValueRepoRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post(clearWithValueRoute) {
|
||||||
|
unifiedRouter.apply {
|
||||||
|
val v = uniload(valueSerializer)
|
||||||
|
|
||||||
|
unianswer(
|
||||||
|
Unit.serializer(),
|
||||||
|
originalRepo.clearWithValue(v),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
post(setRoute) {
|
post(setRoute) {
|
||||||
unifiedRouter.apply {
|
unifiedRouter.apply {
|
||||||
val obj = uniload(keyValueMapSerializer)
|
val obj = uniload(keyValueMapSerializer)
|
||||||
|
@@ -12,7 +12,7 @@ open class TypedSerializer<T : Any>(
|
|||||||
) : KSerializer<T> {
|
) : KSerializer<T> {
|
||||||
protected val serializers = presetSerializers.toMutableMap()
|
protected val serializers = presetSerializers.toMutableMap()
|
||||||
@InternalSerializationApi
|
@InternalSerializationApi
|
||||||
override open val descriptor: SerialDescriptor = buildSerialDescriptor(
|
open override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||||
"TextSourceSerializer",
|
"TextSourceSerializer",
|
||||||
SerialKind.CONTEXTUAL
|
SerialKind.CONTEXTUAL
|
||||||
) {
|
) {
|
||||||
@@ -21,7 +21,7 @@ open class TypedSerializer<T : Any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@InternalSerializationApi
|
@InternalSerializationApi
|
||||||
override open fun deserialize(decoder: Decoder): T {
|
open override fun deserialize(decoder: Decoder): T {
|
||||||
return decoder.decodeStructure(descriptor) {
|
return decoder.decodeStructure(descriptor) {
|
||||||
var type: String? = null
|
var type: String? = null
|
||||||
lateinit var result: T
|
lateinit var result: T
|
||||||
@@ -50,7 +50,7 @@ open class TypedSerializer<T : Any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@InternalSerializationApi
|
@InternalSerializationApi
|
||||||
override open fun serialize(encoder: Encoder, value: T) {
|
open override fun serialize(encoder: Encoder, value: T) {
|
||||||
encoder.encodeStructure(descriptor) {
|
encoder.encodeStructure(descriptor) {
|
||||||
val valueSerializer = value::class.serializer()
|
val valueSerializer = value::class.serializer()
|
||||||
val type = serializers.keys.first { serializers[it] == valueSerializer }
|
val type = serializers.keys.first { serializers[it] == valueSerializer }
|
||||||
|
@@ -28,6 +28,9 @@ String[] includes = [
|
|||||||
":serialization:encapsulator",
|
":serialization:encapsulator",
|
||||||
":serialization:typed_serializer",
|
":serialization:typed_serializer",
|
||||||
|
|
||||||
|
":fsm:common",
|
||||||
|
":fsm:repos:common",
|
||||||
|
|
||||||
":dokka"
|
":dokka"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user