Skip to content

Commit

Permalink
Allow to specify default dispatcher for formula.
Browse files Browse the repository at this point in the history
  • Loading branch information
Laimiux committed Mar 4, 2024
1 parent babd43c commit 8dee4fb
Show file tree
Hide file tree
Showing 18 changed files with 133 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.instacart.formula.coroutines
import com.instacart.formula.FormulaRuntime
import com.instacart.formula.IFormula
import com.instacart.formula.RuntimeConfig
import com.instacart.formula.plugin.Inspector
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
Expand Down
4 changes: 2 additions & 2 deletions formula/src/main/java/com/instacart/formula/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ package com.instacart.formula
*/
interface Action<Event> {
companion object {
private val INIT_ACTION = StartEventAction(Unit)

/**
* Emits an event when [Action] is initialized. You can use this action to send an event
Expand All @@ -44,8 +45,7 @@ interface Action<Event> {
* }
*/
fun onInit(): Action<Unit> {
@Suppress("UNCHECKED_CAST")
return StartEventAction(Unit)
return INIT_ACTION
}

/**
Expand Down
15 changes: 2 additions & 13 deletions formula/src/main/java/com/instacart/formula/ActionBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,7 @@ abstract class ActionBuilder<out Input, State>(
*/
abstract fun <Event> events(
action: Action<Event>,
transition: Transition<Input, State, Event>,
)

/**
* Adds an [Action] as part of this [Evaluation]. [Action] will be initialized
* when it is initially added and cleaned up when it is not returned
* as part of [Evaluation].
*
* @param transition A function that is invoked when [Action] emits an [Event].
*/
abstract fun <Event> onEvent(
action: Action<Event>,
avoidParameterClash: Any = this,
executionType: Transition.ExecutionType? = null,
transition: Transition<Input, State, Event>,
)

Expand All @@ -67,6 +55,7 @@ abstract class ActionBuilder<out Input, State>(
* ```
*/
abstract fun <Event> Action<Event>.onEvent(
executionType: Transition.ExecutionType? = null,
transition: Transition<Input, State, Event>,
)
}
2 changes: 2 additions & 0 deletions formula/src/main/java/com/instacart/formula/Effect.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.instacart.formula

import com.instacart.formula.plugin.Plugin

/**
* Effect is a function returned within [Transition.Result] which will be executed
* by [FormulaRuntime]. The execution timing and thread will depend on the [Effect.Type]
Expand Down
32 changes: 30 additions & 2 deletions formula/src/main/java/com/instacart/formula/FormulaContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.instacart.formula

import com.instacart.formula.internal.Listeners
import com.instacart.formula.internal.UnitListener
import com.instacart.formula.plugin.Dispatcher
import kotlin.reflect.KClass

/**
Expand All @@ -22,12 +23,36 @@ abstract class FormulaContext<out Input, State> internal constructor(
*/
fun callback(
key: Any? = null,
executionType: Transition.ExecutionType? = null,
transition: Transition<Input, State, Unit>,
): () -> Unit {
val listener = onEvent(key, transition)
val listener = onEvent(key, executionType, transition)
return UnitListener(listener)
}


/**
* Creates a listener that takes an event and performs a [Transition].
*/
fun callback(
executionType: Transition.ExecutionType,
transition: Transition<Input, State, Unit>,
): () -> Unit {
val listener = onEvent(null, executionType, transition)
return UnitListener(listener)
}

/**
* Creates a [Listener] that takes a [Event] and performs a [Transition]. It uses a composite
* key of [transition] type and optional [key] property.
*/
fun <Event> onEvent(
executionType: Transition.ExecutionType,
transition: Transition<Input, State, Event>,
): Listener<Event> {
return onEvent(key = null, executionType, transition)
}

/**
* Creates a [Listener] that takes a [Event] and performs a [Transition]. It uses a composite
* key of [transition] type and optional [key] property.
Expand All @@ -37,11 +62,13 @@ abstract class FormulaContext<out Input, State> internal constructor(
*/
fun <Event> onEvent(
key: Any? = null,
executionType: Transition.ExecutionType? = null,
transition: Transition<Input, State, Event>,
): Listener<Event> {
return eventListener(
key = createScopedKey(transition.type(), key),
transition = transition
executionType = executionType,
transition = transition,
)
}

Expand Down Expand Up @@ -86,6 +113,7 @@ abstract class FormulaContext<out Input, State> internal constructor(
internal abstract fun <Event> eventListener(
key: Any,
useIndex: Boolean = true,
executionType: Transition.ExecutionType? = null,
transition: Transition<Input, State, Event>
): Listener<Event>

Expand Down
4 changes: 2 additions & 2 deletions formula/src/main/java/com/instacart/formula/FormulaPlugins.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ object FormulaPlugins {
}

fun mainThreadDispatcher(): Dispatcher {
return plugin?.mainThreadDispatcher() ?: Dispatcher.NoOp
return plugin?.mainThreadDispatcher() ?: Dispatcher.None
}

fun backgroundThreadDispatcher(): Dispatcher {
return plugin?.backgroundThreadDispatcher() ?: Dispatcher.NoOp
return plugin?.backgroundThreadDispatcher() ?: Dispatcher.None
}
}
15 changes: 8 additions & 7 deletions formula/src/main/java/com/instacart/formula/FormulaRuntime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.instacart.formula.internal.FormulaManagerImpl
import com.instacart.formula.internal.ManagerDelegate
import com.instacart.formula.internal.SynchronizedUpdateQueue
import com.instacart.formula.plugin.Dispatcher
import com.instacart.formula.plugin.Inspector
import java.util.LinkedList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
Expand All @@ -20,14 +19,15 @@ class FormulaRuntime<Input : Any, Output : Any>(
config: RuntimeConfig?,
) : ManagerDelegate {
private val isValidationEnabled = config?.isValidationEnabled ?: false
private val synchronizedUpdateQueue = SynchronizedUpdateQueue(
onEmpty = { emitOutputIfNeeded() }
)
private val inspector = FormulaPlugins.inspector(
type = formula.type(),
local = config?.inspector,
)
private val defaultDispatcher: Dispatcher = config?.defaultDispatcher ?: Dispatcher.None
private val implementation = formula.implementation()
private val synchronizedUpdateQueue = SynchronizedUpdateQueue(
onEmpty = { emitOutputIfNeeded() }
)

@Volatile
private var manager: FormulaManagerImpl<Input, *, Output>? = null
Expand Down Expand Up @@ -249,9 +249,9 @@ class FormulaRuntime<Input : Any, Output : Any>(
while (globalEffectQueue.isNotEmpty()) {
val effect = globalEffectQueue.pollFirst()
val dispatcher = when (effect.type) {
Effect.Unconfined -> Dispatcher.NoOp
Effect.Main -> FormulaPlugins.mainThreadDispatcher()
Effect.Background -> FormulaPlugins.backgroundThreadDispatcher()
Effect.Unconfined -> Dispatcher.None
Effect.Main -> Dispatcher.Main
Effect.Background -> Dispatcher.Background
}
dispatcher.dispatch(effect.executable)
}
Expand Down Expand Up @@ -310,6 +310,7 @@ class FormulaRuntime<Input : Any, Output : Any>(
initialInput = initialInput,
loggingType = formula::class,
inspector = inspector,
defaultDispatcher = defaultDispatcher,
)
}
}
10 changes: 10 additions & 0 deletions formula/src/main/java/com/instacart/formula/RuntimeConfig.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.instacart.formula

import com.instacart.formula.plugin.Inspector
import com.instacart.formula.plugin.Dispatcher

/**
* @param defaultDispatcher Dispatcher used for event processing (this can be overwritten by
* individual events). By default, formula runs on the thread on which the event arrived on.
* @param inspector Inspector that will be used when configuring the formula.
* @param isValidationEnabled A boolean that validates inputs and outputs by
* running [Formula.evaluate] twice. Should NOT be used in production builds,
* preferably only unit tests.
*/
class RuntimeConfig(
val defaultDispatcher: Dispatcher? = null,
val inspector: Inspector? = null,
val isValidationEnabled: Boolean = false,
)
18 changes: 18 additions & 0 deletions formula/src/main/java/com/instacart/formula/Transition.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ fun interface Transition<in Input, State, in Event> {
abstract val effects: List<Effect>
}

/**
* Defines an execution model for the transition
*/
sealed class ExecutionType

/**
* Immediate execution type will try to process the transition immediately on the thread that
* it arrives on. This should be used for user events that need to be processed quickly such
* as navigation.
*/
data object Immediate : ExecutionType()

/**
* Background execution type will try to process the transition on a background thread. This
* should be used for data events that might be expensive to process.
*/
data object Background : ExecutionType()

/**
* Called when an [Event] happens and returns a [Result] object which can indicate a state
* change and/or some executable effects. Use [TransitionContext.none] if nothing should happen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.instacart.formula.ActionBuilder
import com.instacart.formula.DeferredAction
import com.instacart.formula.Snapshot
import com.instacart.formula.Transition
import com.instacart.formula.plugin.Dispatcher

/**
* Implements [ActionBuilder] interface.
Expand All @@ -19,24 +20,18 @@ internal class ActionBuilderImpl<out Input, State> internal constructor(

override fun <Event> events(
action: Action<Event>,
executionType: Transition.ExecutionType?,
transition: Transition<Input, State, Event>,
) {
add(toBoundStream(action, transition))
}

override fun <Event> onEvent(
action: Action<Event>,
avoidParameterClash: Any,
transition: Transition<Input, State, Event>,
) {
add(toBoundStream(action, transition))
add(toBoundStream(action, executionType, transition))
}

override fun <Event> Action<Event>.onEvent(
executionType: Transition.ExecutionType?,
transition: Transition<Input, State, Event>,
) {
val stream = this
this@ActionBuilderImpl.events(stream, transition)
this@ActionBuilderImpl.events(stream, executionType, transition)
}

private fun add(action: DeferredAction<*>) {
Expand All @@ -49,10 +44,11 @@ internal class ActionBuilderImpl<out Input, State> internal constructor(

private fun <Event> toBoundStream(
stream: Action<Event>,
executionType: Transition.ExecutionType? = null,
transition: Transition<Input, State, Event>,
): DeferredAction<Event> {
val key = snapshot.context.createScopedKey(transition.type(), stream.key())
val listener = snapshot.context.eventListener(key, useIndex = false, transition)
val listener = snapshot.context.eventListener(key, useIndex = false, executionType, transition)
return DeferredAction(
key = key,
action = stream,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ internal class ChildrenManager(
formula = implementation,
initialInput = input,
loggingType = formula::class,
inspector = inspector
inspector = inspector,
defaultDispatcher = delegate.defaultDispatcher,
)
}
@Suppress("UNCHECKED_CAST")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.instacart.formula.IFormula
import com.instacart.formula.plugin.Inspector
import com.instacart.formula.Snapshot
import com.instacart.formula.Transition
import com.instacart.formula.plugin.Dispatcher
import java.util.LinkedList
import kotlin.reflect.KClass

Expand All @@ -25,6 +26,7 @@ internal class FormulaManagerImpl<Input, State, Output>(
internal val loggingType: KClass<*>,
private val listeners: Listeners = Listeners(),
private val inspector: Inspector?,
val defaultDispatcher: Dispatcher,
) : FormulaManager<Input, Output>, ManagerDelegate {

private var state: State = formula.initialState(initialInput)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.instacart.formula.internal

import com.instacart.formula.Listener
import com.instacart.formula.Transition
import com.instacart.formula.plugin.Dispatcher

/**
* Note: this class is not a data class because equality is based on instance and not [key].
Expand All @@ -11,16 +12,26 @@ internal class ListenerImpl<Input, State, EventT>(internal var key: Any) : Liste

@Volatile internal var manager: FormulaManagerImpl<Input, State, *>? = null
@Volatile internal var snapshotImpl: SnapshotImpl<Input, State>? = null
@Volatile internal var executionType: Transition.ExecutionType? = null

internal lateinit var transition: Transition<Input, State, EventT>

override fun invoke(event: EventT) {
// TODO: log if null listener (it might be due to formula removal or due to callback removal)
val manager = manager ?: return

manager.queue.postUpdate {
val deferredTransition = DeferredTransition(this, transition, event)
manager.onPendingTransition(deferredTransition)
val dispatcher = when (executionType) {
Transition.Immediate -> Dispatcher.None
Transition.Background -> Dispatcher.Background
// If transition does not specify dispatcher, we use the default one.
else -> manager.defaultDispatcher
}

dispatcher.dispatch {
manager.queue.postUpdate {
val deferredTransition = DeferredTransition(this, transition, event)
manager.onPendingTransition(deferredTransition)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.instacart.formula.Listener
import com.instacart.formula.Snapshot
import com.instacart.formula.Transition
import com.instacart.formula.TransitionContext
import com.instacart.formula.plugin.Dispatcher
import java.lang.IllegalStateException
import kotlin.reflect.KClass

Expand Down Expand Up @@ -47,12 +48,14 @@ internal class SnapshotImpl<out Input, State> internal constructor(
override fun <Event> eventListener(
key: Any,
useIndex: Boolean,
executionType: Transition.ExecutionType?,
transition: Transition<Input, State, Event>
): Listener<Event> {
ensureNotRunning()
val listener = listeners.initOrFindListener<Input, State, Event>(key, useIndex)
listener.manager = delegate
listener.snapshotImpl = this
listener.executionType = executionType
listener.transition = transition
return listener
}
Expand Down
Loading

0 comments on commit 8dee4fb

Please sign in to comment.