Compare commits

...

36 Commits

Author SHA1 Message Date
a33ad123f6 Update CHANGELOG.md 2021-07-10 00:36:37 +06:00
7e14fa2f5c Update gradle-wrapper.properties 2021-07-10 00:08:17 +06:00
ba698b41e1 Update dependencies 2021-07-10 00:07:51 +06:00
e76215987e start 0.5.16 2021-07-10 00:04:15 +06:00
d1a247af8c Merge pull request #79 from InsanusMokrassar/0.5.15
0.5.15
2021-06-28 01:00:58 +06:00
2b7e9534f3 hotfix 2021-06-28 00:31:46 +06:00
38521558a1 start 0.5.15 2021-06-27 23:18:08 +06:00
100f3d214b Delete .travis.yml 2021-06-26 19:39:37 +06:00
1309867611 Init space CI 2021-06-26 13:38:30 +00:00
611f64f2e1 Merge pull request #78 from InsanusMokrassar/0.5.14
0.5.14
2021-06-26 00:58:02 +06:00
f118ebce6e update kotlin 2021-06-26 00:50:09 +06:00
59fc90e556 add subscribeAsync 2021-06-26 00:46:51 +06:00
fb9e4d57fb start 0.5.14 2021-06-25 17:19:28 +06:00
960c38b696 Merge pull request #77 from InsanusMokrassar/0.5.13
0.5.13
2021-06-23 21:29:56 +06:00
39895e58a6 changelog upsert 2021-06-23 21:29:28 +06:00
b420d85be5 MPPFile 2021-06-22 13:36:23 +06:00
19ea2f340a replace repos common extension for fsm 2021-06-20 17:11:51 +06:00
11b0d059bf start add fsm 2021-06-19 14:41:29 +06:00
c8a25ce544 start 0.5.13 2021-06-18 13:05:45 +06:00
509583ea2e Merge pull request #76 from InsanusMokrassar/0.5.12
0.5.12
2021-06-17 13:54:36 +06:00
1c86f3f4bf wrap with trycatch StateFlowBasedRecyclerViewAdapter listener 2021-06-17 13:46:22 +06:00
6d999be590 small improvement in StateFlowBasedRecyclerViewAdapter 2021-06-17 13:45:19 +06:00
e715772dbf fill changes 2021-06-17 13:39:02 +06:00
63eb7b7ea8 start 0.5.12 2021-06-17 12:54:06 +06:00
b07683b815 Merge pull request #75 from InsanusMokrassar/0.5.11
0.5.11
2021-06-16 13:25:56 +06:00
96e97d1691 ExposedOneToManyKeyValueRepo fixes 2021-06-16 13:22:40 +06:00
261d8827e3 OneToMany fixes 2021-06-16 13:20:05 +06:00
c3156f2e41 strt 0.5.11 2021-06-16 13:15:25 +06:00
8c08801460 Merge pull request #74 from InsanusMokrassar/0.5.10
0.5.10
2021-06-15 14:38:26 +06:00
aaf1299da7 fill changelog 2021-06-15 14:35:11 +06:00
a411355b4f fixes 2021-06-15 14:24:00 +06:00
eba41066b4 several small improvements in OneToManyAndroidRepo 2021-06-15 01:37:12 +06:00
f295dff8a2 update dependencies 2021-06-14 22:10:25 +06:00
a16815143c doForAllWithCurrentPaging and fun interface for elements in ktor server 2021-06-14 22:04:39 +06:00
6ff3f6ae42 start 0.5.10 2021-06-14 22:02:41 +06:00
84071881af Merge pull request #73 from InsanusMokrassar/0.5.9
0.5.9
2021-06-13 11:56:10 +06:00
47 changed files with 913 additions and 75 deletions

8
.space.kts Normal file
View File

@@ -0,0 +1,8 @@
job("Build and run tests") {
container(displayName = "Run gradle build", image = "openjdk:11") {
kotlinScript { api ->
// here can be your complex logic
api.gradlew("build")
}
}
}

View File

@@ -1,27 +0,0 @@
language: android
install: true
os: linux
dist: trusty
jdk: oraclejdk8
android:
components:
- tools
- platform-tools
- build-tools-30.0.2
- android-30
- add-on
- extra
before_script:
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2"
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30"
jobs:
include:
- stage: build
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
# Tests are temporarily disabled on public travis due to the problems of launching
# - state: test
# script: ./gradlew allTests

