Merge pull request #77 from InsanusMokrassar/0.5.13

0.5.13
This commit is contained in:
InsanusMokrassar 2021-06-23 21:29:56 +06:00 committed by GitHub
commit 960c38b696
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 503 additions and 6 deletions

View File

@ -1,5 +1,14 @@
# Changelog
## 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`:

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

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

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

@ -45,5 +45,5 @@ dokka_version=1.4.32
# Project data
group=dev.inmo
version=0.5.12
android_code_version=53
version=0.5.13
android_code_version=54

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"
]