diff --git a/README.md b/README.md index de308ea5..8c96b0ae 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ Note, this readme offers a quick overview of the framework. For more in-depth in ```kotlin dependencies { // the core library - implementation("com.1gravity:bloc-core:0.10.0") + implementation("com.1gravity:bloc-core:0.11.0") // add to use the framework together with Redux - implementation("com.1gravity:bloc-redux:0.10.0") + implementation("com.1gravity:bloc-redux:0.11.0") // useful extensions for Android and Jetpack/JetBrains Compose - implementation("com.1gravity:bloc-compose:0.10.0") + implementation("com.1gravity:bloc-compose:0.11.0") } ``` diff --git a/bloc-core/src/androidMain/kotlin/com/onegravity/bloc/BlocContextBuilder.kt b/bloc-core/src/androidMain/kotlin/com/onegravity/bloc/BlocContextBuilder.kt index 4e3bc13c..67eb6328 100644 --- a/bloc-core/src/androidMain/kotlin/com/onegravity/bloc/BlocContextBuilder.kt +++ b/bloc-core/src/androidMain/kotlin/com/onegravity/bloc/BlocContextBuilder.kt @@ -83,11 +83,13 @@ fun ViewModel.blocContext(): BlocContext = * The ViewModel would have to extend some BaseViewModel and we don't want that. */ private fun ViewModel.viewModelLifeCycle(): Lifecycle = object : LifecycleOwner { - override fun getLifecycle() = lifecycleRegistry private val lifecycleRegistry = LifecycleRegistry(this) + override val lifecycle = lifecycleRegistry + init { - viewModelScope.launch(Dispatchers.Main) { + // viewModelScope is tied to Dispatchers.Main.immediate but we want to be explicit here + viewModelScope.launch(Dispatchers.Main.immediate) { lifecycleRegistry.currentState = Lifecycle.State.CREATED // triggers onCreate() lifecycleRegistry.currentState = Lifecycle.State.STARTED // triggers onStart() lifecycleRegistry.currentState = Lifecycle.State.RESUMED // triggers onResume() diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/Bloc.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/Bloc.kt index f9288920..e305bed6 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/Bloc.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/Bloc.kt @@ -40,4 +40,10 @@ public abstract class Bloc : sideEffect: BlocObserver? ) + /** + * Sink.send(Action) + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + abstract override fun send(action: Action) + } diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/BlocImpl.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/BlocImpl.kt index 9d68410c..49f9fe6e 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/BlocImpl.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/BlocImpl.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlin.coroutines.suspendCoroutine private const val QUEUE_INITIAL_SIZE = 10 @@ -73,9 +75,21 @@ internal class BlocImpl Unit = { proposal -> - reduceProcessor.reduce { Effect(proposal, emptyList()) } + /** + * Function that sends the proposal directly to the BlocState and waits for the process to + * finish by suspending execution. + */ + private val reducer: suspend (proposal: Proposal) -> Unit = { proposal -> + suspendCoroutine { continuation -> + reduceProcessor.reduce({ Effect(proposal, emptyList()) }, continuation) + } + } + + /** + * Reduces the given action by suspending the coroutine until the action has been processed. + */ + private suspend fun reduce(action: Action) = suspendCoroutine { continuation -> + reduceProcessor.send(action, continuation) } private val thunkProcessor = ThunkProcessor( @@ -83,7 +97,7 @@ internal class BlocImpl thunkProcessor.send(action) + blocLifecycle.isStarted() -> { + val processed = thunkProcessor.send(action) + if (processed.not()) { + // reducers are meant to run on the main thread -> using runBlocking here is OK + runBlocking { + reduce(action) + } + } + } else -> { /* NOP*/ } } diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/InitializeProcessor.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/InitializeProcessor.kt index 9d4a4681..24b2a693 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/InitializeProcessor.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/InitializeProcessor.kt @@ -18,8 +18,8 @@ internal class InitializeProcessor( private val state: BlocState, dispatcher: CoroutineDispatcher = Dispatchers.Default, private var initializer: Initializer? = null, - private val dispatch: (Action) -> Unit, - private val reduce: (proposal: Proposal) -> Unit + private val dispatch: suspend (Action) -> Unit, + private val reduce: suspend (proposal: Proposal) -> Unit ) { /** @@ -68,7 +68,7 @@ internal class InitializeProcessor( coroutineHelper.launch { if (mutex.tryLock(this@InitializeProcessor)) { val context = InitializerContext( - state = state.value, + getState = { state.value }, dispatch = dispatch, reduce = reduce, launchBlock = coroutineHelper::launch diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReduceProcessor.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReduceProcessor.kt index 7681676b..48fed065 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReduceProcessor.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReduceProcessor.kt @@ -16,6 +16,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.flow.receiveAsFlow +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume /** * The ReduceProcessor is responsible for processing the reduce { }, reduceAnd { } and @@ -73,8 +75,10 @@ internal class ReduceProcessor run a Reducer Redux style */ - internal fun send(action: Action) { + internal fun send(action: Action, continuation: Continuation) { logger.d("received reducer with action ${action.trimOutput()}") - reduceChannel.trySend(ReducerContainer(action)) + reduceChannel.trySend(ReducerContainer(action = action, continuation = continuation)) } /** * BlocExtension interface implementation: * reduce { } -> run a Reducer MVVM+ style */ - internal fun reduce(reduce: ReducerNoAction>) { + internal fun reduce( + reduce: ReducerNoAction>, + continuation: Continuation? = null + ) { logger.d("received reducer without action") - reduceChannel.trySend(ReducerContainer(reducer = reduce)) + reduceChannel.trySend(ReducerContainer(reducer = reduce, continuation = continuation)) } /** * Triggered to execute reducers with a matching Action */ - private fun runReducers(action: Action) { + private fun runReducers(action: Action, continuation: Continuation?) { logger.d("run reducers for action ${action.trimOutput()}") getMatchingReducers(action).fold(false) { proposalEmitted, matcherReducer -> val (_, reducer, expectsProposal) = matcherReducer @@ -122,6 +129,7 @@ internal class ReduceProcessor proposalEmitted } } + continuation?.resume(Unit) } private fun getMatchingReducers(action: Action) = reducers @@ -144,11 +152,15 @@ internal class ReduceProcessor>) { + private fun runReducer( + reduce: ReducerNoAction>, + continuation: Continuation? + ) { val context = ReducerContextNoAction(state.value, coroutineHelper::launch) val (proposal, sideEffects) = context.reduce() proposal?.let(state::send) sideEffects.forEach(sideEffectChannel::trySend) + continuation?.resume(Unit) } } diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReducerContainer.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReducerContainer.kt index 284dc177..db686743 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReducerContainer.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ReducerContainer.kt @@ -2,11 +2,17 @@ package com.onegravity.bloc.internal import com.onegravity.bloc.utils.Effect import com.onegravity.bloc.utils.ReducerNoAction +import kotlin.coroutines.Continuation /** * Wrapper class for reducers that are submitted Redux style (send(Action)) or MVVM+ style (reduce { }) + * + * @param action Action if the reducer was triggered by an Action + * @param reducer Reducer function if the reducer was triggered MVVM+ style + * @param continuation Continuation if the caller is suspending till the reducer is done */ internal data class ReducerContainer( val action: Action? = null, - val reducer: ReducerNoAction>? = null + val reducer: ReducerNoAction>? = null, + val continuation: Continuation? ) diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ThunkProcessor.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ThunkProcessor.kt index 7b108699..29cc5a17 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ThunkProcessor.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/ThunkProcessor.kt @@ -22,8 +22,8 @@ internal class ThunkProcessor( private val state: BlocState, dispatcher: CoroutineDispatcher = Dispatchers.Default, private val thunks: List> = emptyList(), - private val dispatch: (action: Action) -> Unit, - private val reduce: (proposal: Proposal) -> Unit + private val dispatch: suspend (action: Action) -> Unit, + private val reduce: suspend (proposal: Proposal) -> Unit ) { /** @@ -65,13 +65,13 @@ internal class ThunkProcessor( * BlocDSL: * thunk { } -> run a thunk Redux style */ - internal fun send(action: Action) { + internal fun send(action: Action): Boolean { logger.d("received thunk with action ${action.trimOutput()}") if (thunks.any { it.matcher == null || it.matcher.matches(action) }) { thunkChannel.trySend(action) - } else { - dispatch(action) + return true } + return false } /** diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/builder/BlocBuilder.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/builder/BlocBuilder.kt index 675e4ac4..e1208d0c 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/builder/BlocBuilder.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/internal/builder/BlocBuilder.kt @@ -55,7 +55,7 @@ public class BlocBuilder { public fun onCreate(initialize: Initializer) { when (_initialize) { null -> _initialize = initialize - else -> logger.w("Initializer already defined -> ignoring this one") + else -> error("Initializer already defined, there can be only one!") } } diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/state/BlocState.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/state/BlocState.kt index b38672c8..d0017d6d 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/state/BlocState.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/state/BlocState.kt @@ -10,7 +10,8 @@ import kotlinx.coroutines.flow.FlowCollector * * It needs to be a class so generic types aren't erased in Swift. */ -public abstract class BlocState : StateStream, +public abstract class BlocState : + StateStream, Sink { /** diff --git a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/utils/Contexts.kt b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/utils/Contexts.kt index b5698125..d91c6ce2 100644 --- a/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/utils/Contexts.kt +++ b/bloc-core/src/commonMain/kotlin/com/onegravity/bloc/utils/Contexts.kt @@ -8,11 +8,17 @@ package com.onegravity.bloc.utils * // dispatch(action) * } * ``` + * + * @param getState returns the current state + * @param dispatch dispatches an action + * @param reduce reduces a proposal + * @param launchBlock launch a coroutine without exposing the bloc's CoroutineScope, + * it's internal to allow for JobConfig default values via extension functions */ public data class InitializerContext( - val state: State, + val getState: GetState, val dispatch: Dispatcher, - val reduce: (proposal: Proposal) -> Unit, + val reduce: suspend (proposal: Proposal) -> Unit, internal val launchBlock: Launch ) @@ -27,12 +33,19 @@ public data class InitializerContext( * thunk(EnumAction) { * } * ``` + * + * @param getState returns the current state + * @param action the action that triggered the thunk + * @param dispatch dispatches an action + * @param reduce reduces a proposal + * @param launchBlock launch a coroutine without exposing the bloc's CoroutineScope, + * it's internal to allow for JobConfig default values via extension functions */ public data class ThunkContext( val getState: GetState, val action: A, val dispatch: Dispatcher, - val reduce: (proposal: Proposal) -> Unit, + val reduce: suspend (proposal: Proposal) -> Unit, internal val launchBlock: Launch ) @@ -42,11 +55,17 @@ public data class ThunkContext( * fun doSomething() = thunk { * } * ``` + * + * @param getState returns the current state + * @param dispatch dispatches an action + * @param reduce reduces a proposal + * @param launchBlock launch a coroutine without exposing the bloc's CoroutineScope, + * it's internal to allow for JobConfig default values via extension functions */ public data class ThunkContextNoAction( val getState: GetState, val dispatch: Dispatcher, - val reduce: (proposal: Proposal) -> Unit, + val reduce: suspend (proposal: Proposal) -> Unit, internal val launchBlock: Launch ) @@ -61,6 +80,11 @@ public data class ThunkContextNoAction( * reduce(EnumAction) { * } * ``` + * + * @param state the current state + * @param action the action that triggered the reducer + * @param launchBlock launch a coroutine without exposing the bloc's CoroutineScope, + * it's internal to allow for JobConfig default values via extension functions */ public data class ReducerContext( val state: State, @@ -74,6 +98,10 @@ public data class ReducerContext( * fun doSomething() = reduce { * } * ``` + * + * @param state the current state + * @param launchBlock launch a coroutine without exposing the bloc's CoroutineScope, + * it's internal to allow for JobConfig default values via extension functions */ public data class ReducerContextNoAction( val state: State, diff --git a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/Utils.kt b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/Utils.kt index 9589fc39..880ad744 100644 --- a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/Utils.kt +++ b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/Utils.kt @@ -95,9 +95,10 @@ fun collectSideEffects( suspend fun testState( bloc: Bloc, action: Action?, - expected: State + expected: State, + delay: Long = 10 ) { if (action != null) bloc.send(action) - delay(100) + delay(delay) assertEquals(expected, bloc.value) } diff --git a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocInitializerExecutionTests.kt b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocInitializerExecutionTests.kt index 065ae531..b241bd6f 100644 --- a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocInitializerExecutionTests.kt +++ b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocInitializerExecutionTests.kt @@ -16,7 +16,7 @@ import kotlin.test.assertEquals class BlocInitializerExecutionTests : BaseTestClass() { @Test - fun testWithoutInitializer() = runTests { + fun `test without initializer`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -26,43 +26,107 @@ class BlocInitializerExecutionTests : BaseTestClass() { lifecycleRegistry.onCreate() assertEquals(1, bloc.value) - bloc.send(3) - assertEquals(1, bloc.value) + testState(bloc, 3, 1) lifecycleRegistry.onStart() - delay(150) assertEquals(1, bloc.value) + testState(bloc, 3, 4) lifecycleRegistry.onStop() lifecycleRegistry.onDestroy() } /** - * Test regular initializer + * Test regular initializer with dispatch to reducer */ @Test - fun testInitializerExecutionReduxStyle() = runTests { + fun `test initializer with dispatch to reducer`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) val bloc = bloc(context, 1) { reduce { state + 1 } - onCreate { dispatch(Increment) } + onCreate { + dispatch(Increment) + assertEquals(2, getState()) + } reduce { state - 1 } reduce { state + 5 } - onCreate { dispatch(Decrement) } } assertEquals(1, bloc.value) lifecycleRegistry.onCreate() - delay(50) testState(bloc, null, 2) testState(bloc, Increment, 2) lifecycleRegistry.onStart() + testState(bloc, null, 2) + testState(bloc, Increment, 3) + + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + + @Test + fun `test initializer with reduce and dispatch to reducer`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + bloc(context, 1) { + onCreate { + repeat(10) { + val newState = getState() + 3 + reduce(newState) + assertEquals(newState, getState()) + dispatch(3) + assertEquals(newState + 3, getState()) + } + } + reduce { state + action } + } + + lifecycleRegistry.onCreate() delay(50) + lifecycleRegistry.onStart() + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + + /** + * Test regular initializer that triggers a thunk + */ + @Test + fun `test initializer with dispatch to thunk`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + reduce { state + 1 } + onCreate { + // this dispatch is caught by the thunk -> state is updated asynchronously + // (hence with delay in this case) + dispatch(Increment) + assertEquals(1, getState()) + delay(200) + assertEquals(2, getState()) + } + thunk { + delay(50) + dispatch(Increment) + } + reduce { state - 1 } + reduce { state + 5 } + } + + assertEquals(1, bloc.value) + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() + + delay(200) testState(bloc, null, 2) + testState(bloc, Increment, 3, 75) lifecycleRegistry.onStop() lifecycleRegistry.onDestroy() @@ -73,7 +137,7 @@ class BlocInitializerExecutionTests : BaseTestClass() { * which will start processing directly dispatched actions (not by the initializer). */ @Test - fun testInitializerExecutionDelayed1() = runTests { + fun `test initializer with dispatch and delay 1`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -81,6 +145,7 @@ class BlocInitializerExecutionTests : BaseTestClass() { onCreate { delay(1000) dispatch(Increment) + assertEquals(6, getState()) } reduce { state + 1 } reduce { state + 5 } @@ -112,7 +177,7 @@ class BlocInitializerExecutionTests : BaseTestClass() { * which will start processing directly dispatched actions (not by the initializer). */ @Test - fun testInitializerExecutionDelayed2() = runTests { + fun `test initializer with dispatch and delay 2`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -120,6 +185,7 @@ class BlocInitializerExecutionTests : BaseTestClass() { onCreate { delay(1000) dispatch(Increment) + assertEquals(6, getState()) } reduce { state + 1 } reduce { state + 5 } @@ -150,7 +216,7 @@ class BlocInitializerExecutionTests : BaseTestClass() { * Test whether long running initializers still run before everything else */ @Test - fun testInitializerExecutionOrder() = runTests { + fun `test long-running initializer's execution order`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -194,7 +260,7 @@ class BlocInitializerExecutionTests : BaseTestClass() { * now for MVVM+ style reducers and thunks */ @Test - fun testInitializerExecutionOrderMVVM() = runTests { + fun `test long-running initializer's execution order with MVVM+`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -267,36 +333,4 @@ class BlocInitializerExecutionTests : BaseTestClass() { lifecycleRegistry.onDestroy() } - @Test - fun testInitializerReduce() = runTests { - val lifecycleRegistry = LifecycleRegistry() - val context = BlocContextImpl(lifecycleRegistry) - - val bloc = bloc(context, 1) { - onCreate { - reduce(state + 2) - } - reduce { state + action } - } - - // initializer executes and reduces the state - lifecycleRegistry.onCreate() - delay(50) - assertEquals(3, bloc.value) - - // this action however will be ignored - bloc.send(3) - delay(50) - assertEquals(3, bloc.value) - - // only after onStart are "regular" reducers being executed - lifecycleRegistry.onStart() - bloc.send(3) - delay(50) - assertEquals(6, bloc.value) - - lifecycleRegistry.onStop() - lifecycleRegistry.onDestroy() - } - } diff --git a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocLifecycleTests.kt b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocLifecycleTests.kt index 3612ffd6..3b620348 100644 --- a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocLifecycleTests.kt +++ b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocLifecycleTests.kt @@ -19,7 +19,7 @@ class BlocLifecycleTests : BaseTestClass() { * Testing the Essenty lifecycle */ @Test - fun lifecycleTransitions() = runTests { + fun `test Essenty lifecycle transitions`() = runTests { LifecycleRegistry().legalTransition { onCreate() } LifecycleRegistry().illegalTransition { onStart() } LifecycleRegistry().illegalTransition { onResume() } @@ -84,7 +84,7 @@ class BlocLifecycleTests : BaseTestClass() { } @Test - fun reducerLifecycleTest() = runTests { + fun `test bloc lifecycle with reducer`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) val bloc = bloc(context, 1) { @@ -101,14 +101,14 @@ class BlocLifecycleTests : BaseTestClass() { lifecycleRegistry.onStart() testState(bloc, null, 1) assertEquals(1, bloc.value) - testState(bloc, 1, 2) + testState(bloc, 5, 6) lifecycleRegistry.onStop() - testState(bloc, 1, 2) + testState(bloc, 1, 6) bloc.reduce { state + 5 } delay(10) - assertEquals(2, bloc.value) + assertEquals(6, bloc.value) lifecycleRegistry.onStart() lifecycleRegistry.onStop() @@ -116,21 +116,21 @@ class BlocLifecycleTests : BaseTestClass() { lifecycleRegistry.onStop() lifecycleRegistry.onStart() - testState(bloc, null, 2) - testState(bloc, 1, 3) + testState(bloc, null, 6) + testState(bloc, 1, 7) bloc.reduce { state + 5 } delay(10) - assertEquals(8, bloc.value) + assertEquals(12, bloc.value) lifecycleRegistry.onStop() lifecycleRegistry.onDestroy() - testState(bloc, 1, 8) + testState(bloc, 1, 12) } @Test - fun thunkLifecycleTest() = runTests { + fun `test bloc lifecycle with thunk`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) val bloc = bloc(context, 1) { @@ -157,14 +157,14 @@ class BlocLifecycleTests : BaseTestClass() { testState(bloc, 1, 3) bloc.thunk { dispatch(1) } - delay(50) + delay(10) assertEquals(5, bloc.value) lifecycleRegistry.onStop() testState(bloc, 1, 5) bloc.thunk { dispatch(1) } - delay(50) + delay(10) assertEquals(5, bloc.value) lifecycleRegistry.onStart() @@ -178,7 +178,7 @@ class BlocLifecycleTests : BaseTestClass() { @Suppress("RemoveExplicitTypeArguments") @Test - fun initializerLifecycleTest() = runTests { + fun `test bloc lifecycle with initializer`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) val bloc = bloc(context, 1) { @@ -191,7 +191,8 @@ class BlocLifecycleTests : BaseTestClass() { assertEquals(1, bloc.value) lifecycleRegistry.onCreate() - delay(50) + delay(10) + assertEquals(8, bloc.value) testState(bloc, null, 8) lifecycleRegistry.onStart() @@ -219,7 +220,7 @@ class BlocLifecycleTests : BaseTestClass() { @Suppress("RemoveExplicitTypeArguments") @Test - fun initializerLifecycleTestEarlyStart() = runTests { + fun `test bloc lifecycle started early with initializer`() = runTests { val lifecycleRegistry = LifecycleRegistry() lifecycleRegistry.onCreate() lifecycleRegistry.onStart() @@ -230,7 +231,7 @@ class BlocLifecycleTests : BaseTestClass() { reduce { state + action } } - delay(100) + delay(10) assertEquals(8, bloc.value) lifecycleRegistry.onStop() @@ -239,7 +240,7 @@ class BlocLifecycleTests : BaseTestClass() { @Suppress("RemoveExplicitTypeArguments") @Test - fun initializerLifecycleTestEarlyStart2() = runTests { + fun `test bloc lifecycle started and stopped early with initializer`() = runTests { val lifecycleRegistry = LifecycleRegistry() lifecycleRegistry.onCreate() lifecycleRegistry.onStart() @@ -251,12 +252,12 @@ class BlocLifecycleTests : BaseTestClass() { reduce { state + action } } - delay(50) - assertEquals(8, bloc.value) - bloc.send(1) - delay(50) + delay(10) assertEquals(8, bloc.value) + // this won't have an effect because the bloc is already stopped + testState(bloc, 1, 8) + lifecycleRegistry.onDestroy() } diff --git a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocReducerExecutionTests.kt b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocReducerExecutionTests.kt index 786a54f6..c46eb86e 100644 --- a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocReducerExecutionTests.kt +++ b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocReducerExecutionTests.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds class BlocReducerExecutionTests : BaseTestClass() { @Test @@ -100,15 +101,14 @@ class BlocReducerExecutionTests : BaseTestClass() { } @Test - fun testReducerExecutionReducerDelays() = runTest { + fun testReducerExecutionReducerDelays() = runTest(timeout = 20.seconds) { testReducerExecutionWithDelays(0, 0, 0, 1000, 100, 0) testReducerExecutionWithDelays(0, 0, 0, 10, 100, 500) } - @Test - fun testReducerExecutionSendAndReducerDelays() = runTest { - testReducerExecutionWithDelays(123, 1000, 500, 1000, 100, 500) - testReducerExecutionWithDelays(10, 100, 0, 1000, 100, 500) + @Test fun testReducerExecutionSendAndReducerDelays() = runTest(timeout = 20.seconds) { + testReducerExecutionWithDelays(90, 99, 55, 50, 50, 99) + testReducerExecutionWithDelays(10, 100, 0, 300, 100, 250) } @Suppress("LongParameterList") @@ -130,7 +130,7 @@ class BlocReducerExecutionTests : BaseTestClass() { testCollectState( bloc, listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), - delayReducerInc.times(10).plus(100).coerceAtLeast(100) + delayReducerInc.plus(100).coerceAtLeast(100) ) { repeat(10) { bloc.send(Increment) @@ -142,11 +142,10 @@ class BlocReducerExecutionTests : BaseTestClass() { testCollectState( bloc, listOf(11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), - delayReducerDec.times(10).plus(100).coerceAtLeast(100) + delayReducerDec.plus(100).coerceAtLeast(100) ) { repeat(10) { bloc.send(Decrement) - delay(100) delay(delaySendDec) } } @@ -155,7 +154,7 @@ class BlocReducerExecutionTests : BaseTestClass() { testCollectState( bloc, listOf(1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51), - delayReducerWhatever.times(10).plus(100).coerceAtLeast(100) + delayReducerWhatever.plus(100).coerceAtLeast(100) ) { repeat(10) { bloc.send(Whatever) @@ -167,7 +166,7 @@ class BlocReducerExecutionTests : BaseTestClass() { testCollectState( bloc, listOf(51, 52, 57, 56, 57, 62, 61, 62, 67, 66), - (delayReducerInc + delayReducerDec + delayReducerWhatever + 100).times(3) + (delayReducerInc + delayReducerDec + delayReducerWhatever + 100) .coerceAtLeast(100) ) { repeat(3) { diff --git a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocSideEffectTests.kt b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocSideEffectTests.kt index 10493e7b..410448fc 100644 --- a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocSideEffectTests.kt +++ b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocSideEffectTests.kt @@ -10,7 +10,35 @@ import kotlin.test.Test class BlocSideEffectTests : BaseTestClass() { @Test - fun testSideEffects() = runTests { + fun `test side effects - catch all`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + sideEffect { Open } + sideEffect { Close } + sideEffect { Something } + } + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() + + testCollectSideEffects( + bloc, + listOf(Something, Something, Something, Something, Something) + ) { + repeat(5) { + bloc.send(Whatever) + delay(10) + } + } + + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + + @Test + fun `test side effects - action and catch all`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -29,13 +57,30 @@ class BlocSideEffectTests : BaseTestClass() { delay(10) } } + } + + @Test + fun `test side effects - multiple actions and catch all`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + reduce { state + 1 } + sideEffect { Open } + sideEffect { Open } + sideEffect { Close } + sideEffect { Something } + } + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() testCollectSideEffects( bloc, - listOf(Something, Something, Something, Something, Something) + listOf(Open, Open, Something, Open, Open, Something, Open, Open, Something) ) { - repeat(5) { - bloc.send(Whatever) + repeat(3) { + bloc.send(Increment) delay(10) } } @@ -45,7 +90,7 @@ class BlocSideEffectTests : BaseTestClass() { } @Test - fun testMultipleSideEffects() = runTests { + fun `test side effects - reducers and actions and catch all`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -75,12 +120,44 @@ class BlocSideEffectTests : BaseTestClass() { } @Test - fun testThunksAndSideEffects() = runTests { + fun `test side effects - thunks and actions and catch all`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + thunk { dispatch(Decrement) } + sideEffect { Open } + sideEffect { Open } + sideEffect { Close } + sideEffect { Something } + } + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() + + testCollectSideEffects( + bloc, + listOf(Close, Something, Close, Something, Close, Something) + ) { + repeat(3) { + bloc.send(Increment) + delay(10) + } + } + + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + + @Test + fun `test side effects - thunks and reducers and actions and catch all`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) val bloc = bloc(context, 1) { thunk { dispatch(Decrement) } + reduce { state + 1 } + reduce { state - 1 } sideEffect { Open } sideEffect { Open } sideEffect { Close } @@ -90,7 +167,10 @@ class BlocSideEffectTests : BaseTestClass() { lifecycleRegistry.onCreate() lifecycleRegistry.onStart() - testCollectSideEffects(bloc, listOf(Close, Something, Close, Something, Close, Something)) { + testCollectSideEffects( + bloc, + listOf(Close, Something, Close, Something, Close, Something) + ) { repeat(3) { bloc.send(Increment) delay(10) diff --git a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocThunkExecutionTests.kt b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocThunkExecutionTests.kt index 5a802a5c..5f03f67a 100644 --- a/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocThunkExecutionTests.kt +++ b/bloc-core/src/commonTest/kotlin/com/onegravity/bloc/internal/BlocThunkExecutionTests.kt @@ -12,7 +12,7 @@ import kotlin.test.assertEquals class BlocThunkExecutionTests : BaseTestClass() { @Test - fun testThunkExecution1() = runTests { + fun `test thunk execution - lifecycle - order of execution - matching`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -27,14 +27,15 @@ class BlocThunkExecutionTests : BaseTestClass() { assertEquals(1, bloc.value) + // action is sent before the bloc is started -> no effect lifecycleRegistry.onCreate() testState(bloc, Increment, 1) - lifecycleRegistry.onStart() delay(50) - testState(bloc, null, 1) assertEquals(0, count) + testState(bloc, null, 1) + // thunk 2, 3 and 4 are executed testState(bloc, Decrement, 1) assertEquals(9, count) @@ -55,7 +56,7 @@ class BlocThunkExecutionTests : BaseTestClass() { } @Test - fun testThunkExecution2() = runTests { + fun `test thunk execution - with dispatch`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -84,9 +85,9 @@ class BlocThunkExecutionTests : BaseTestClass() { assertEquals(1, bloc.value) + // action is sent before the bloc is started -> no effect lifecycleRegistry.onCreate() testState(bloc, Increment, 1) - lifecycleRegistry.onStart() testState(bloc, null, 1) assertEquals(0, count) @@ -99,7 +100,7 @@ class BlocThunkExecutionTests : BaseTestClass() { } @Test - fun testThunkExecution3() = runTests { + fun `test thunk execution - builder and MVVM+ style`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -142,7 +143,7 @@ class BlocThunkExecutionTests : BaseTestClass() { } @Test - fun testThunkReduceExecution() = runTests { + fun `test thunk execution - getState`() = runTests { val lifecycleRegistry = LifecycleRegistry() val context = BlocContextImpl(lifecycleRegistry) @@ -177,4 +178,102 @@ class BlocThunkExecutionTests : BaseTestClass() { lifecycleRegistry.onDestroy() } + @Test + fun `test thunk execution - with getState and dispatch`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + thunk { + repeat(3) { + val state = getState() + dispatch(Increment) + assertEquals(state + 1, getState()) + dispatch(Decrement) + assertEquals(state, getState()) + reduce(getState() + 1) + assertEquals(state + 1, getState()) + reduce(getState() - 1) + assertEquals(state, getState()) + reduce(getState() + 2) + assertEquals(state + 2, getState()) + } + } + reduce { state + 1 } + reduce { state - 1 } + } + + assertEquals(1, bloc.value) + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() + + testState(bloc, Increment, 7) + testState(bloc, Increment, 13) + testState(bloc, Increment, 19) + + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + + @Test + fun `test thunk execution - with getState and dispatch - MVVM+ style`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + reduce { state + 1 } + reduce { state - 1 } + } + + assertEquals(1, bloc.value) + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() + + bloc.thunk { + val state = getState() + dispatch(Increment) + assertEquals(state + 1, getState()) + dispatch(Decrement) + assertEquals(state, getState()) + } + + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + + @Test + fun `test thunk execution - with getState and dispatch and reduce - MVVM+ style`() = runTests { + val lifecycleRegistry = LifecycleRegistry() + val context = BlocContextImpl(lifecycleRegistry) + + val bloc = bloc(context, 1) { + reduce { state + 1 } + reduce { state - 1 } + } + + assertEquals(1, bloc.value) + + lifecycleRegistry.onCreate() + lifecycleRegistry.onStart() + + bloc.thunk { + val state = getState() + dispatch(Increment) + assertEquals(state + 1, getState()) + dispatch(Decrement) + assertEquals(state, getState()) + reduce(getState() + 1) + assertEquals(state + 1, getState()) + reduce(getState() - 1) + assertEquals(state, getState()) + reduce(getState() + 2) + assertEquals(state + 2, getState()) + } + + lifecycleRegistry.onStop() + lifecycleRegistry.onDestroy() + } + } diff --git a/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/bloc/Posts.kt b/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/bloc/Posts.kt index a7c418c7..28266fe9 100644 --- a/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/bloc/Posts.kt +++ b/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/bloc/Posts.kt @@ -31,7 +31,7 @@ object Posts { context, blocState(PostsState()) ) { - onCreate { if (state.isEmpty()) dispatch(Action.Load) } + onCreate { if (getState().isEmpty()) dispatch(Action.Load) } // we could also put the thunk code into the onCreate block but we want to illustrate the // ability to use initializing code diff --git a/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/compose/PostsComponentImpl.kt b/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/compose/PostsComponentImpl.kt index 519d0616..e9dc819b 100644 --- a/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/compose/PostsComponentImpl.kt +++ b/bloc-samples/src/commonMain/kotlin/com/onegravity/bloc/sample/posts/compose/PostsComponentImpl.kt @@ -41,6 +41,7 @@ class PostsComponentImpl( bloc(context, blocState) { onCreate { // example of "reducing" state from an initializer directly + val state = getState() reduce(state.copy(postsState = state.postsState.copy(loading = true))) // we can access the db here because Dispatchers.Default is a Bloc's default diff --git a/buildSrc/gradle/libs.versions.toml b/buildSrc/gradle/libs.versions.toml index 7cb7f30f..9cdadc8f 100644 --- a/buildSrc/gradle/libs.versions.toml +++ b/buildSrc/gradle/libs.versions.toml @@ -11,7 +11,7 @@ android-gradle-plugin = "7.4.0" compose-gradle-plugin = "1.4.0-alpha01-dev980" complete-kotlin= "1.1.0" -detekt = "1.22.0" +detekt = "1.23.0" taskinfo = "2.1.0" kotlin-dsl = "2.3.3" dokka = "1.8.10" @@ -20,7 +20,7 @@ androidx-activity = "1.6.1" androidx-appcompat = "1.6.1" androidx-constraintlayout = "2.1.4" androidx-core = "1.9.0" -androidx-fragment = "1.5.5" +androidx-fragment = "1.6.0" androidx-lifecycle = "2.6.0" androidx-lifecycle-viewmodel-compose = "2.6.0" androidx-multidex = "2.0.1" @@ -32,7 +32,7 @@ androidx-compose-animation = "1.3.3" androidx-compose-material = "1.3.1" androidx-compose-ui = "1.3.3" -atomicfu = "0.18.4" +atomicfu = "0.20.2" bignum = "0.3.7" google-android-material = "1.8.0" @@ -45,14 +45,14 @@ kermit = "1.2.2" koin = "3.3.3" -kotlin-result = "1.1.16" +kotlin-result = "1.1.18" -kotlinx-coroutines = "1.6.4" +kotlinx-coroutines = "1.7.1" kotlinx-datetime = "0.4.0" ktor = "2.2.4" -essenty = "1.0.0" +essenty = "1.1.0" reaktive = "1.2.3" diff --git a/gradle.properties b/gradle.properties index 62ff0fb6..c7a49f18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,17 @@ org.gradle.parallel=true # Kotlin kotlin.code.style=official +kotlin.caching.enabled=true +kotlin.incremental.useClasspathSnapshot=true +kotlin.incremental.usePreciseJavaTracking=true +kotlin.incremental=true + +# KAPT +# kapt configurations +kapt.incremental=true +kapt.incremental.apt=true +kapt.useBuildCache=true +kapt.workers.isolation="ISOLATED" # Android android.useAndroidX=true @@ -25,7 +36,7 @@ kotlin.internal.mpp.hierarchicalStructureByDefault=true # Maven POM_GROUP=com.1gravity -POM_VERSION_NAME=0.10.0 +POM_VERSION_NAME=0.11.0 # set PUBLISH_AS_SNAPSHOT to false to publish a release version PUBLISH_AS_SNAPSHOT=true diff --git a/todo.txt b/todo.txt index f0cf1deb..1fee4a1e 100644 --- a/todo.txt +++ b/todo.txt @@ -1,8 +1,6 @@ - TODO implement Subscriber tests -- TODO implements Bloc tests with initializer, reducer, thunk, side effect - TODO implements BlocState tests -- TODO implements all the other tests - TODO think about composition of blocs, one for ui state, one for domain logic? @@ -24,3 +22,5 @@ - TODO implement `sideEffects` in the Bloc DSL to allow emission of multiple side effects (`sideEffect` allows only a single side effect) - TODO compiler plugin to have compile-time checks when building Blocs and BlocStates + +- TODO implement lazy initial state \ No newline at end of file diff --git a/website/docs/architecture/bloc/initializer.md b/website/docs/architecture/bloc/initializer.md index 47b3fbae..778b49f9 100644 --- a/website/docs/architecture/bloc/initializer.md +++ b/website/docs/architecture/bloc/initializer.md @@ -7,10 +7,10 @@ hide_title: true ## Definition -Initializers are functions executed when the bloc is created, typically to kick off some initial load. They can execute asynchronous code and dispatch actions to be processed by other thunks and reducers. Initializers are executed exactly once during the [Lifecycle](./lifecycle) of a bloc. +Initializers are functions executed when the bloc is created, typically to kick off some initial load. They can execute asynchronous code and dispatch actions to be processed by thunks and reducers. Initializers are executed exactly once during the [Lifecycle](./lifecycle) of a bloc. :::tip -If more than one initializer is defined, the first one (according to their order of declaration) is used, all others are ignored. +If more than one initializer is defined, building the bloc will fail with a runtime exception. ::: ### Context @@ -20,21 +20,42 @@ An initializer is called with a `InitializerContext` as receiver. The context is ```kotlin public data class InitializerContext( - val state: State, + val getState: GetState, val dispatch: Dispatcher, val reduce: (proposal: Proposal) -> Unit ) ``` + ### reduce() Analogous to thunks, initializers have a `reduce()` function to eliminate boilerplate code: ```kotlin onCreate { - reduce( state.copy(loading = true) ) - + reduce(getState().copy(loading = true)) val books = repository.load() - - reduce( state.copy(loading = false, books = books) ) + reduce(state.copy(loading = false, books = books)) +} +``` + +`reduce()` will suspend till the queued reducer was executed. This is identical to [how reduce() works in thunks](thunk#reduce) and guarantees that this always succeeds: +``` +onCreate { + reduce(newState) // <- suspends till the state reduction is done + assertEquals(getState(), newState) // <- assertion is always true +} +``` + +### dispatch() + +`dispatch()` will suspend till the triggered reducer was executed. This is identical to [how dispatch() works in thunks](thunk#dispatch) and guarantees that this always succeeds: +```kotlin +onCreate { + val state = getState() + dispatch(Increment) // <- suspends till the reducer has run + assertEquals(getState(), state + 1) // <- assertion is always true +} +reduce { + state + 1 } ``` @@ -50,7 +71,7 @@ thunk { } onCreate { - if (state.isEmpty()) dispatch(Load) + if (getState().isEmpty()) dispatch(Load) } ``` @@ -60,7 +81,7 @@ The thunk's asynchronous code could also be in the initializer itself: ```kotlin onCreate { - if (state.isEmpty()) { + if (getState().isEmpty()) { dispatch(Loading) val result = repository.getPosts() dispatch(Loaded(result)) @@ -76,7 +97,7 @@ val bloc = bloc(context, 1) { onCreate { dispatch(2) } - reduce { state + action } + reduce { getState() + action } } // initializer executes -> reduce state @@ -105,19 +126,19 @@ val bloc = bloc(context, 1) { delay(1000) dispatch(2) } - reduce { state + action } + reduce { getState() + action } } lifecycleRegistry.onCreate() -// this action will be ignored +// this action will be ignored (also the initializer is still running) bloc.send(3) delay(50) assertEquals(1, bloc.value) lifecycleRegistry.onStart() -// this action will be queued... +// this action will be queued... (and the initializer is still running) bloc.send(3) delay(50) assertEquals(1, bloc.value) diff --git a/website/docs/architecture/bloc/reducer.md b/website/docs/architecture/bloc/reducer.md index f71e8a7b..851b3569 100644 --- a/website/docs/architecture/bloc/reducer.md +++ b/website/docs/architecture/bloc/reducer.md @@ -15,11 +15,11 @@ While the official Redux reducer definition captures the essence, reducers in th ```kotlin (State, Action) -> Proposal ``` -Compared to a Redux reducer, "our" reducer returns a `Proposal` instead of `State`. +Compared to a Redux reducer, "our" reducer returns a `Proposal` instead of `State` (see [Bloc State](../blocstate/bloc_state.md#sink)). ### Context -A reducer is called with a `ReducerContext` as receiver. The context giving access to the current `State` and the `Action` that triggered the reducer's execution: +A reducer is called with a `ReducerContext` as receiver. The context gives access to the current `State` and the `Action` that triggered the reducer's execution: ```kotlin diff --git a/website/docs/architecture/bloc/thunk.md b/website/docs/architecture/bloc/thunk.md index 14dc41d2..986d6413 100644 --- a/website/docs/architecture/bloc/thunk.md +++ b/website/docs/architecture/bloc/thunk.md @@ -77,18 +77,34 @@ There are extension functions to launch `Coroutines` from a thunk (see [Coroutin Thunks are meant to run asynchronous code and reducers are meant to reduce state. In many cases however the reducers are very simple functions. In the example above we need to add a dedicated action `Loading`, dispatch that action in the thunk in order for a reducer to reduce the current state to one that indicates loading. While that "separation of concerns" is useful in many cases, it adds a good amount of boilerplate code for simple cases like the one we have here. To simplify this we can use the `ThunkContext.reduce` function: ```kotlin thunk { - reduce( getState().copy(loading = true) ) - + reduce(getState().copy(loading = true)) val books = repository.load() - - reduce( getState().copy(loading = false, books = books) ) + reduce(getState().copy(loading = false, books = books)) } ``` -`ThunkContext.reduce()` is identical to submitting an action that triggers a reducer except that the reducer is implicitly defined as: +:::tip +Reducers are executed in the order they are added to the reducer queue (see [Reducer Concurrency](reducer#concurrency)). When `reduce()` is called from a thunk or an initializer, that reducer function is also added to the queue to guarantee the correct order of execution but the reduce call itself will suspend till the queued reducer was executed. This guarantees that this always succeeds: +``` +thunk { + reduce(newState) // <- suspends till the state reduction is done + assertEquals(getState(), newState) // <- assertion is always true +} +``` +::: + +### dispatch() + +When dispatching actions to reducers, the dispatch and the subsequent state reduction will happen in a blocking / suspending manner. Similar to `reduce()`, a call to `dispatch()` will suspend and continue once the reducer is done. This is NOT true if the action was dispatched to another thunk though, only actions dispatched to reducers are processed synchronously: + ```kotlin -reduce { - Effect(proposal, emptyList()) +thunk { + val state = getState() + dispatch(Increment) // <- suspends till the reducer has run + assertEquals(getState(), state + 1) // <- assertion is always true +} +reduce { + state + 1 } ``` diff --git a/website/docs/extensions/android/compose.md b/website/docs/extensions/android/compose.md index d66260ef..cc4c17bf 100644 --- a/website/docs/extensions/android/compose.md +++ b/website/docs/extensions/android/compose.md @@ -10,7 +10,7 @@ hide_title: true To use the [Jetpack Compose](https://developer.android.com/jetpack/compose) extensions please add the `bloc-compose` artifact as a dependency in the Gradle build file: ```kotlin -implementation("com.1gravity:bloc-compose:0.10.0") +implementation("com.1gravity:bloc-compose:0.11.0") ``` ## observeState diff --git a/website/docs/extensions/redux/setup.md b/website/docs/extensions/redux/setup.md index 4d8063dd..9d7b3bc9 100644 --- a/website/docs/extensions/redux/setup.md +++ b/website/docs/extensions/redux/setup.md @@ -10,7 +10,7 @@ hide_title: true To use the [Redux](https://developer.android.com/jetpack/compose) extensions please add the `bloc-redux` artifact as a dependency in the Gradle build file: ```kotlin -implementation("com.1gravity:bloc-redux:0.10.0") +implementation("com.1gravity:bloc-redux:0.11.0") ``` ## Libraries diff --git a/website/docs/getting_started/setup.md b/website/docs/getting_started/setup.md index a3f27917..ec2bc398 100644 --- a/website/docs/getting_started/setup.md +++ b/website/docs/getting_started/setup.md @@ -12,12 +12,12 @@ hide_title: true ```kotlin dependencies { // the core library - implementation("com.1gravity:bloc-core:0.10.0") + implementation("com.1gravity:bloc-core:0.11.0") // add to use the framework together with Redux - implementation("com.1gravity:bloc-redux:0.10.0") + implementation("com.1gravity:bloc-redux:0.11.0") // useful extensions for Android and Jetpack/JetBrains Compose - implementation("com.1gravity:bloc-compose:0.10.0") + implementation("com.1gravity:bloc-compose:0.11.0") } ``` diff --git a/website/package.json b/website/package.json index a9c7f2a7..2326fb6a 100644 --- a/website/package.json +++ b/website/package.json @@ -14,9 +14,9 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "^2.1.0", - "@docusaurus/preset-classic": "^2.1.0", - "@docusaurus/theme-search-algolia": "^2.1.0", + "@docusaurus/core": "^2.4.1", + "@docusaurus/preset-classic": "^2.4.1", + "@docusaurus/theme-search-algolia": "^2.4.1", "@mdx-js/react": "^1.6.22", "clsx": "^1.1.1", "prism-react-renderer": "^1.3.3", @@ -24,7 +24,7 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^2.1.0" + "@docusaurus/module-type-aliases": "^2.4.1" }, "browserslist": { "production": [ diff --git a/website/yarn.lock b/website/yarn.lock index fe2359c2..c0fa14c7 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2052,10 +2052,10 @@ "@docsearch/css" "3.2.1" algoliasearch "^4.0.0" -"@docusaurus/core@2.1.0", "@docusaurus/core@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.1.0.tgz#4aedc306f4c4cd2e0491b641bf78941d4b480ab6" - integrity sha512-/ZJ6xmm+VB9Izbn0/s6h6289cbPy2k4iYFwWDhjiLsVqwa/Y0YBBcXvStfaHccudUC3OfP+26hMk7UCjc50J6Q== +"@docusaurus/core@2.4.1", "@docusaurus/core@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.4.1.tgz#4b8ff5766131ce3fbccaad0b1daf2ad4dc76f62d" + integrity sha512-SNsY7PshK3Ri7vtsLXVeAJGS50nJN3RgF836zkyUfAD01Fq+sAk5EwWgLw+nnm5KVNGDu7PRR2kRGDsWvqpo0g== dependencies: "@babel/core" "^7.18.6" "@babel/generator" "^7.18.7" @@ -2067,13 +2067,13 @@ "@babel/runtime" "^7.18.6" "@babel/runtime-corejs3" "^7.18.6" "@babel/traverse" "^7.18.8" - "@docusaurus/cssnano-preset" "2.1.0" - "@docusaurus/logger" "2.1.0" - "@docusaurus/mdx-loader" "2.1.0" + "@docusaurus/cssnano-preset" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-common" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" "@slorber/static-site-generator-webpack-plugin" "^4.0.7" "@svgr/webpack" "^6.2.1" autoprefixer "^10.4.7" @@ -2094,7 +2094,7 @@ del "^6.1.1" detect-port "^1.3.0" escape-html "^1.0.3" - eta "^1.12.3" + eta "^2.0.0" file-loader "^6.2.0" fs-extra "^10.1.0" html-minifier-terser "^6.1.0" @@ -2129,33 +2129,33 @@ webpack-merge "^5.8.0" webpackbar "^5.0.2" -"@docusaurus/cssnano-preset@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.1.0.tgz#5b42107769b7cbc61655496090bc262d7788d6ab" - integrity sha512-pRLewcgGhOies6pzsUROfmPStDRdFw+FgV5sMtLr5+4Luv2rty5+b/eSIMMetqUsmg3A9r9bcxHk9bKAKvx3zQ== +"@docusaurus/cssnano-preset@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.1.tgz#eacadefb1e2e0f59df3467a0fe83e4ff79eed163" + integrity sha512-ka+vqXwtcW1NbXxWsh6yA1Ckii1klY9E53cJ4O9J09nkMBgrNX3iEFED1fWdv8wf4mJjvGi5RLZ2p9hJNjsLyQ== dependencies: cssnano-preset-advanced "^5.3.8" postcss "^8.4.14" postcss-sort-media-queries "^4.2.1" tslib "^2.4.0" -"@docusaurus/logger@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.1.0.tgz#86c97e948f578814d3e61fc2b2ad283043cbe87a" - integrity sha512-uuJx2T6hDBg82joFeyobywPjSOIfeq05GfyKGHThVoXuXsu1KAzMDYcjoDxarb9CoHCI/Dor8R2MoL6zII8x1Q== +"@docusaurus/logger@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.4.1.tgz#4d2c0626b40752641f9fdd93ad9b5a7a0792f767" + integrity sha512-5h5ysIIWYIDHyTVd8BjheZmQZmEgWDR54aQ1BX9pjFfpyzFo5puKXKYrYJXbjEHGyVhEzmB9UXwbxGfaZhOjcg== dependencies: chalk "^4.1.2" tslib "^2.4.0" -"@docusaurus/mdx-loader@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.1.0.tgz#3fca9576cc73a22f8e7d9941985590b9e47a8526" - integrity sha512-i97hi7hbQjsD3/8OSFhLy7dbKGH8ryjEzOfyhQIn2CFBYOY3ko0vMVEf3IY9nD3Ld7amYzsZ8153RPkcnXA+Lg== +"@docusaurus/mdx-loader@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.4.1.tgz#6425075d7fc136dbfdc121349060cedd64118393" + integrity sha512-4KhUhEavteIAmbBj7LVFnrVYDiU51H5YWW1zY6SmBSte/YLhDutztLTBE0PQl1Grux1jzUJeaSvAzHpTn6JJDQ== dependencies: "@babel/parser" "^7.18.8" "@babel/traverse" "^7.18.8" - "@docusaurus/logger" "2.1.0" - "@docusaurus/utils" "2.1.0" + "@docusaurus/logger" "2.4.1" + "@docusaurus/utils" "2.4.1" "@mdx-js/mdx" "^1.6.22" escape-html "^1.0.3" file-loader "^6.2.0" @@ -2170,13 +2170,13 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@docusaurus/module-type-aliases@2.1.0", "@docusaurus/module-type-aliases@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.1.0.tgz#322f8fd5b436af2154c0dddfa173435730e66261" - integrity sha512-Z8WZaK5cis3xEtyfOT817u9xgGUauT0PuuVo85ysnFRX8n7qLN1lTPCkC+aCmFm/UcV8h/W5T4NtIsst94UntQ== +"@docusaurus/module-type-aliases@2.4.1", "@docusaurus/module-type-aliases@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.1.tgz#38b3c2d2ae44bea6d57506eccd84280216f0171c" + integrity sha512-gLBuIFM8Dp2XOCWffUDSjtxY7jQgKvYujt7Mx5s4FCTfoL5dN1EVbnrn+O2Wvh8b0a77D57qoIDY7ghgmatR1A== dependencies: "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/types" "2.1.0" + "@docusaurus/types" "2.4.1" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -2184,18 +2184,18 @@ react-helmet-async "*" react-loadable "npm:@docusaurus/react-loadable@5.5.2" -"@docusaurus/plugin-content-blog@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.1.0.tgz#32b1a7cd4b0026f4a76fce4edc5cfdd0edb1ec42" - integrity sha512-xEp6jlu92HMNUmyRBEeJ4mCW1s77aAEQO4Keez94cUY/Ap7G/r0Awa6xSLff7HL0Fjg8KK1bEbDy7q9voIavdg== - dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/logger" "2.1.0" - "@docusaurus/mdx-loader" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-common" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" +"@docusaurus/plugin-content-blog@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.1.tgz#c705a8b1a36a34f181dcf43b7770532e4dcdc4a3" + integrity sha512-E2i7Knz5YIbE1XELI6RlTnZnGgS52cUO4BlCiCUCvQHbR+s1xeIWz4C6BtaVnlug0Ccz7nFSksfwDpVlkujg5Q== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" cheerio "^1.0.0-rc.12" feed "^4.2.2" fs-extra "^10.1.0" @@ -2206,18 +2206,18 @@ utility-types "^3.10.0" webpack "^5.73.0" -"@docusaurus/plugin-content-docs@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.1.0.tgz#3fcdf258c13dde27268ce7108a102b74ca4c279b" - integrity sha512-Rup5pqXrXlKGIC4VgwvioIhGWF7E/NNSlxv+JAxRYpik8VKlWsk9ysrdHIlpX+KJUCO9irnY21kQh2814mlp/Q== - dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/logger" "2.1.0" - "@docusaurus/mdx-loader" "2.1.0" - "@docusaurus/module-type-aliases" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" +"@docusaurus/plugin-content-docs@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.1.tgz#ed94d9721b5ce7a956fb01cc06c40d8eee8dfca7" + integrity sha512-Lo7lSIcpswa2Kv4HEeUcGYqaasMUQNpjTXpV0N8G6jXgZaQurqp7E8NGYeGbDXnb48czmHWbzDL4S3+BbK0VzA== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" "@types/react-router-config" "^5.0.6" combine-promises "^1.1.0" fs-extra "^10.1.0" @@ -2228,84 +2228,95 @@ utility-types "^3.10.0" webpack "^5.73.0" -"@docusaurus/plugin-content-pages@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.1.0.tgz#714d24f71d49dbfed888f50c15e975c2154c3ce8" - integrity sha512-SwZdDZRlObHNKXTnFo7W2aF6U5ZqNVI55Nw2GCBryL7oKQSLeI0lsrMlMXdzn+fS7OuBTd3MJBO1T4Zpz0i/+g== - dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/mdx-loader" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" +"@docusaurus/plugin-content-pages@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.1.tgz#c534f7e49967699a45bbe67050d1605ebbf3d285" + integrity sha512-/UjuH/76KLaUlL+o1OvyORynv6FURzjurSjvn2lbWTFc4tpYY2qLYTlKpTCBVPhlLUQsfyFnshEJDLmPneq2oA== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" fs-extra "^10.1.0" tslib "^2.4.0" webpack "^5.73.0" -"@docusaurus/plugin-debug@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.1.0.tgz#b3145affb40e25cf342174638952a5928ddaf7dc" - integrity sha512-8wsDq3OIfiy6440KLlp/qT5uk+WRHQXIXklNHEeZcar+Of0TZxCNe2FBpv+bzb/0qcdP45ia5i5WmR5OjN6DPw== +"@docusaurus/plugin-debug@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.4.1.tgz#461a2c77b0c5a91b2c05257c8f9585412aaa59dc" + integrity sha512-7Yu9UPzRShlrH/G8btOpR0e6INFZr0EegWplMjOqelIwAcx3PKyR8mgPTxGTxcqiYj6hxSCRN0D8R7YrzImwNA== dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils" "2.1.0" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" fs-extra "^10.1.0" react-json-view "^1.21.3" tslib "^2.4.0" -"@docusaurus/plugin-google-analytics@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.1.0.tgz#c9a7269817b38e43484d38fad9996e39aac4196c" - integrity sha512-4cgeqIly/wcFVbbWP03y1QJJBgH8W+Bv6AVbWnsXNOZa1yB3AO6hf3ZdeQH9x20v9T2pREogVgAH0rSoVnNsgg== +"@docusaurus/plugin-google-analytics@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.1.tgz#30de1c35773bf9d52bb2d79b201b23eb98022613" + integrity sha512-dyZJdJiCoL+rcfnm0RPkLt/o732HvLiEwmtoNzOoz9MSZz117UH2J6U2vUDtzUzwtFLIf32KkeyzisbwUCgcaQ== dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" tslib "^2.4.0" -"@docusaurus/plugin-google-gtag@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.1.0.tgz#e4f351dcd98b933538d55bb742650a2a36ca9a32" - integrity sha512-/3aDlv2dMoCeiX2e+DTGvvrdTA+v3cKQV3DbmfsF4ENhvc5nKV23nth04Z3Vq0Ci1ui6Sn80TkhGk/tiCMW2AA== +"@docusaurus/plugin-google-gtag@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.1.tgz#6a3eb91022714735e625c7ca70ef5188fa7bd0dc" + integrity sha512-mKIefK+2kGTQBYvloNEKtDmnRD7bxHLsBcxgnbt4oZwzi2nxCGjPX6+9SQO2KCN5HZbNrYmGo5GJfMgoRvy6uA== dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" tslib "^2.4.0" -"@docusaurus/plugin-sitemap@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.1.0.tgz#b316bb9a42a1717845e26bd4e2d3071748a54b47" - integrity sha512-2Y6Br8drlrZ/jN9MwMBl0aoi9GAjpfyfMBYpaQZXimbK+e9VjYnujXlvQ4SxtM60ASDgtHIAzfVFBkSR/MwRUw== - dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/logger" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-common" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" +"@docusaurus/plugin-google-tag-manager@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.1.tgz#b99f71aec00b112bbf509ef2416e404a95eb607e" + integrity sha512-Zg4Ii9CMOLfpeV2nG74lVTWNtisFaH9QNtEw48R5QE1KIwDBdTVaiSA18G1EujZjrzJJzXN79VhINSbOJO/r3g== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + tslib "^2.4.0" + +"@docusaurus/plugin-sitemap@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.1.tgz#8a7a76ed69dc3e6b4474b6abb10bb03336a9de6d" + integrity sha512-lZx+ijt/+atQ3FVE8FOHV/+X3kuok688OydDXrqKRJyXBJZKgGjA2Qa8RjQ4f27V2woaXhtnyrdPop/+OjVMRg== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" fs-extra "^10.1.0" sitemap "^7.1.1" tslib "^2.4.0" -"@docusaurus/preset-classic@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.1.0.tgz#45b23c8ec10c96ded9ece128fac3a39b10bcbc56" - integrity sha512-NQMnaq974K4BcSMXFSJBQ5itniw6RSyW+VT+6i90kGZzTwiuKZmsp0r9lC6BYAvvVMQUNJQwrETmlu7y2XKW7w== - dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/plugin-content-blog" "2.1.0" - "@docusaurus/plugin-content-docs" "2.1.0" - "@docusaurus/plugin-content-pages" "2.1.0" - "@docusaurus/plugin-debug" "2.1.0" - "@docusaurus/plugin-google-analytics" "2.1.0" - "@docusaurus/plugin-google-gtag" "2.1.0" - "@docusaurus/plugin-sitemap" "2.1.0" - "@docusaurus/theme-classic" "2.1.0" - "@docusaurus/theme-common" "2.1.0" - "@docusaurus/theme-search-algolia" "2.1.0" - "@docusaurus/types" "2.1.0" +"@docusaurus/preset-classic@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.4.1.tgz#072f22d0332588e9c5f512d4bded8d7c99f91497" + integrity sha512-P4//+I4zDqQJ+UDgoFrjIFaQ1MeS9UD1cvxVQaI6O7iBmiHQm0MGROP1TbE7HlxlDPXFJjZUK3x3cAoK63smGQ== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/plugin-content-blog" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/plugin-content-pages" "2.4.1" + "@docusaurus/plugin-debug" "2.4.1" + "@docusaurus/plugin-google-analytics" "2.4.1" + "@docusaurus/plugin-google-gtag" "2.4.1" + "@docusaurus/plugin-google-tag-manager" "2.4.1" + "@docusaurus/plugin-sitemap" "2.4.1" + "@docusaurus/theme-classic" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/theme-search-algolia" "2.4.1" + "@docusaurus/types" "2.4.1" "@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": version "5.5.2" @@ -2315,27 +2326,27 @@ "@types/react" "*" prop-types "^15.6.2" -"@docusaurus/theme-classic@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.1.0.tgz#d957a907ea8dd035c1cf911d0fbe91d8f24aef3f" - integrity sha512-xn8ZfNMsf7gaSy9+ClFnUu71o7oKgMo5noYSS1hy3svNifRTkrBp6+MReLDsmIaj3mLf2e7+JCBYKBFbaGzQng== - dependencies: - "@docusaurus/core" "2.1.0" - "@docusaurus/mdx-loader" "2.1.0" - "@docusaurus/module-type-aliases" "2.1.0" - "@docusaurus/plugin-content-blog" "2.1.0" - "@docusaurus/plugin-content-docs" "2.1.0" - "@docusaurus/plugin-content-pages" "2.1.0" - "@docusaurus/theme-common" "2.1.0" - "@docusaurus/theme-translations" "2.1.0" - "@docusaurus/types" "2.1.0" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-common" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" +"@docusaurus/theme-classic@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.4.1.tgz#0060cb263c1a73a33ac33f79bb6bc2a12a56ad9e" + integrity sha512-Rz0wKUa+LTW1PLXmwnf8mn85EBzaGSt6qamqtmnh9Hflkc+EqiYMhtUJeLdV+wsgYq4aG0ANc+bpUDpsUhdnwg== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/plugin-content-blog" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/plugin-content-pages" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/theme-translations" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" "@mdx-js/react" "^1.6.22" clsx "^1.2.1" copy-text-to-clipboard "^3.0.1" - infima "0.2.0-alpha.42" + infima "0.2.0-alpha.43" lodash "^4.17.21" nprogress "^0.2.0" postcss "^8.4.14" @@ -2346,17 +2357,18 @@ tslib "^2.4.0" utility-types "^3.10.0" -"@docusaurus/theme-common@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.1.0.tgz#dff4d5d1e29efc06125dc06f7b259f689bb3f24d" - integrity sha512-vT1otpVPbKux90YpZUnvknsn5zvpLf+AW1W0EDcpE9up4cDrPqfsh0QoxGHFJnobE2/qftsBFC19BneN4BH8Ag== - dependencies: - "@docusaurus/mdx-loader" "2.1.0" - "@docusaurus/module-type-aliases" "2.1.0" - "@docusaurus/plugin-content-blog" "2.1.0" - "@docusaurus/plugin-content-docs" "2.1.0" - "@docusaurus/plugin-content-pages" "2.1.0" - "@docusaurus/utils" "2.1.0" +"@docusaurus/theme-common@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.4.1.tgz#03e16f7aa96455e952f3243ac99757b01a3c83d4" + integrity sha512-G7Zau1W5rQTaFFB3x3soQoZpkgMbl/SYNG8PfMFIjKa3M3q8n0m/GRf5/H/e5BqOvt8c+ZWIXGCiz+kUCSHovA== + dependencies: + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/plugin-content-blog" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/plugin-content-pages" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -2364,42 +2376,43 @@ parse-numeric-range "^1.3.0" prism-react-renderer "^1.3.5" tslib "^2.4.0" + use-sync-external-store "^1.2.0" utility-types "^3.10.0" -"@docusaurus/theme-search-algolia@2.1.0", "@docusaurus/theme-search-algolia@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.1.0.tgz#e7cdf64b6f7a15b07c6dcf652fd308cfdaabb0ee" - integrity sha512-rNBvi35VvENhucslEeVPOtbAzBdZY/9j55gdsweGV5bYoAXy4mHB6zTGjealcB4pJ6lJY4a5g75fXXMOlUqPfg== +"@docusaurus/theme-search-algolia@2.4.1", "@docusaurus/theme-search-algolia@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.1.tgz#906bd2cca3fced0241985ef502c892f58ff380fc" + integrity sha512-6BcqW2lnLhZCXuMAvPRezFs1DpmEKzXFKlYjruuas+Xy3AQeFzDJKTJFIm49N77WFCTyxff8d3E4Q9pi/+5McQ== dependencies: "@docsearch/react" "^3.1.1" - "@docusaurus/core" "2.1.0" - "@docusaurus/logger" "2.1.0" - "@docusaurus/plugin-content-docs" "2.1.0" - "@docusaurus/theme-common" "2.1.0" - "@docusaurus/theme-translations" "2.1.0" - "@docusaurus/utils" "2.1.0" - "@docusaurus/utils-validation" "2.1.0" + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/theme-translations" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" algoliasearch "^4.13.1" algoliasearch-helper "^3.10.0" clsx "^1.2.1" - eta "^1.12.3" + eta "^2.0.0" fs-extra "^10.1.0" lodash "^4.17.21" tslib "^2.4.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.1.0.tgz#ce9a2955afd49bff364cfdfd4492b226f6dd3b6e" - integrity sha512-07n2akf2nqWvtJeMy3A+7oSGMuu5F673AovXVwY0aGAux1afzGCiqIFlYW3EP0CujvDJAEFSQi/Tetfh+95JNg== +"@docusaurus/theme-translations@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.4.1.tgz#4d49df5865dae9ef4b98a19284ede62ae6f98726" + integrity sha512-T1RAGP+f86CA1kfE8ejZ3T3pUU3XcyvrGMfC/zxCtc2BsnoexuNI9Vk2CmuKCb+Tacvhxjv5unhxXce0+NKyvA== dependencies: fs-extra "^10.1.0" tslib "^2.4.0" -"@docusaurus/types@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.1.0.tgz#01e13cd9adb268fffe87b49eb90302d5dc3edd6b" - integrity sha512-BS1ebpJZnGG6esKqsjtEC9U9qSaPylPwlO7cQ1GaIE7J/kMZI3FITnNn0otXXu7c7ZTqhb6+8dOrG6fZn6fqzQ== +"@docusaurus/types@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.4.1.tgz#d8e82f9e0f704984f98df1f93d6b4554d5458705" + integrity sha512-0R+cbhpMkhbRXX138UOc/2XZFF8hiZa6ooZAEEJFp5scytzCw4tC1gChMFXrpa3d2tYE6AX8IrOEpSonLmfQuQ== dependencies: "@types/history" "^4.7.11" "@types/react" "*" @@ -2410,31 +2423,32 @@ webpack "^5.73.0" webpack-merge "^5.8.0" -"@docusaurus/utils-common@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.1.0.tgz#248434751096f8c6c644ed65eed2a5a070a227f8" - integrity sha512-F2vgmt4yRFgRQR2vyEFGTWeyAdmgKbtmu3sjHObF0tjjx/pN0Iw/c6eCopaH34E6tc9nO0nvp01pwW+/86d1fg== +"@docusaurus/utils-common@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.4.1.tgz#7f72e873e49bd5179588869cc3ab7449a56aae63" + integrity sha512-bCVGdZU+z/qVcIiEQdyx0K13OC5mYwxhSuDUR95oFbKVuXYRrTVrwZIqQljuo1fyJvFTKHiL9L9skQOPokuFNQ== dependencies: tslib "^2.4.0" -"@docusaurus/utils-validation@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.1.0.tgz#c8cf1d8454d924d9a564fefa86436268f43308e3" - integrity sha512-AMJzWYKL3b7FLltKtDXNLO9Y649V2BXvrnRdnW2AA+PpBnYV78zKLSCz135cuWwRj1ajNtP4onbXdlnyvCijGQ== +"@docusaurus/utils-validation@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.4.1.tgz#19959856d4a886af0c5cfb357f4ef68b51151244" + integrity sha512-unII3hlJlDwZ3w8U+pMO3Lx3RhI4YEbY3YNsQj4yzrkZzlpqZOLuAiZK2JyULnD+TKbceKU0WyWkQXtYbLNDFA== dependencies: - "@docusaurus/logger" "2.1.0" - "@docusaurus/utils" "2.1.0" + "@docusaurus/logger" "2.4.1" + "@docusaurus/utils" "2.4.1" joi "^17.6.0" js-yaml "^4.1.0" tslib "^2.4.0" -"@docusaurus/utils@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.1.0.tgz#b77b45b22e61eb6c2dcad8a7e96f6db0409b655f" - integrity sha512-fPvrfmAuC54n8MjZuG4IysaMdmvN5A/qr7iFLbSGSyDrsbP4fnui6KdZZIa/YOLIPLec8vjZ8RIITJqF18mx4A== +"@docusaurus/utils@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.4.1.tgz#9c5f76eae37b71f3819c1c1f0e26e6807c99a4fc" + integrity sha512-1lvEZdAQhKNht9aPXPoh69eeKnV0/62ROhQeFKKxmzd0zkcuE/Oc5Gpnt00y/f5bIsmOsYMY7Pqfm/5rteT5GA== dependencies: - "@docusaurus/logger" "2.1.0" + "@docusaurus/logger" "2.4.1" "@svgr/webpack" "^6.2.1" + escape-string-regexp "^4.0.0" file-loader "^6.2.0" fs-extra "^10.1.0" github-slugger "^1.4.0" @@ -4566,10 +4580,10 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eta@^1.12.3: - version "1.12.3" - resolved "https://registry.yarnpkg.com/eta/-/eta-1.12.3.tgz#2982d08adfbef39f9fa50e2fbd42d7337e7338b1" - integrity sha512-qHixwbDLtekO/d51Yr4glcaUJCIjGVJyTzuqV4GPlgZo1YpgOKG+avQynErZIYrfM6JIJdtiG2Kox8tbb+DoGg== +eta@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eta/-/eta-2.2.0.tgz#eb8b5f8c4e8b6306561a455e62cd7492fe3a9b8a" + integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== etag@~1.8.1: version "1.8.1" @@ -5360,10 +5374,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -infima@0.2.0-alpha.42: - version "0.2.0-alpha.42" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.42.tgz#f6e86a655ad40877c6b4d11b2ede681eb5470aa5" - integrity sha512-ift8OXNbQQwtbIt6z16KnSWP7uJ/SysSMFI4F87MNRTicypfl4Pv3E2OGVv6N3nSZFJvA8imYulCBS64iyHYww== +infima@0.2.0-alpha.43: + version "0.2.0-alpha.43" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" + integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== inflight@^1.0.4: version "1.0.6" @@ -8229,6 +8243,11 @@ use-latest@^1.2.1: dependencies: use-isomorphic-layout-effect "^1.1.1" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"