Skip to content

Commit

Permalink
[#1] Combine stores (#6)
Browse files Browse the repository at this point in the history
* extract store implementation to abstract store

* created store combiner

* updated docs
  • Loading branch information
faogustavo authored Oct 5, 2020
1 parent edcf153 commit 234da18
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 78 deletions.
13 changes: 13 additions & 0 deletions core/src/commonMain/kotlin/dev.valvassori/fluks/Combiner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.valvassori.fluks

import kotlinx.coroutines.ExperimentalCoroutinesApi

@ExperimentalCoroutinesApi
fun <S0 : Fluks.State, S1 : Fluks.State, SOUT : Fluks.State> combiner(
block: (S0, S1) -> SOUT,
): Combiner<S0, S1, SOUT> = Combiner { s0, s1 -> block(s0, s1) }

@ExperimentalCoroutinesApi
fun interface Combiner<S0 : Fluks.State, S1 : Fluks.State, SOUT : Fluks.State> {
fun combine(state0: S0, state1: S1): SOUT
}
49 changes: 2 additions & 47 deletions core/src/commonMain/kotlin/dev.valvassori/fluks/Fluks.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
package dev.valvassori.fluks

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.coroutines.CoroutineContext

@ExperimentalCoroutinesApi
fun <S : Fluks.State> store(
initialValue: S,
reducer: Reducer<S>,
context: CoroutineContext = Dispatchers.Default,
): Fluks.Store<S> = object : Fluks.Store<S>(context) {
override val initialValue: S
get() = initialValue

override fun reduce(
currentState: S,
action: Fluks.Action
): S = reducer.reduce(currentState, action)
}

@ExperimentalCoroutinesApi
object Fluks {
interface Action
Expand All @@ -33,39 +17,10 @@ object Fluks {
abstract val initialValue: S
abstract fun reduce(currentState: S, action: Action): S

internal val reducer: Reducer<S> = Reducer { currentState, action -> reduce(currentState, action) }
internal val state by lazy { MutableStateFlow(initialValue) }
internal val scope = CoroutineScope(baseContext + SupervisorJob())

private val _queue: Channel<Action> by lazy { Channel(Channel.UNLIMITED) }
private var _middlewares: ChainNode<S> = asChainNode()

init {
scope.launch {
register()
for (action in _queue) {
state.value = _middlewares.execute(
store = this@Store,
action = action
)
}
}
}

override fun dispatch(action: Action) {
_queue.offer(action)
}

fun applyMiddleware(middleware: Middleware<S>) {
applyMiddleware(listOf(middleware))
}

fun applyMiddleware(middlewares: List<Middleware<S>>) {
_middlewares = createChain(middlewares)
}

private fun register() {
GlobalDispatcher.register(this)
}
open fun applyMiddleware(middleware: Middleware<S>) {}
open fun applyMiddleware(middlewares: List<Middleware<S>>) {}
}
}
107 changes: 107 additions & 0 deletions core/src/commonMain/kotlin/dev.valvassori/fluks/Store.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package dev.valvassori.fluks

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext

@ExperimentalCoroutinesApi
fun <S : Fluks.State> store(
initialValue: S,
context: CoroutineContext = Dispatchers.Default,
reducer: Reducer<S>,
): Fluks.Store<S> = object : AbstractStore<S>(context) {
override val initialValue: S
get() = initialValue

override fun reduce(
currentState: S,
action: Fluks.Action
): S = reducer.reduce(currentState, action)
}

@ExperimentalCoroutinesApi
abstract class AbstractStore<S : Fluks.State> constructor(
baseContext: CoroutineContext = Dispatchers.Default,
) : Fluks.Store<S>(baseContext) {

private val _queue: Channel<Fluks.Action> by lazy { Channel(Channel.UNLIMITED) }
private var _middlewares: ChainNode<S> = asChainNode()

init {
scope.launch {
register()
for (action in _queue) {
state.value = _middlewares.execute(
store = this@AbstractStore,
action = action
)
}
}
}

final override fun dispatch(action: Fluks.Action) {
_queue.offer(action)
}

final override fun applyMiddleware(middleware: Middleware<S>) {
applyMiddleware(listOf(middleware))
}

final override fun applyMiddleware(middlewares: List<Middleware<S>>) {
_middlewares = createChain(middlewares)
}

private fun register() {
GlobalDispatcher.register(this)
}
}

@ExperimentalCoroutinesApi
fun <S0 : Fluks.State, S1 : Fluks.State, SOUT : Fluks.State> combineStores(
initialValue: SOUT,
store0: Fluks.Store<S0>,
store1: Fluks.Store<S1>,
baseContext: CoroutineContext = Dispatchers.Default,
combiner: Combiner<S0, S1, SOUT>
) = object : AbstractCombinedStore<S0, S1, SOUT>(store0, store1, baseContext) {
override val initialValue: SOUT
get() = initialValue

override fun combine(state0: S0, state1: S1): SOUT = combiner.combine(state0, state1)
}

@ExperimentalCoroutinesApi
abstract class AbstractCombinedStore<S0 : Fluks.State, S1 : Fluks.State, SOUT : Fluks.State> constructor(
store0: Fluks.Store<S0>,
store1: Fluks.Store<S1>,
baseContext: CoroutineContext = Dispatchers.Default,
) : Fluks.Store<SOUT>(baseContext), Combiner<S0, S1, SOUT> {

init {
store0.state
.combine(store1.state) { s0, s1 -> combine(s0, s1) }
.onEach { sout -> state.value = sout }
.launchIn(scope)
}

@Deprecated("Combined stores just react to changes in the other stores")
final override fun reduce(currentState: SOUT, action: Fluks.Action): SOUT = currentState

@Deprecated("Combined stores just react to changes in the other stores")
final override fun dispatch(action: Fluks.Action) {
}

@Deprecated("Combined stores just react to changes in the other stores")
final override fun applyMiddleware(middleware: Middleware<SOUT>) {
}

@Deprecated("Combined stores just react to changes in the other stores")
override fun applyMiddleware(middlewares: List<Middleware<SOUT>>) {
}
}
20 changes: 19 additions & 1 deletion core/src/commonMain/kotlin/dev.valvassori/fluks/ext/+store.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
@file:JvmName("StoreExtKt")
package dev.valvassori.fluks.ext

import dev.valvassori.fluks.Combiner
import dev.valvassori.fluks.Fluks
import dev.valvassori.fluks.combineStores
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmName

@ExperimentalCoroutinesApi
Expand All @@ -12,4 +16,18 @@ val <S : Fluks.State> Fluks.Store<S>.value: S

@ExperimentalCoroutinesApi
val <S : Fluks.State> Fluks.Store<S>.valueFlow: Flow<S>
get() = state
get() = state

@ExperimentalCoroutinesApi
fun <S0 : Fluks.State, S1 : Fluks.State, SOUT : Fluks.State> Fluks.Store<S0>.combineWith(
initialValue: SOUT,
other: Fluks.Store<S1>,
baseContext: CoroutineContext = Dispatchers.Default,
combiner: Combiner<S0, S1, SOUT>,
) = combineStores(
initialValue = initialValue,
store0 = this,
store1 = other,
baseContext = baseContext,
combiner = combiner
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package dev.valvassori.fluks

import dev.valvassori.fluks.ext.value
import dev.valvassori.fluks.util.CoroutineTestRule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals

@ExperimentalCoroutinesApi
class AbstractCombinedStoreTest {

@get:Rule
val coroutineTestRule = CoroutineTestRule()

object Inc : Fluks.Action

private data class State0(val count0: Int) : Fluks.State
private data class State1(val count1: Int) : Fluks.State
private data class StateOut(val multiplication: Int) : Fluks.State

private val store0 by lazy {
store(
initialValue = State0(count0 = 1),
context = Dispatchers.Main
) { currentState, action ->
when (action) {
Inc -> currentState.copy(count0 = currentState.count0 + 1)
else -> currentState
}
}
}

private val store1 by lazy {
store(
initialValue = State1(count1 = 1),
context = Dispatchers.Main
) { currentState, action ->
when (action) {
Inc -> currentState.copy(count1 = currentState.count1 + 1)
else -> currentState
}
}
}

@Test
fun combinedStores_shouldRespondCorrectly() = coroutineTestRule.runBlockingTest {
val combinedStores = combineStores(
initialValue = StateOut(multiplication = 1),
store0 = store0,
store1 = store1,
baseContext = Dispatchers.Main
) { s0, s1 -> StateOut(multiplication = s0.count0 * s1.count1) }

// 1 - 1
assertEquals(1, combinedStores.value.multiplication)

// 2 - 1
store0.dispatch(Inc)
assertEquals(2, combinedStores.value.multiplication)

// 2 - 2
store1.dispatch(Inc)
assertEquals(4, combinedStores.value.multiplication)

// 2 - 3
store1.dispatch(Inc)
assertEquals(6, combinedStores.value.multiplication)

// 3 - 3
store0.dispatch(Inc)
assertEquals(9, combinedStores.value.multiplication)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.junit.Test
import kotlin.test.assertEquals

@ExperimentalCoroutinesApi
internal class FluksTest {
internal class AbstractStoreTest {

@get:Rule
val coroutineTestRule = CoroutineTestRule()
Expand All @@ -27,25 +27,24 @@ internal class FluksTest {
private val store: Fluks.Store<State> by lazy {
store(
initialValue = State(0),
reducer = reducer { state, action ->
when (action) {
is Action.Inc -> state.copy(
count = state.count + 1
)
is Action.Dec -> state.copy(
count = state.count - 1
)
is Action.Mult -> state.copy(
count = state.count * action.multiplier
)
is Action.Div -> state.copy(
count = state.count / action.divider
)
else -> state
}
},
context = Dispatchers.Main
)
) { state, action ->
when (action) {
is Action.Inc -> state.copy(
count = state.count + 1
)
is Action.Dec -> state.copy(
count = state.count - 1
)
is Action.Mult -> state.copy(
count = state.count * action.multiplier
)
is Action.Div -> state.copy(
count = state.count / action.divider
)
else -> state
}
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ class GlobalDispatcherTest {

@Test
fun dispatch_callsAllStores() = coroutineTestRule.runBlockingTest {
val store1 = store(initialValue, reducer, Dispatchers.Main)
val store2 = store(initialValue, reducer, Dispatchers.Main)
val store3 = store(initialValue, reducer, Dispatchers.Main)
val store1 = store(initialValue = initialValue, context = Dispatchers.Main, reducer = reducer)
val store2 = store(initialValue = initialValue, context = Dispatchers.Main, reducer = reducer)
val store3 = store(initialValue = initialValue, context = Dispatchers.Main, reducer = reducer)

dispatch(Switch)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class CoroutineTestRule : TestWatcher(), TestCoroutineScope by TestCoroutineScop
val testDispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher

override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
super.starting(description)
}

override fun finished(description: Description) {
Expand Down
Loading

0 comments on commit 234da18

Please sign in to comment.