View File

@@ -1,5 +1,78 @@
# Changelog
## 0.5.16
* `Versions`
* `Coroutines`: `1.5.0` -> `1.5.1`
* `Serialization`: `1.2.1` -> `1.2.2`
* `Ktor`: `1.6.0` -> `1.6.1`
* `Klock`: `2.1.2` -> `2.2.0`
* `Core KTX`: `1.5.0` -> `1.6.0`
## 0.5.15 HOTFIX FOR 0.5.14
* `Coroutines`
* Fixes in `subscribeAsync`
## 0.5.14 NOT RECOMMENDED
* `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`

View File

@@ -35,9 +35,9 @@ class ActionViewHolder(
}
class ActionsRecyclerViewAdapter(
data: List<AlertAction>,
override val data: List<AlertAction>,
private val dialogInterfaceGetter: () -> DialogInterface
) : RecyclerViewAdapter<AlertAction>(data) {
) : RecyclerViewAdapter<AlertAction>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
parent, dialogInterfaceGetter
)

View File

@@ -11,6 +11,7 @@ kotlin {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api project(":micro_utils.common")
}
}
androidMain {

View File

@@ -1,12 +1,21 @@
package dev.inmo.micro_utils.android.recyclerview
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.*
abstract class RecyclerViewAdapter<T>(
val data: List<T>
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
protected abstract val data: List<T>
private val _dataCountState by lazy {
MutableStateFlow<Int>(data.size)
}
val dataCountState: StateFlow<Int> by lazy {
_dataCountState.asStateFlow()
}
var emptyView: View? = null
set(value) {
field = value
@@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>(
object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
_dataCountState.value = data.size
checkEmpty()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
super.onItemRangeChanged(positionStart, itemCount, payload)
_dataCountState.value = data.size
checkEmpty()
}
override fun onChanged() {
super.onChanged()
_dataCountState.value = data.size
checkEmpty()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
_dataCountState.value = data.size
checkEmpty()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
_dataCountState.value = data.size
checkEmpty()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
_dataCountState.value = data.size
checkEmpty()
}
}
@@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>(
private fun checkEmpty() {
emptyView ?. let {
if (data.isEmpty()) {
if (dataCountState.value == 0) {
it.visibility = View.VISIBLE
} else {
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)
}

View File

@@ -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)
}
}

View File

@@ -5,3 +5,18 @@ plugins {
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
jvmMain {
dependencies {
api project(":micro_utils.coroutines")
}
}
androidMain {
dependencies {
api project(":micro_utils.coroutines")
}
}
}
}

View File

@@ -16,7 +16,7 @@ package dev.inmo.micro_utils.common
AnnotationTarget.TYPEALIAS,
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(
"This thing is marked as warned. See message of warn to get more info",

View File

@@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
typealias ByteArrayAllocator = () -> ByteArray
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
val ByteArray.asAllocator: ByteArrayAllocator
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> {
private val realSerializer = ByteArraySerializer()
@@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
val bytes = realSerializer.deserialize(decoder)
return { bytes }
return bytes.asAllocator
}
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {

View File

@@ -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()

View File

@@ -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

View File

@@ -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) })
}

View File

@@ -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()
}
}
}

View File

@@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) {
show()
}
}
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
if (show) {
show()
} else {
if (goneOnHide) {
gone()
} else {
hide()
}
}
}

View File

@@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* 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(
scope: CoroutineScope,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
noinline block: suspend (T) -> Unit
) = subscribe(scope) {
safelyWithoutExceptions {
safelyWithoutExceptions(onException) {
block(it)
}
}

View File

@@ -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.LinkedSupervisorScope()) {
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 { if (subscope.isActive) 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)
}
}

17
fsm/common/build.gradle Normal file
View 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")
}
}
}
}

View File

@@ -0,0 +1,5 @@
package dev.inmo.micro_utils.fsm.common
interface State {
val context: Any
}

View File

@@ -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) }
}
}

View File

@@ -0,0 +1,5 @@
package dev.inmo.micro_utils.fsm.common
fun interface StatesHandler<I : State> {
suspend fun StatesMachine.handleState(state: I): State?
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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()

View 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()
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.fsm.common"/>

View 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")
}
}
}
}

View File

@@ -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
)
)

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>

View File

