diff --git a/formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt b/formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt index cfa0704d..c880738b 100644 --- a/formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt +++ b/formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt @@ -279,6 +279,10 @@ internal class FormulaManagerImpl( * @return True if we need to re-evaluate. */ private fun postEvaluation(evaluationId: Long): Boolean { + if (isEvaluationNeeded(evaluationId)) { + return true + } + if (handleTransitionQueue(evaluationId)) { return true } diff --git a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt index aaf090a2..2252e52c 100644 --- a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt +++ b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt @@ -26,11 +26,13 @@ import com.instacart.formula.subjects.EventFormula import com.instacart.formula.subjects.ExtremelyNestedFormula import com.instacart.formula.subjects.FromObservableWithInputFormula import com.instacart.formula.subjects.HasChildFormula +import com.instacart.formula.subjects.MultiChildIndirectStateChangeRobot import com.instacart.formula.subjects.InputChangeWhileFormulaRunningRobot import com.instacart.formula.subjects.KeyUsingListFormula import com.instacart.formula.subjects.MessageFormula import com.instacart.formula.subjects.MixingCallbackUseWithKeyUse import com.instacart.formula.subjects.MultipleChildEvents +import com.instacart.formula.subjects.NestedCallbackCallRobot import com.instacart.formula.subjects.NestedChildTransitionAfterNoEvaluationPass import com.instacart.formula.subjects.NestedKeyFormula import com.instacart.formula.subjects.NestedTerminationWithInputChanged @@ -41,6 +43,7 @@ import com.instacart.formula.subjects.OptionalChildFormula import com.instacart.formula.subjects.OptionalEventCallbackFormula import com.instacart.formula.subjects.ParallelChildFormulaFiresEventOnStart import com.instacart.formula.subjects.ParentTransitionOnChildActionStart +import com.instacart.formula.subjects.ParentUpdateChildAndSelfOnEventRobot import com.instacart.formula.subjects.PendingActionFormulaTerminatedOnActionInit import com.instacart.formula.subjects.RemovingTerminateStreamSendsNoMessagesFormula import com.instacart.formula.subjects.RootFormulaKeyTestSubject @@ -673,6 +676,35 @@ class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) { .assertCurrentValue(2) } + @Test fun `parent updates a child and self in a single action`() { + val robot = ParentUpdateChildAndSelfOnEventRobot(runtime) + robot.start() + robot.subject.output { onAction() } + robot.subject.output { + assertThat(childValue).isEqualTo(1) + assertThat(parentValue).isEqualTo(3) + } + } + + @Test + fun `formula calls an event listener from a transition`() { + val robot = NestedCallbackCallRobot(runtime) + robot.start() + robot.subject.output { onAction() } + robot.subject.output { assertThat(value).isEqualTo(1) } + } + + @Test + fun `formula calls own event listener which starts multiple transitions`() { + val robot = MultiChildIndirectStateChangeRobot(runtime) + robot.start() + robot.subject.output { onAction() } + robot.subject.output { + assertThat(childValue).isEqualTo(2) + assertThat(parentValue).isEqualTo(3) + } + } + @Test fun `action runAgain`() { val inspector = CountingInspector() diff --git a/formula/src/test/java/com/instacart/formula/subjects/MultiChildIndirectStateChangeRobot.kt b/formula/src/test/java/com/instacart/formula/subjects/MultiChildIndirectStateChangeRobot.kt new file mode 100644 index 00000000..b35c9cc6 --- /dev/null +++ b/formula/src/test/java/com/instacart/formula/subjects/MultiChildIndirectStateChangeRobot.kt @@ -0,0 +1,120 @@ +package com.instacart.formula.subjects + +import com.instacart.formula.Evaluation +import com.instacart.formula.Formula +import com.instacart.formula.Snapshot +import com.instacart.formula.rxjava3.RxAction +import com.instacart.formula.test.TestableRuntime +import io.reactivex.rxjava3.core.Observable + +class MultiChildIndirectStateChangeRobot(runtime: TestableRuntime) { + val subject = runtime.test(Parent()) + + fun start() = apply { + subject.input(Unit) + } + + class Child : Formula() { + data class Input( + val preAction: () -> Unit = {}, + ) + + data class State( + val actionId: Int = 0, + val value: Int = 0, + ) + + data class Output( + val value: Int, + val onChildAction: () -> Unit, + ) + + override fun initialState(input: Input): State = State() + + override fun Snapshot.evaluate(): Evaluation { + return Evaluation( + output = Output( + value = state.value, + onChildAction = context.callback { + val newState = state.copy(actionId = state.actionId + 1) + transition(newState) + } + ), + actions = context.actions { + if (state.actionId > 0) { + RxAction.fromObservable(state.actionId) { + input.preAction() + + // Increment two times + Observable.just(1, 1) + }.onEvent { + val newState = state.copy(value = state.value + it) + transition(newState) + } + } + } + ) + } + } + + class Parent : Formula() { + data class State( + val actionId: Int = 0, + val value: Int = 0, + ) + + data class Output( + val parentValue: Int, + val childValue: Int, + val onAction: () -> Unit + ) + + private val incrementFormula = Child() + + override fun initialState(input: Unit): State = State() + + override fun Snapshot.evaluate(): Evaluation { + val next = context.callback { + val newState = state.copy(actionId = state.actionId + 1) + transition(newState) + } + + val firstChild = context.key("first") { + context.child(incrementFormula, Child.Input()) + } + + val secondChild = context.key("second") { + context.child( + formula = incrementFormula, + input = Child.Input(firstChild.onChildAction), + ) + } + + return Evaluation( + output = Output( + parentValue = state.value, + childValue = secondChild.value, + onAction = context.callback { + transition { + next() + } + } + ), + actions = context.actions { + if (state.actionId > 0) { + RxAction.fromObservable(state.actionId) { + // Call child + secondChild.onChildAction() + + // Emit events + Observable.just(1, 1, 1) + }.onEvent { + val newState = state.copy(value = state.value + it) + transition(newState) + } + } + } + ) + } + } +} \ No newline at end of file diff --git a/formula/src/test/java/com/instacart/formula/subjects/NestedCallbackCallRobot.kt b/formula/src/test/java/com/instacart/formula/subjects/NestedCallbackCallRobot.kt new file mode 100644 index 00000000..27eab428 --- /dev/null +++ b/formula/src/test/java/com/instacart/formula/subjects/NestedCallbackCallRobot.kt @@ -0,0 +1,39 @@ +package com.instacart.formula.subjects + +import com.instacart.formula.Evaluation +import com.instacart.formula.Formula +import com.instacart.formula.Snapshot +import com.instacart.formula.test.TestableRuntime + +class NestedCallbackCallRobot(runtime: TestableRuntime) { + val subject = runtime.test(Parent()) + + fun start() = apply { + subject.input(Unit) + } + + class Parent : Formula() { + data class Output( + val value: Int, + val onAction: () -> Unit, + ) + + override fun initialState(input: Unit): Int = 0 + + override fun Snapshot.evaluate(): Evaluation { + val next = context.callback { + transition(state + 1) + } + return Evaluation( + output = Output( + value = state, + onAction = context.callback { + transition { + next() + } + } + ) + ) + } + } +} \ No newline at end of file diff --git a/formula/src/test/java/com/instacart/formula/subjects/ParentUpdateChildAndSelfOnEventRobot.kt b/formula/src/test/java/com/instacart/formula/subjects/ParentUpdateChildAndSelfOnEventRobot.kt new file mode 100644 index 00000000..5853cb7b --- /dev/null +++ b/formula/src/test/java/com/instacart/formula/subjects/ParentUpdateChildAndSelfOnEventRobot.kt @@ -0,0 +1,65 @@ +package com.instacart.formula.subjects + +import com.instacart.formula.Evaluation +import com.instacart.formula.Formula +import com.instacart.formula.Snapshot +import com.instacart.formula.rxjava3.RxAction +import com.instacart.formula.test.TestableRuntime +import com.instacart.formula.types.IncrementFormula +import io.reactivex.rxjava3.core.Observable + +class ParentUpdateChildAndSelfOnEventRobot( + runtime: TestableRuntime, +) { + + val subject = runtime.test(Parent()) + + fun start() = apply { + subject.input(Unit) + } + + class Parent : Formula() { + data class State( + val actionId: Int = 0, + val value: Int = 0, + ) + + data class Output( + val parentValue: Int, + val childValue: Int, + val onAction: () -> Unit + ) + + private val incrementFormula = IncrementFormula() + + override fun initialState(input: Unit): State = State() + + override fun Snapshot.evaluate(): Evaluation { + val incrementOutput = context.child(incrementFormula) + return Evaluation( + output = Output( + parentValue = state.value, + childValue = incrementOutput.value, + onAction = context.callback { + val newState = state.copy(actionId = state.actionId + 1) + transition(newState) + } + ), + actions = context.actions { + if (state.actionId > 0) { + RxAction.fromObservable(state.actionId) { + // Call child + incrementOutput.onIncrement() + + // Emit events + Observable.just(1, 1, 1) + }.onEvent { + val newState = state.copy(value = state.value + it) + transition(newState) + } + } + } + ) + } + } +} \ No newline at end of file