Skip to content

Commit

Permalink
Merge branch '0.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
nsk90 committed Mar 26, 2024
2 parents 4f7fff2 + 62d1849 commit b7c29a3
Show file tree
Hide file tree
Showing 34 changed files with 1,043 additions and 121 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
out/
build/
*.iml
local.properties
local.properties
kotlin-js-store/
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
[![codecov](https://codecov.io/gh/nsk90/kstatemachine/branch/master/graph/badge.svg?token=IR2JR43FOZ)](https://codecov.io/gh/nsk90/kstatemachine)
[![Maven Central Version](https://img.shields.io/maven-central/v/io.github.nsk90/kstatemachine?logo=sonatype)](https://central.sonatype.com/artifact/io.github.nsk90/kstatemachine)
[![JitPack](https://img.shields.io/jitpack/version/io.github.nsk90/kstatemachine?style=flat&logo=jitpack&color=brgreen)](https://jitpack.io/#nsk90/kstatemachine)
[![multiplatform support](https://img.shields.io/badge/multiplatform-jvm%20%7C%20android%20%7C%20ios-brightgreen)](https://kstatemachine.github.io/kstatemachine/#multiplatform)
[![multiplatform support](https://img.shields.io/badge/multiplatform-jvm%20%7C%20android%20%7C%20ios%20%7C%20js%20%7C%20wasm-brightgreen)](https://kstatemachine.github.io/kstatemachine/#multiplatform)

[![Open Collective](https://img.shields.io/badge/open%20collective-kstatemachine-lightblue?logo=opencollective&style=flat)](https://opencollective.com/kstatemachine)
[![JetBrains support](https://img.shields.io/badge/JetBrains-support-black?style=flat&logo=jetbrains)](https://jb.gg/OpenSourceSupport)
[![Mentioned in Awesome Kotlin](https://awesome.re/mentioned-badge.svg)](https://github.com/KotlinBy/awesome-kotlin)
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-kstatemachine-green.svg?style=flat)](https://android-arsenal.com/details/1/8276)
[![Share on X](https://img.shields.io/badge/twitter-share-white?logo=x&style=flat)](https://twitter.com/intent/tweet?text=I%20like%20KStateMachine%20library%20%0A%0Ahttps%3A%2F%2Fgithub.com%2Fkstatemachine%2Fkstatemachine&hashtags=kstatemachine,kotlin,opensource)
[![Share on Reddit](https://img.shields.io/badge/reddit-share-red?logo=reddit&style=flat)](https://www.reddit.com/submit?url=https%3A%2F%2Fgithub.com%2Fkstatemachine%2Fkstatemachine&title=I%20like%20KStateMachine%20library)


**[Documentation](https://kstatemachine.github.io/kstatemachine) | [Sponsors](#sponsors-) | [Quick start](#quick-start-sample) | [Samples](#samples) | [Install](#install) | [Contribution](#contribution) | [License](#license) | [Discussions](https://github.com/kstatemachine/kstatemachine/discussions)**

KStateMachine is a Kotlin DSL library for creating [state machines](https://en.wikipedia.org/wiki/Finite-state_machine)
Expand All @@ -35,27 +35,37 @@ Integration features are:

State management features:

* **Event based** - [transitions](https://kstatemachine.github.io/kstatemachine/#setup-transitions) are performed by processing
* **Event based** - [transitions](https://kstatemachine.github.io/kstatemachine/#setup-transitions) are performed by
processing
incoming events
* **[Reactive](https://kstatemachine.github.io/kstatemachine/#listen-states)** - listen for machine, states,
[state groups](https://kstatemachine.github.io/kstatemachine/#listen-group-of-states) and transitions
* **[Guarded](https://kstatemachine.github.io/kstatemachine/#guarded-transitions)
and [Conditional transitions](https://kstatemachine.github.io/kstatemachine/#conditional-transitions)** - dynamic target
and [Conditional transitions](https://kstatemachine.github.io/kstatemachine/#conditional-transitions)** - dynamic
target
state which is calculated in a moment of event processing depending on application business logic
* **[Nested states](https://kstatemachine.github.io/kstatemachine/#nested-states)** - build hierarchical state machines
(statecharts)
with [cross-level transitions](https://kstatemachine.github.io/kstatemachine/#cross-level-transitions) support
* **[Composed (nested) state machines.](https://kstatemachine.github.io/kstatemachine/#composed-(nested)-state-machines)** Use
* **[Composed (nested) state machines.](https://kstatemachine.github.io/kstatemachine/#composed-(nested)-state-machines)
** Use
state machines as atomic child states
* **[Pseudo states](https://kstatemachine.github.io/kstatemachine/#pseudo-states)** for additional logic in machine behaviour
* **[Typesafe transitions](https://kstatemachine.github.io/kstatemachine/#typesafe-transitions)** - pass data in typesafe way
* **[Pseudo states](https://kstatemachine.github.io/kstatemachine/#pseudo-states)** for additional logic in machine
behaviour
* **[Typesafe transitions](https://kstatemachine.github.io/kstatemachine/#typesafe-transitions)** - pass data in
typesafe way
from event to state
* **[Parallel states](https://kstatemachine.github.io/kstatemachine/#parallel-states)** - avoid a combinatorial explosion of
* **[Parallel states](https://kstatemachine.github.io/kstatemachine/#parallel-states)** - avoid a combinatorial
explosion of
states
* **[Undo transitions](https://kstatemachine.github.io/kstatemachine/#undo-transitions)** - navigate back to previous state
* **[Optional argument](https://kstatemachine.github.io/kstatemachine/#optinal-arguments)** passing for events and transitions
* **[Undo transitions](https://kstatemachine.github.io/kstatemachine/#undo-transitions)** - navigate back to previous
state
* **[Optional argument](https://kstatemachine.github.io/kstatemachine/#optinal-arguments)** passing for events and
transitions
* **[Export](https://kstatemachine.github.io/kstatemachine/#export)** state machine structure
to [PlantUML](https://plantuml.com/) and [Mermaid](https://mermaid.js.org/) diagrams
* **[Persist (serialize)](https://kstatemachine.github.io/kstatemachine/#persistence)** state machine's active
configuration and restore it later
* **[Logging](https://kstatemachine.github.io/kstatemachine/#logging)** - useful for debugging
* **[Testable](https://kstatemachine.github.io/kstatemachine/#testing)** - run state machine from specified state
* **Well tested** - all features are covered
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
kotlin("jvm") version Versions.kotlin apply false
id("org.jetbrains.dokka") version Versions.kotlinDokka
}

group = Versions.libraryMavenCentralGroup
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/ru/nsk/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ object Versions {
const val apiVersion = "1.5"

// dependencies
const val coroutinesCore = "1.7.3"
const val coroutinesCore = "1.8.0"

// test dependencies
const val mockk = "1.13.9"
const val mockk = "1.13.10"
const val kotest = "5.8.0"
}
40 changes: 37 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
* [PlantUML](#plantuml)
* [Mermaid](#mermaid)
* [Controlling export output](#controlling-export-output)
* [Persistence](#persistence)
* [Event recording](#event-recording)
* [Testing](#testing)
* [Multiplatform](#multiplatform)
* [Consider using Kotlin sealed classes](#consider-using-kotlin-sealed-classes)
Expand Down Expand Up @@ -423,7 +425,7 @@ like this `machine.processEvent(UndoEvent)`. State Machine will roll back last t
to previous state (except target-less transitions).
This API might be called as many times as needed.
To implement this feature library stores transitions in a stack, it takes memory,
so this feature is disabled by default and must be enabled explicitly using
so this feature is disabled by default and must be enabled explicitly using
`createStateMachine(creationArguments = CreationArguments(isUndoEnabled = true))` argument.

Undo functionality is implemented as `Event`, so it possible to call `undo()` from notification callbacks, if you use
Expand Down Expand Up @@ -786,6 +788,8 @@ createStateMachine(scope) {
}
```
The library provides implementation of such throwing handler by `throwingIgnoredEventHandler()` function.
### Pending events
Pending events are such events that are posted for processing while another event is already processing, for example
Expand Down Expand Up @@ -867,7 +871,7 @@ conditions, it is not correct.
Even `Dispatchers.Default.limitedParallelism(1)` that seems to be ok at glance,
does not provide guarantee that each coroutine will be executed on the same single thread, it only limits the amount of
used threads. So race condition still takes place, as nothing forces threads, running on different processor cores,
to update variable values in their processor core caches, so outdated values could be used from core cache. Other words,
to update variable values in their processor core caches, so outdated values could be used from core cache. Other words,
one thread does not to know about variable changes made by other one. This known as __visibility guarantee__,
that `volatile` keyword provides on `jvm`.
Expand Down Expand Up @@ -1024,6 +1028,34 @@ state("State1") {
See [PlantUML with MetaInfo export sample](https://github.com/nsk90/kstatemachine/tree/master/samples/src/commonMain/kotlin/ru/nsk/samples/PlantUmlExportWithMetaInfoSample.kt)
## Persistence
**Persist** `StateMachine` - means transform it into serializable representation, such as `Serializable` object or
JSON text, and possibly saving it into some persistent storage like file or sending by network.
**Restoration** is a process of restoring the `StateMachine` from the serializable representation.
There are several kinds or levels of `StateMachine` persistence (serialization). Let's look at sample use cases:

1) **Structure + configuration** - Create `StateMachine` on some process/host and send its structure and
active configuration by network to another process/host.
The receiver can dynamically create the same `StateMachine` instance in the same state as original one.
2) **Configuration only** - Both original and restored `StateMachine` instances are crated by identical static code
(in a single or multiple different processes/hosts). Only active configuration can be saved and restored.

Case 1 currently lacks built-in support by the library (you can open an issue if you need something like that).
Case 2 in turn may be reached in two different ways:

1) **Persisting state** - serializing all internal data, active states, variables etc. from original `StateMachine` and
applying them to restored one.
2) **Event recording** - serializing all incoming events, and applying them later on new `StateMachine` instance,
which should lead it into the same state as original. This also allows to execute library callbacks (listeners)
if necessary, which is not possible with state persistence approach.
_Currently only this approach has built-in support._

### Event recording

WIP

## Testing

For testing, it might be useful to check how state machine reacts on events from particular state. There
Expand All @@ -1045,7 +1077,9 @@ machine.startFrom(state2)
## Multiplatform

Starting from v0.22.0 KStateMachine has moved to Kotlin Multiplatform only with `JVM` platform support.
In v0.22.1 `iOS` support has been added also.
In **v0.22.1** `iOS` support has been added also, **v0.30.0** adds `js` and `wasm` targets.
`js` and `wasm` targets do not support blocking library apis as those platforms do not have `runBlocking` support which
is used internally.

_If you need missing platform support please create a GitHub issue._

Expand Down
22 changes: 20 additions & 2 deletions kstatemachine-coroutines/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
kotlin("multiplatform")
`java-library`
ru.nsk.`maven-publish`
id("org.jetbrains.dokka") version Versions.kotlinDokka
id("org.jetbrains.dokka")
}

group = rootProject.group
Expand All @@ -21,7 +21,14 @@ kotlin {
iosArm64()
iosX64()
iosSimulatorArm64()
js {
browser()
nodejs()
}
@Suppress("OPT_IN_USAGE") // this is alpha feature
wasmJs()

applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies {
Expand All @@ -30,5 +37,16 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutinesCore}")
}
}

// contains blocking APIs which are not supported on JS
val blockingMain by creating { dependsOn(commonMain.get()) }
val jsCommonMain by creating { dependsOn(commonMain.get()) }

jvmMain.get().dependsOn(blockingMain)
iosMain.get().dependsOn(blockingMain)

val wasmJsMain by getting
wasmJsMain.dependsOn(jsCommonMain)
jsMain.get().dependsOn(jsCommonMain)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ru.nsk.kstatemachine.coroutines

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.CoroutineContext

internal actual fun <T> doRunBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T =
runBlocking(context, block)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.nsk.kstatemachine.statemachine

import kotlinx.coroutines.CoroutineScope
import ru.nsk.kstatemachine.coroutines.CoroutinesLibCoroutineAbstraction
import ru.nsk.kstatemachine.coroutines.createStateMachine
import ru.nsk.kstatemachine.state.ChildMode

/**
* Blocking [createStateMachine] alternative
*/
fun createStateMachineBlocking(
scope: CoroutineScope,
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
init: suspend BuildingStateMachine.() -> Unit
) = with(CoroutinesLibCoroutineAbstraction(scope)) {
runBlocking {
createStateMachine(name, childMode, start, creationArguments, init)
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package ru.nsk.kstatemachine.coroutines

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

internal class CoroutinesLibCoroutineAbstraction(private val scope: CoroutineScope) : CoroutineAbstraction {
/**
* Calls [kotlinx.coroutines.runBlocking]
*/
override fun <R : Any> runBlocking(block: suspend () -> R) =
runBlocking(scope.coroutineContext) { block() }
internal class CoroutinesLibCoroutineAbstraction(internal val scope: CoroutineScope) : CoroutineAbstraction {
override fun <R : Any> runBlocking(block: suspend () -> R): R =
doRunBlocking(scope.coroutineContext) { block() }

/** Switches to context of the [scope] */
override suspend fun <R : Any> withContext(block: suspend () -> R) : R =
override suspend fun <R : Any> withContext(block: suspend () -> R): R =
withContext(scope.coroutineContext) { block() }
}
}

/**
* Calls [kotlinx.coroutines.runBlocking] on supporting platforms, otherwise throws
*/
internal expect fun <T> doRunBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package ru.nsk.kstatemachine.statemachine

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import ru.nsk.kstatemachine.coroutines.CoroutinesLibCoroutineAbstraction
import ru.nsk.kstatemachine.coroutines.createStateMachine
import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.ChildMode

/**
Expand All @@ -27,15 +31,32 @@ suspend fun createStateMachine(
) = CoroutinesLibCoroutineAbstraction(scope)
.createStateMachine(name, childMode, start, creationArguments, init)

fun createStateMachineBlocking(
scope: CoroutineScope,
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
init: suspend BuildingStateMachine.() -> Unit
) = with(CoroutinesLibCoroutineAbstraction(scope)) {
runBlocking {
createStateMachine(name, childMode, start, creationArguments, init)
/**
* Processes event in async fashion (using launch() to start new coroutine).
*
* This API requires [StateMachine]'s [CoroutineScope] so it throws if called on
* machines created by [createStdLibStateMachine].
*/
fun StateMachine.processEventByLaunch(event: Event, argument: Any? = null) {
val coroutineAbstraction = coroutineAbstraction
require(coroutineAbstraction is CoroutinesLibCoroutineAbstraction) {
"${::processEventByLaunch.name} API may be called on ${StateMachine::class.simpleName} " +
"created with coroutines support only"
}
coroutineAbstraction.scope.launch { processEvent(event, argument) }
}

/**
* Processes event in async fashion (using async() to start new coroutine) and returns result as [Deferred].
*
* This API requires [StateMachine]'s [CoroutineScope] so it throws if called on
* machines created by [createStdLibStateMachine].
*/
fun StateMachine.processEventByAsync(event: Event, argument: Any? = null): Deferred<ProcessingResult> {
val coroutineAbstraction = coroutineAbstraction
require(coroutineAbstraction is CoroutinesLibCoroutineAbstraction) {
"${::processEventByAsync.name} API may be called on ${StateMachine::class.simpleName} " +
"created with coroutines support only"
}
return coroutineAbstraction.scope.async { processEvent(event, argument) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ fun StateMachine.stateMachineNotificationFlow(replay: Int = 0): SharedFlow<State
return flow.asSharedFlow()
}

/**
* Provides active states as [Flow]
*/
fun StateMachine.activeStatesFlow() : StateFlow<Set<IState>> {
val flow = MutableStateFlow(activeStates())
onTransitionComplete { _, activeStates -> flow.emit(activeStates) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ru.nsk.kstatemachine.coroutines

import kotlinx.coroutines.CoroutineScope
import ru.nsk.kstatemachine.statemachine.StateMachine
import kotlin.coroutines.CoroutineContext

internal actual fun <T> doRunBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T =
throw UnsupportedOperationException(
"This platform does not support kotlinx.coroutines.runBlocking(). " +
"Do not use blocking API or use ${StateMachine::class.simpleName} without coroutines support"
)
8 changes: 7 additions & 1 deletion kstatemachine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
`java-library`
ru.nsk.`maven-publish`
ru.nsk.jacoco
id("org.jetbrains.dokka") version Versions.kotlinDokka
id("org.jetbrains.dokka")
}

group = rootProject.group
Expand All @@ -22,4 +22,10 @@ kotlin {
iosArm64()
iosX64()
iosSimulatorArm64()
js {
browser()
nodejs()
}
@Suppress("OPT_IN_USAGE") // this is alpha feature
wasmJs()
}
Loading

0 comments on commit b7c29a3

Please sign in to comment.