@@ -7,14 +7,14 @@ android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g
kotlin_version=1.5.10
kotlin_coroutines_version=1.5.0
kotlin_serialisation_core_version=1.2.1
kotlin_version=1.5.20
kotlin_coroutines_version=1.5.1
kotlin_serialisation_core_version=1.2.2
kotlin_exposed_version=0.32.1
ktor_version=1.6.0
ktor_version=1.6.1
klockVersion=2.1.2
klockVersion=2.2.0
github_release_plugin_version=2.2.12
@@ -22,14 +22,14 @@ uuidVersion=0.3.0
# ANDROID
core_ktx_version=1.3.2
androidx_recycler_version=1.2.0
appcompat_version=1.2.0
core_ktx_version=1.6.0
androidx_recycler_version=1.2.1
appcompat_version=1.3.0
android_minSdkVersion=19
android_compileSdkVersion=30
android_buildToolsVersion=30.0.3
dexcount_version=2.0.0
dexcount_version=2.1.0-RC01
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
@@ -45,5 +45,5 @@ dokka_version=1.4.32
# Project data
group=dev.inmo
version=0.5.9
android_code_version=50
version=0.5.16
android_code_version=57

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
data class ApplicationCachingHeadersConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
interface Element { operator fun CachingHeaders.Configuration.invoke() }
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
override fun Application.configure() {
install(CachingHeaders) {

View File

@@ -10,17 +10,18 @@ import kotlinx.serialization.Serializable
class ApplicationRoutingConfigurator(
private val elements: List<@Contextual Element>
) : 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() {
try {
feature(Routing)
} catch (e: IllegalStateException) {
install(Routing) {
elements.forEach {
it.apply { invoke() }
}
}
featureOrNull(Routing) ?.apply {
rootInstaller.apply { invoke() }
} ?: install(Routing) {
rootInstaller.apply { invoke() }
}
}
}

View File

@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
class ApplicationSessionsConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
interface Element { operator fun Sessions.Configuration.invoke() }
fun interface Element { operator fun Sessions.Configuration.invoke() }
override fun Application.configure() {
install(Sessions) {

View File

@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
class StatusPagesConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
interface Element { operator fun StatusPages.Configuration.invoke() }
fun interface Element { operator fun StatusPages.Configuration.invoke() }
override fun Application.configure() {
install(StatusPages) {

View File

@@ -33,3 +33,8 @@ suspend fun <T> doAllWithCurrentPaging(
block
)
}
suspend fun <T> doForAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
) = doAllWithCurrentPaging(initialPagination, block)

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.repos
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.doForAllWithCurrentPaging
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
import kotlinx.coroutines.flow.Flow
@@ -47,6 +48,7 @@ interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
suspend fun remove(toRemove: Map<Key, List<Value>>)
suspend fun clear(k: Key)
suspend fun clearWithValue(v: Value)
suspend fun set(toSet: Map<Key, List<Value>>) {
toSet.keys.forEach { key -> clear(key) }
@@ -87,7 +89,19 @@ suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
k: Key, vararg v: Value
) = 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>
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(

View File

@@ -114,6 +114,7 @@ open class MapperWriteOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
}
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
override suspend fun clearWithValue(v: FromValue) = to.clearWithValue(v.toOutValue())
}
@Suppress("NOTHING_TO_INLINE")

View File

@@ -74,6 +74,19 @@ fun SQLiteDatabase.select(
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 {
return (0 until count).joinToString { "?" }
}

View File

@@ -3,10 +3,7 @@ package dev.inmo.micro_utils.repos.onetomany
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.content.contentValuesOf
import dev.inmo.micro_utils.common.mapNotNullA
import dev.inmo.micro_utils.pagination.FirstPagePagination
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.*
import dev.inmo.micro_utils.pagination.utils.reverse
import dev.inmo.micro_utils.repos.*
import kotlinx.coroutines.flow.Flow
@@ -38,7 +35,9 @@ class OneToManyAndroidRepo<Key, Value>(
override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
private val idColumnName = "id"
private val idColumnArray = arrayOf(idColumnName)
private val valueColumnName = "value"
private val valueColumnArray = arrayOf(valueColumnName)
init {
helper.blockingWritableTransaction {
@@ -56,6 +55,17 @@ class OneToManyAndroidRepo<Key, Value>(
helper.blockingWritableTransaction {
for ((k, values) in toAdd) {
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(
tableName,
null,
@@ -108,7 +118,7 @@ class OneToManyAndroidRepo<Key, Value>(
}
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = firstPageWithOneElementPagination.limitClause()).use {
it.count > 0
}
}
@@ -124,7 +134,7 @@ class OneToManyAndroidRepo<Key, Value>(
}
}
override suspend fun count(): Long =helper.blockingReadableTransaction {
override suspend fun count(): Long = helper.blockingReadableTransaction {
select(
tableName
).use {
@@ -133,7 +143,7 @@ class OneToManyAndroidRepo<Key, Value>(
}.toLong()
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
it.count
}
}.toLong()
@@ -143,10 +153,17 @@ class OneToManyAndroidRepo<Key, Value>(
pagination: Pagination,
reversed: Boolean
): 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 }
helper.blockingReadableTransaction {
select(
tableName,
valueColumnArray,
selection = "$idColumnName=?",
selectionArgs = arrayOf(k.keyAsString()),
limit = resultPagination.limitClause()
@@ -169,10 +186,17 @@ class OneToManyAndroidRepo<Key, Value>(
pagination: Pagination,
reversed: Boolean
): 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 }
helper.blockingReadableTransaction {
select(
selectDistinct(
tableName,
idColumnArray,
limit = resultPagination.limitClause()
).use { c ->
mutableListOf<Key>().also {
@@ -196,8 +220,9 @@ class OneToManyAndroidRepo<Key, Value>(
): PaginationResult<Key> = count().let { count ->
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
helper.blockingReadableTransaction {
select(
selectDistinct(
tableName,
idColumnArray,
selection = "$valueColumnName=?",
selectionArgs = arrayOf(v.valueAsString()),
limit = resultPagination.limitClause()

View File

@@ -31,6 +31,9 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>(
transaction(database) {
toAdd.keys.flatMap { k ->
toAdd[k] ?.mapNotNull { v ->
if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
return@mapNotNull null
}
insertIgnore {
it[keyColumn] = k
it[valueColumn] = v

View File

@@ -86,6 +86,12 @@ class MapWriteOneToManyKeyValueRepo<Key, Value>(
override suspend fun clear(k: Key) {
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>(

View File

@@ -67,6 +67,15 @@ class KtorWriteOneToManyKeyValueRepo<Key, Value> (
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(
buildStandardUrl(
baseUrl,
@@ -75,4 +84,4 @@ class KtorWriteOneToManyKeyValueRepo<Key, Value> (
BodyPair(keyValueMapSerializer, toSet),
Unit.serializer(),
)
}
}

View File

@@ -14,4 +14,5 @@ const val onDataClearedRoute = "onDataCleared"
const val addRoute = "add"
const val removeRoute = "remove"
const val clearRoute = "clear"
const val setRoute = "set"
const val clearWithValueRoute = "clearWithValue"
const val setRoute = "set"

View File

@@ -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) {
unifiedRouter.apply {
val obj = uniload(keyValueMapSerializer)

View File

@@ -12,7 +12,7 @@ open class TypedSerializer<T : Any>(
) : KSerializer<T> {
protected val serializers = presetSerializers.toMutableMap()
@InternalSerializationApi
override open val descriptor: SerialDescriptor = buildSerialDescriptor(
open override val descriptor: SerialDescriptor = buildSerialDescriptor(
"TextSourceSerializer",
SerialKind.CONTEXTUAL
) {
@@ -21,7 +21,7 @@ open class TypedSerializer<T : Any>(
}
@InternalSerializationApi
override open fun deserialize(decoder: Decoder): T {
open override fun deserialize(decoder: Decoder): T {
return decoder.decodeStructure(descriptor) {
var type: String? = null
lateinit var result: T
@@ -50,7 +50,7 @@ open class TypedSerializer<T : Any>(
}
@InternalSerializationApi
override open fun serialize(encoder: Encoder, value: T) {
open override fun serialize(encoder: Encoder, value: T) {
encoder.encodeStructure(descriptor) {
val valueSerializer = value::class.serializer()
val type = serializers.keys.first { serializers[it] == valueSerializer }

View File

@@ -28,6 +28,9 @@ String[] includes = [
":serialization:encapsulator",
":serialization:typed_serializer",
":fsm:common",
":fsm:repos:common",
":dokka"
]