diff --git a/formula/src/main/java/com/instacart/formula/internal/ChildrenManager.kt b/formula/src/main/java/com/instacart/formula/internal/ChildrenManager.kt new file mode 100644 index 00000000..867ad6d5 --- /dev/null +++ b/formula/src/main/java/com/instacart/formula/internal/ChildrenManager.kt @@ -0,0 +1,84 @@ +package com.instacart.formula.internal + +import com.instacart.formula.IFormula + +/** + * Keeps track of child formula managers. + */ +internal class ChildrenManager( + private val childTransitionListener: TransitionListener, +) { + private var children: SingleRequestMap>? = null + private var pendingRemoval: MutableList>? = null + + fun updateTransitionId(transitionId: TransitionId) { + children?.forEachValue { it.updateTransitionId(transitionId) } + } + + fun evaluationFinished() { + children?.clearUnrequested { + pendingRemoval = pendingRemoval ?: mutableListOf() + it.markAsTerminated() + pendingRemoval?.add(it) + } + } + + fun terminateDetachedChildren(transitionId: TransitionId): Boolean { + val local = pendingRemoval + pendingRemoval = null + local?.forEach { it.performTerminationSideEffects() } + if (transitionId.hasTransitioned()) { + return true + } + + return children?.any { it.value.value.terminateDetachedChildren(transitionId) } ?: false + } + + fun terminateOldUpdates(transitionId: TransitionId): Boolean { + children?.forEachValue { + if (it.terminateOldUpdates(transitionId)) { + return true + } + } + return false + } + + fun startNewUpdates(transitionId: TransitionId): Boolean { + children?.forEachValue { + if (it.startNewUpdates(transitionId)) { + return true + } + } + return false + } + + fun markAsTerminated() { + children?.forEachValue { it.markAsTerminated() } + } + + fun performTerminationSideEffects() { + children?.forEachValue { it.performTerminationSideEffects() } + } + + fun findOrInitChild( + key: Any, + formula: IFormula, + input: ChildInput, + ): FormulaManager { + @Suppress("UNCHECKED_CAST") + val children = children ?: run { + val initialized: SingleRequestMap> = LinkedHashMap() + this.children = initialized + initialized + } + + return children + .findOrInit(key) { + val implementation = formula.implementation() + FormulaManagerImpl(implementation, input, childTransitionListener) + } + .requestAccess { + throw IllegalStateException("There already is a child with same key: $key. Override [Formula.key] function.") + } as FormulaManager + } +} \ No newline at end of file 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 69d7ea29..9dd2428d 100644 --- a/formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt +++ b/formula/src/main/java/com/instacart/formula/internal/FormulaManagerImpl.kt @@ -3,6 +3,7 @@ package com.instacart.formula.internal import com.instacart.formula.Evaluation import com.instacart.formula.Formula import com.instacart.formula.IFormula +import com.instacart.formula.Snapshot import com.instacart.formula.Transition /** @@ -17,26 +18,16 @@ import com.instacart.formula.Transition internal class FormulaManagerImpl( private val formula: Formula, initialInput: Input, - private val listeners: Listeners, - private val transitionListener: TransitionListener + private val transitionListener: TransitionListener, + private val listeners: Listeners = Listeners(), + private val actionManager: ActionManager = ActionManager(), ) : FormulaManager { - constructor( - formula: Formula, - input: Input, - transitionListener: TransitionListener - ): this(formula, input, Listeners(), transitionListener) - - private val actionManager = ActionManager() - - private var children: SingleRequestMap>? = null - private var frame: Frame? = null - var terminated = false - private var state: State = formula.initialState(initialInput) - private var pendingRemoval: MutableList>? = null + private var frame: Frame? = null + private var childrenManager: ChildrenManager? = null - private var childTransitionListener: TransitionListener? = null + var terminated = false private fun handleTransitionResult(result: Transition.Result) { if (terminated) { @@ -58,7 +49,7 @@ internal class FormulaManagerImpl( val lastFrame = checkNotNull(frame) { "missing frame means this is called before initial evaluate" } lastFrame.transitionDispatcher.transitionId = transitionId - children?.forEachValue { it.updateTransitionId(transitionId) } + childrenManager?.updateTransitionId(transitionId) } /** @@ -82,32 +73,20 @@ internal class FormulaManagerImpl( val transitionDispatcher = TransitionDispatcher(input, state, this::handleTransitionResult, transitionId) val snapshot = SnapshotImpl(transitionId, listeners, this, transitionDispatcher) - val result = snapshot.run { formula.run { evaluate() } } + val result = formula.evaluate(snapshot) val frame = Frame(input, state, result, transitionDispatcher) actionManager.updateEventListeners(frame.evaluation.actions) this.frame = frame listeners.evaluationFinished() - - children?.clearUnrequested { - pendingRemoval = pendingRemoval ?: mutableListOf() - it.markAsTerminated() - pendingRemoval?.add(it) - } + childrenManager?.evaluationFinished() transitionDispatcher.running = true return result } override fun terminateDetachedChildren(transitionId: TransitionId): Boolean { - val local = pendingRemoval - pendingRemoval = null - local?.forEach { it.performTerminationSideEffects() } - if (transitionId.hasTransitioned()) { - return true - } - - return children?.any { it.value.value.terminateDetachedChildren(transitionId) } ?: false + return childrenManager?.terminateDetachedChildren(transitionId) == true } // TODO: should probably terminate children streams, then self. @@ -119,10 +98,8 @@ internal class FormulaManagerImpl( } // Step through children frames - children?.forEachValue { - if (it.terminateOldUpdates(transitionId)) { - return true - } + if (childrenManager?.terminateOldUpdates(transitionId) == true) { + return true } return false @@ -137,10 +114,8 @@ internal class FormulaManagerImpl( } // Step through children frames - children?.forEachValue { - if (it.startNewUpdates(transitionId)) { - return true - } + if (childrenManager?.startNewUpdates(transitionId) == true) { + return true } return false @@ -152,50 +127,43 @@ internal class FormulaManagerImpl( input: ChildInput, transitionId: TransitionId ): ChildOutput { - @Suppress("UNCHECKED_CAST") - val children = children ?: run { - val initialized: SingleRequestMap> = LinkedHashMap() - this.children = initialized - initialized - } - - val manager = children - .findOrInit(key) { - val childTransitionListener = getOrInitChildTransitionListener() - val implementation = formula.implementation() - FormulaManagerImpl(implementation, input, childTransitionListener) - } - .requestAccess { - throw IllegalStateException("There already is a child with same key: $key. Override [Formula.key] function.") - } as FormulaManager - + val childrenManager = getOrInitChildrenManager() + val manager = childrenManager.findOrInitChild(key, formula, input) return manager.evaluate(input, transitionId).output } override fun markAsTerminated() { terminated = true frame?.transitionDispatcher?.terminated = true - children?.forEachValue { it.markAsTerminated() } + childrenManager?.markAsTerminated() } override fun performTerminationSideEffects() { - children?.forEachValue { it.performTerminationSideEffects() } + childrenManager?.performTerminationSideEffects() actionManager.terminate() listeners.disableAll() } - private fun getOrInitChildTransitionListener(): TransitionListener { - return childTransitionListener ?: run { - TransitionListener { result, isChildValid -> + private fun getOrInitChildrenManager(): ChildrenManager { + return childrenManager ?: run { + val listener = TransitionListener { result, isChildValid -> val frame = this.frame if (!isChildValid) { frame?.childInvalidated() } val isValid = frame != null && frame.isValid() transitionListener.onTransitionResult(result, isValid) - }.apply { - childTransitionListener = this } + + val value = ChildrenManager(listener) + childrenManager = value + value } } + + private fun Formula.evaluate( + snapshot: Snapshot + ): Evaluation { + return snapshot.run { evaluate() } + } }