diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb0d97306a..892f2c75e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`: diff --git a/common/build.gradle b/common/build.gradle index 7c54502f100..14324f2f227 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -5,3 +5,18 @@ plugins { } apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + jvmMain { + dependencies { + api project(":micro_utils.coroutines") + } + } + androidMain { + dependencies { + api project(":micro_utils.coroutines") + } + } + } +} diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/InputAllocator.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/InputAllocator.kt index 2fd48505694..b33b406805d 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/InputAllocator.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/InputAllocator.kt @@ -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 { private val realSerializer = ByteArraySerializer() @@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer { override fun deserialize(decoder: Decoder): ByteArrayAllocator { val bytes = realSerializer.deserialize(decoder) - return { bytes } + return bytes.asAllocator } override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt new file mode 100644 index 00000000000..c60eb0d6b80 --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt @@ -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() + diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt new file mode 100644 index 00000000000..b163f489506 --- /dev/null +++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt @@ -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 { 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 diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/PromiseAwait.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/PromiseAwait.kt new file mode 100644 index 00000000000..6c2f3bfcd16 --- /dev/null +++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/PromiseAwait.kt @@ -0,0 +1,8 @@ +package dev.inmo.micro_utils.common + +import kotlin.coroutines.* +import kotlin.js.Promise + +suspend fun Promise.await(): T = suspendCoroutine { cont -> + then({ cont.resume(it) }, { cont.resumeWithException(it) }) +} diff --git a/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt b/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt new file mode 100644 index 00000000000..770dfc95f7f --- /dev/null +++ b/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt @@ -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() + } + } + } diff --git a/fsm/common/build.gradle b/fsm/common/build.gradle new file mode 100644 index 00000000000..854f51c5dfb --- /dev/null +++ b/fsm/common/build.gradle @@ -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") + } + } + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/State.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/State.kt new file mode 100644 index 00000000000..818c15f0a35 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/State.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.fsm.common + +interface State { + val context: Any +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StateHandlerHolder.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StateHandlerHolder.kt new file mode 100644 index 00000000000..5b06f317c16 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StateHandlerHolder.kt @@ -0,0 +1,15 @@ +package dev.inmo.micro_utils.fsm.common + +import kotlin.reflect.KClass + +class StateHandlerHolder( + private val inputKlass: KClass, + private val strict: Boolean = false, + private val delegateTo: StatesHandler +) : StatesHandler { + 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) } + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesHandler.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesHandler.kt new file mode 100644 index 00000000000..b152b38dea6 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesHandler.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.fsm.common + +fun interface StatesHandler { + suspend fun StatesMachine.handleState(state: I): State? +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt new file mode 100644 index 00000000000..5c2ea9592d9 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesMachine.kt @@ -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 StatesMachine.launchStateHandling( + state: State, + handlers: List> +): State? { + return handlers.firstOrNull { it.checkHandleable(state) } ?.run { + handleState(state) + } +} + +class StatesMachine ( + private val statesManager: StatesManager, + private val handlers: List> +) : StatesHandler { + 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) + } +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesManager.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesManager.kt new file mode 100644 index 00000000000..ac52d84b6e8 --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/StatesManager.kt @@ -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> + val onStartChain: Flow + val onEndChain: Flow + + + /** + * 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 +} + +/** + * @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>(0) + override val onChainStateUpdated: Flow> = _onChainStateUpdated.asSharedFlow() + private val _onStartChain = MutableSharedFlow(0) + override val onStartChain: Flow = _onStartChain.asSharedFlow() + private val _onEndChain = MutableSharedFlow(0) + override val onEndChain: Flow = _onEndChain.asSharedFlow() + + private val contextsToStates = mutableMapOf() + 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 = contextsToStates.values.toList() + +} diff --git a/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt new file mode 100644 index 00000000000..f0694f360ef --- /dev/null +++ b/fsm/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/common/dsl/FSMBuilder.kt @@ -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>() + + fun add(kClass: KClass, handler: StatesHandler) { + states.add(StateHandlerHolder(kClass, false, handler)) + } + + fun addStrict(kClass: KClass, handler: StatesHandler) { + states.add(StateHandlerHolder(kClass, true, handler)) + } + + fun build() = StatesMachine( + statesManager, + states.toList() + ) +} + +inline fun FSMBuilder.onStateOrSubstate(handler: StatesHandler) { + add(I::class, handler) +} + +inline fun FSMBuilder.strictlyOn(handler: StatesHandler) { + addStrict(I::class, handler) +} + +fun buildFSM( + block: FSMBuilder.() -> Unit +): StatesMachine = FSMBuilder().apply(block).build() diff --git a/fsm/common/src/jvmTest/kotlin/PlayableMain.kt b/fsm/common/src/jvmTest/kotlin/PlayableMain.kt new file mode 100644 index 00000000000..a317c2a5f17 --- /dev/null +++ b/fsm/common/src/jvmTest/kotlin/PlayableMain.kt @@ -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 { + delay(1000L) + YellowCommon(it.context).also(::println) + } + strictlyOn { + delay(1000L) + RedCommon(it.context).also(::println) + } + strictlyOn { + delay(1000L) + GreenCommon(it.context).also(::println) + } + this.statesManager = statesManager + } + + initialStates.forEach { machine.startChain(it) } + + val scope = CoroutineScope(Dispatchers.Default) + machine.start(scope).join() + + } + } +} diff --git a/fsm/common/src/main/AndroidManifest.xml b/fsm/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..2a1eb7f83d7 --- /dev/null +++ b/fsm/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/fsm/repos/common/build.gradle b/fsm/repos/common/build.gradle new file mode 100644 index 00000000000..d20bb3f38a4 --- /dev/null +++ b/fsm/repos/common/build.gradle @@ -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") + } + } + } +} diff --git a/fsm/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/repos/common/KeyValueBasedStatesManager.kt b/fsm/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/repos/common/KeyValueBasedStatesManager.kt new file mode 100644 index 00000000000..4f6f98d536c --- /dev/null +++ b/fsm/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/fsm/repos/common/KeyValueBasedStatesManager.kt @@ -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, + private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true } +) : StatesManager { + private val _onChainStateUpdated = MutableSharedFlow>(0) + override val onChainStateUpdated: Flow> = _onChainStateUpdated.asSharedFlow() + private val _onEndChain = MutableSharedFlow(0) + override val onEndChain: Flow = _onEndChain.asSharedFlow() + + override val onStartChain: Flow = 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 { + return keyValueRepo.getAll { keys(it) }.map { it.second } + } + +} + +inline fun createStatesManager( + targetKeyValueRepo: KeyValueRepo, + noinline contextToOutTransformer: suspend Any.() -> TargetContextType, + noinline stateToOutTransformer: suspend State.() -> TargetStateType, + noinline outToContextTransformer: suspend TargetContextType.() -> Any, + noinline outToStateTransformer: suspend TargetStateType.() -> State, +) = KeyValueBasedStatesManager( + targetKeyValueRepo.withMapper( + contextToOutTransformer, + stateToOutTransformer, + outToContextTransformer, + outToStateTransformer + ) +) diff --git a/fsm/repos/common/src/main/AndroidManifest.xml b/fsm/repos/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..d0773843842 --- /dev/null +++ b/fsm/repos/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/gradle.properties b/gradle.properties index f6220a8c7ae..b853266f659 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/serialization/typed_serializer/src/commonMain/kotlin/dev/inmo/micro_utils/serialization/typed_serializer/TypedSerializer.kt b/serialization/typed_serializer/src/commonMain/kotlin/dev/inmo/micro_utils/serialization/typed_serializer/TypedSerializer.kt index d1f75914f4c..9a54292e629 100644 --- a/serialization/typed_serializer/src/commonMain/kotlin/dev/inmo/micro_utils/serialization/typed_serializer/TypedSerializer.kt +++ b/serialization/typed_serializer/src/commonMain/kotlin/dev/inmo/micro_utils/serialization/typed_serializer/TypedSerializer.kt @@ -12,7 +12,7 @@ open class TypedSerializer( ) : KSerializer { 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( } @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( } @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 } diff --git a/settings.gradle b/settings.gradle index 1cdf43a6d64..7fcae92134f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,9 @@ String[] includes = [ ":serialization:encapsulator", ":serialization:typed_serializer", + ":fsm:common", + ":fsm:repos:common", + ":dokka" ]