
me.aartikov.sesame.loop.Loop.kt Maven / Gradle / Ivy
package me.aartikov.sesame.loop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
* A state manager. Stores current state. Receives actions from [dispatch] method or from external [actionSources].
* Sends actions to [Reducer]. [Reducer] generates [Next]-object (new state + side effects). Side effects are handled by [effectHandlers].
*
* [StateT] - type of state that is stored in a state manager. State must be immutable
* [ActionT] - type of action (some external command)
* [EffectT] - type of side effects (such as starting a network request or saving to database)
*/
open class Loop(
initialState: StateT,
private val reducer: Reducer,
private val effectHandlers: List> = emptyList(),
private val actionSources: List> = emptyList(),
private val logger: LoopLogger? = null
) {
/**
* Current state
*/
val state: StateT get() = stateFlow.value
/**
* Flow of state changes
*/
val stateFlow: StateFlow get() = mutableStateFlow
/**
* Returns true if [start] was called and is not canceled yet.
*/
var started: Boolean = false
private set
private val mutableStateFlow = MutableStateFlow(initialState)
private val actionChannel = Channel(Channel.UNLIMITED)
/**
* Starts loop
*/
suspend fun start() {
if (started) {
throw IllegalStateException("Loop is already started")
}
started = true
logger?.logOnStarted(state)
coroutineScope {
try {
startActionSources(this)
for (action in actionChannel) {
logger?.logBeforeReduce(state, action)
val next = reducer.reduce(state, action)
logger?.logAfterReduce(state, action, next)
changeState(next.state)
handleEffects(this, next.effects)
}
} finally {
started = false
}
}
}
/**
* Sends action for processing
*/
fun dispatch(action: ActionT) {
actionChannel.offer(action)
}
private suspend fun startActionSources(scope: CoroutineScope) {
actionSources.forEach { source ->
scope.launch {
source.start { action ->
dispatch(action)
}
}
}
}
private fun changeState(newState: StateT?) {
if (newState != null) {
mutableStateFlow.value = newState
}
}
private suspend fun handleEffects(scope: CoroutineScope, effects: List) {
effects.forEach { effect ->
effectHandlers.forEach { handler ->
scope.launch {
handler.handleEffect(effect) { action ->
dispatch(action)
}
}
}
}
}
}
/**
* A helper method to start [Loop] in a [scope].
*/
fun Loop.startIn(scope: CoroutineScope): Job {
return scope.launch {
start()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy