diff --git a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt
index 09101a17..ca5c6a91 100644
--- a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt
+++ b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntime.kt
@@ -10,6 +10,13 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.FormulaDisposableHelper
object RxJavaRuntime {
+
+ private var defaultErrorHandler: RxJavaRuntimeErrorHandler? = null
+
+ fun setDefaultErrorHandler(errorHandler: RxJavaRuntimeErrorHandler?) {
+ this.defaultErrorHandler = errorHandler
+ }
+
fun start(
input: Observable,
formula: IFormula,
@@ -22,12 +29,18 @@ object RxJavaRuntime {
type = formula.type(),
local = inspector,
)
+
+ val onError = { error: Throwable ->
+ val handled = defaultErrorHandler?.onError(error) ?: false
+ if (!handled) emitter.onError(error)
+ }.takeIf { defaultErrorHandler != null } ?: emitter::onError
+
val runtimeFactory = {
FormulaRuntime(
threadChecker = threadChecker,
formula = formula,
onOutput = emitter::onNext,
- onError = emitter::onError,
+ onError = onError,
inspector = mergedInspector,
isValidationEnabled = isValidationEnabled,
)
@@ -45,7 +58,7 @@ object RxJavaRuntime {
runtime = runtimeFactory()
}
runtime.onInput(input)
- }, emitter::onError))
+ }, onError))
val runnable = Runnable {
threadChecker.check("Need to unsubscribe on the main thread.")
@@ -56,4 +69,4 @@ object RxJavaRuntime {
emitter.setDisposable(disposables)
}.distinctUntilChanged()
}
-}
\ No newline at end of file
+}
diff --git a/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt
new file mode 100644
index 00000000..aa5fb1ed
--- /dev/null
+++ b/formula-rxjava3/src/main/java/com/instacart/formula/rxjava3/RxJavaRuntimeErrorHandler.kt
@@ -0,0 +1,10 @@
+package com.instacart.formula.rxjava3
+
+interface RxJavaRuntimeErrorHandler {
+ /**
+ * @param error [Throwable] that occurred
+ *
+ * @return true if error was handled, false otherwise
+ */
+ fun onError(error: Throwable): Boolean
+}
diff --git a/formula/src/main/java/com/instacart/formula/Exceptions.kt b/formula/src/main/java/com/instacart/formula/Exceptions.kt
new file mode 100644
index 00000000..347224ee
--- /dev/null
+++ b/formula/src/main/java/com/instacart/formula/Exceptions.kt
@@ -0,0 +1,6 @@
+package com.instacart.formula
+
+/**
+ * Thrown when a child formula is added with a duplicate key.
+ */
+class DuplicateKeyException(override val message: String?) : IllegalStateException()
diff --git a/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt b/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt
index 14a336fc..3fc1ea22 100644
--- a/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt
+++ b/formula/src/main/java/com/instacart/formula/internal/SingleRequestHolder.kt
@@ -1,5 +1,7 @@
package com.instacart.formula.internal
+import com.instacart.formula.DuplicateKeyException
+
/**
* Holder tracks when object has already been request and throws an error if requested again. This
* is used to track duplicate requests for a particular key.
@@ -9,7 +11,7 @@ internal class SingleRequestHolder(val value: T) {
inline fun requestAccess(errorMessage: () -> String): T {
if (requested) {
- throw IllegalStateException(errorMessage())
+ throw DuplicateKeyException(errorMessage())
}
requested = true
diff --git a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt
index d8f52823..198a9b6f 100644
--- a/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt
+++ b/formula/src/test/java/com/instacart/formula/FormulaRuntimeTest.kt
@@ -7,6 +7,7 @@ import com.instacart.formula.internal.ClearPluginsRule
import com.instacart.formula.internal.TestInspector
import com.instacart.formula.internal.Try
import com.instacart.formula.rxjava3.RxAction
+import com.instacart.formula.rxjava3.RxJavaRuntime
import com.instacart.formula.subjects.ChildActionFiresParentEventOnStart
import com.instacart.formula.subjects.ChildMessageNoParentStateChange
import com.instacart.formula.subjects.ChildMessageTriggersEventTransitionInParent
@@ -1108,7 +1109,9 @@ class FormulaRuntimeTest(val runtime: TestableRuntime, val name: String) {
}
val error = result.errorOrNull()?.cause
- assertThat(error).isInstanceOf(IllegalStateException::class.java)
+ assertThat(error).isInstanceOf(DuplicateKeyException::class.java)
+ val expectedMessage = "There already is a child with same key: FormulaKey(scopeKey=null, type=class com.instacart.formula.subjects.KeyFormula, key=TestKey(id=1)). Override [Formula.key] function."
+ assertThat(error?.message).isEqualTo(expectedMessage)
}
@Test
diff --git a/formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt b/formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt
new file mode 100644
index 00000000..17ef2681
--- /dev/null
+++ b/formula/src/test/java/com/instacart/formula/error/RxJavaRuntimeErrorHandlerTest.kt
@@ -0,0 +1,95 @@
+package com.instacart.formula.error
+
+import com.google.common.truth.Truth.assertThat
+import com.instacart.formula.Action
+import com.instacart.formula.DuplicateKeyException
+import com.instacart.formula.internal.ClearPluginsRule
+import com.instacart.formula.internal.Try
+import com.instacart.formula.rxjava3.RxJavaRuntime
+import com.instacart.formula.rxjava3.RxJavaRuntimeErrorHandler
+import com.instacart.formula.subjects.DynamicParentFormula
+import com.instacart.formula.subjects.OnlyUpdateFormula
+import com.instacart.formula.subjects.TestKey
+import com.instacart.formula.test.RxJavaTestableRuntime
+import com.instacart.formula.test.TestableRuntime
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TestName
+import kotlin.IllegalStateException
+
+class RxJavaRuntimeErrorHandlerTest {
+
+ val runtime: TestableRuntime = RxJavaTestableRuntime
+
+ @get:Rule
+ val rule = RuleChain
+ .outerRule(TestName())
+ .around(ClearPluginsRule())
+ .around(runtime.rule)
+
+ private val errorLogs = mutableListOf()
+
+ private val duplicateKeyErrorHandler = object : RxJavaRuntimeErrorHandler {
+ override fun onError(error: Throwable): Boolean {
+ return when (error) {
+ is DuplicateKeyException -> {
+ errorLogs.add(error.message.orEmpty())
+ true
+ }
+
+ else -> {
+ false
+ }
+ }
+ }
+ }
+
+ @Before
+ fun setUp() {
+ RxJavaRuntime.setDefaultErrorHandler(duplicateKeyErrorHandler)
+ }
+
+ @After
+ fun tearDown() {
+ RxJavaRuntime.setDefaultErrorHandler(null)
+ }
+
+ @Test
+ fun `emitting a generic error throws an exception`() {
+ val result = Try {
+ val formula = OnlyUpdateFormula {
+ events(Action.onInit()) {
+ throw IllegalStateException("crashed")
+ }
+ }
+ runtime.test(formula, Unit)
+ }
+
+ val error = result.errorOrNull()?.cause
+ assertThat(error).isInstanceOf(IllegalStateException::class.java)
+ assertThat(error?.message).isEqualTo("crashed")
+
+ assertThat(errorLogs).isEmpty()
+ }
+
+ @Test
+ fun `adding duplicate child logs an exception`() {
+ val result = Try {
+ val formula = DynamicParentFormula()
+ runtime.test(formula, Unit)
+ .output { addChild(TestKey("1")) }
+ .output { addChild(TestKey("1")) }
+ }
+
+ val error = result.errorOrNull()?.cause
+ assertThat(error).isNull()
+ assertThat(errorLogs).hasSize(1)
+
+ val log = errorLogs.first()
+ val expectedLog = "There already is a child with same key: FormulaKey(scopeKey=null, type=class com.instacart.formula.subjects.KeyFormula, key=TestKey(id=1)). Override [Formula.key] function."
+ assertThat(log).isEqualTo(expectedLog)
+ }
+}