diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt
index 324ed39ae..903599871 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt
@@ -1,6 +1,7 @@
package com.squareup.workflow1
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
+import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -194,6 +195,8 @@ public interface WorkflowInterceptor {
/**
* Information about the session of a workflow in the runtime that a [WorkflowInterceptor] method
* is intercepting.
+ *
+ * Implementations should override [toString] to call [WorkflowSession.workflowSessionToString].
*/
public interface WorkflowSession {
/** The [WorkflowIdentifier] that represents the type of this workflow. */
@@ -419,6 +422,16 @@ internal fun
WorkflowInterceptor.intercept(
}
}
+internal fun WorkflowSession.workflowSessionToString(): String {
+ val parentDescription = parent?.let { "WorkflowInstance(…)" }
+ return "WorkflowInstance(" +
+ "identifier=$identifier, " +
+ "renderKey=$renderKey, " +
+ "instanceId=$sessionId, " +
+ "parent=$parentDescription" +
+ ")"
+}
+
private class InterceptedRenderContext
(
private val baseRenderContext: BaseRenderContext
,
private val interceptor: RenderContextInterceptor
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt
new file mode 100644
index 000000000..aa09861d0
--- /dev/null
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt
@@ -0,0 +1,374 @@
+package com.squareup.workflow1.internal
+
+import com.squareup.workflow1.ActionApplied
+import com.squareup.workflow1.ActionProcessingResult
+import com.squareup.workflow1.ActionsExhausted
+import com.squareup.workflow1.NoopWorkflowInterceptor
+import com.squareup.workflow1.NullableInitBox
+import com.squareup.workflow1.RenderContext
+import com.squareup.workflow1.RuntimeConfig
+import com.squareup.workflow1.RuntimeConfigOptions
+import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
+import com.squareup.workflow1.StatefulWorkflow
+import com.squareup.workflow1.TreeSnapshot
+import com.squareup.workflow1.Workflow
+import com.squareup.workflow1.WorkflowAction
+import com.squareup.workflow1.WorkflowExperimentalApi
+import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.WorkflowIdentifier
+import com.squareup.workflow1.WorkflowInterceptor
+import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
+import com.squareup.workflow1.WorkflowTracer
+import com.squareup.workflow1.applyTo
+import com.squareup.workflow1.intercept
+import com.squareup.workflow1.internal.RealRenderContext.RememberStore
+import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner
+import com.squareup.workflow1.trace
+import com.squareup.workflow1.workflowSessionToString
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart.LAZY
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.selects.SelectBuilder
+import kotlin.coroutines.CoroutineContext
+import kotlin.reflect.KType
+
+/**
+ * A node in a state machine tree. Manages the actual state for a given [Workflow].
+ *
+ * @param emitAppliedActionToParent A function that this node will call to pass the result of
+ * applying an action to its parent.
+ * @param baseContext [CoroutineContext] that is appended to the end of the context used to launch
+ * worker coroutines. This context will override anything from the workflow's scope and any other
+ * hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate
+ * structured concurrency).
+ */
+@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
+internal class StatefulWorkflowNode(
+ id: WorkflowNodeId,
+ workflow: StatefulWorkflow,
+ initialProps: PropsT,
+ snapshot: TreeSnapshot?,
+ baseContext: CoroutineContext,
+ // Providing default value so we don't need to specify in test.
+ override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
+ override val workflowTracer: WorkflowTracer? = null,
+ emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult = { it },
+ override val parent: WorkflowSession? = null,
+ interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
+ idCounter: IdCounter? = null
+) : WorkflowNode(
+ id = id,
+ baseContext = baseContext,
+ interceptor = interceptor,
+ emitAppliedActionToParent = emitAppliedActionToParent,
+),
+ SideEffectRunner,
+ RememberStore,
+ WorkflowSession {
+
+ // WorkflowSession properties
+ override val identifier: WorkflowIdentifier get() = id.identifier
+ override val renderKey: String get() = id.name
+ override val sessionId: Long = idCounter.createId()
+ private var cachedWorkflowInstance: StatefulWorkflow
+ private var interceptedWorkflowInstance: StatefulWorkflow
+
+ override val session: WorkflowSession
+ get() = this
+
+ private val subtreeManager = SubtreeManager(
+ snapshotCache = snapshot?.childTreeSnapshots,
+ contextForChildren = scope.coroutineContext,
+ emitActionToParent = ::applyAction,
+ runtimeConfig = runtimeConfig,
+ workflowTracer = workflowTracer,
+ workflowSession = this,
+ interceptor = interceptor,
+ idCounter = idCounter
+ )
+ private val sideEffects = ActiveStagingList()
+ private val remembered = ActiveStagingList>()
+ private var lastProps: PropsT = initialProps
+ private var lastRendering: NullableInitBox = NullableInitBox()
+ private val eventActionsChannel =
+ Channel>(capacity = UNLIMITED)
+ private var state: StateT
+
+ /**
+ * The state of this node or that of one of our descendants changed since we last rendered.
+ */
+ private var subtreeStateDirty: Boolean = true
+
+ /**
+ * The state of this node changed since we last rendered.
+ */
+ private var selfStateDirty: Boolean = true
+
+ private val baseRenderContext = RealRenderContext(
+ renderer = subtreeManager,
+ sideEffectRunner = this,
+ rememberStore = this,
+ eventActionsChannel = eventActionsChannel,
+ workflowTracer = workflowTracer,
+ runtimeConfig = runtimeConfig
+ )
+ private val context = RenderContext(baseRenderContext, workflow)
+
+ init {
+ interceptor.onSessionStarted(workflowScope = scope, session = this)
+
+ cachedWorkflowInstance = workflow
+ interceptedWorkflowInstance = interceptor.intercept(
+ workflow = cachedWorkflowInstance,
+ workflowSession = this
+ )
+ state = interceptedWorkflowInstance.initialState(
+ props = initialProps,
+ snapshot = snapshot?.workflowSnapshot,
+ workflowScope = scope
+ )
+ }
+
+ override fun toString(): String = workflowSessionToString()
+
+ /**
+ * Walk the tree of workflows, rendering each one and using
+ * [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
+ * render themselves and aggregate those child renderings.
+ */
+ @Suppress("UNCHECKED_CAST")
+ override fun render(
+ workflow: Workflow,
+ input: PropsT
+ ): RenderingT = renderWithStateType(
+ workflow = workflow.asStatefulWorkflow() as
+ StatefulWorkflow,
+ props = input
+ )
+
+ /**
+ * Walk the tree of state machines again, this time gathering snapshots and aggregating them
+ * automatically.
+ */
+ override fun snapshot(): TreeSnapshot {
+ return interceptor.onSnapshotStateWithChildren(
+ proceed = {
+ val childSnapshots = subtreeManager.createChildSnapshots()
+ val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
+ TreeSnapshot(
+ workflowSnapshot = rootSnapshot,
+ // Create the snapshots eagerly since subtreeManager is mutable.
+ childTreeSnapshots = { childSnapshots }
+ )
+ },
+ session = this
+ )
+ }
+
+ override fun runningSideEffect(
+ key: String,
+ sideEffect: suspend CoroutineScope.() -> Unit
+ ) {
+ // Prevent duplicate side effects with the same key.
+ sideEffects.forEachStaging {
+ requireWithKey(key != it.key, key) { "Expected side effect keys to be unique: \"$key\"" }
+ }
+
+ sideEffects.retainOrCreate(
+ predicate = { key == it.key },
+ create = { createSideEffectNode(key, sideEffect) }
+ )
+ }
+
+ override fun remember(
+ key: String,
+ resultType: KType,
+ vararg inputs: Any?,
+ calculation: () -> ResultT
+ ): ResultT {
+ remembered.forEachStaging {
+ requireWithKey(
+ key != it.key || resultType != it.resultType || !inputs.contentEquals(it.inputs),
+ stackTraceKey = key
+ ) {
+ "Expected unique combination of key, input types and result type: \"$key\""
+ }
+ }
+
+ val result = remembered.retainOrCreate(
+ predicate = {
+ key == it.key && it.resultType == resultType && inputs.contentEquals(it.inputs)
+ },
+ create = { RememberedNode(key, resultType, inputs, calculation()) }
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ return result.lastCalculated as ResultT
+ }
+
+ override fun registerTreeActionSelectors(selector: SelectBuilder) {
+ // Listen for any child workflow updates.
+ subtreeManager.registerChildActionSelectors(selector)
+
+ // Listen for any events.
+ with(selector) {
+ eventActionsChannel.onReceive { action ->
+ return@onReceive applyAction(action)
+ }
+ }
+ }
+
+ override fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean): ActionProcessingResult {
+ if (skipDirtyNodes && selfStateDirty) return ActionsExhausted
+
+ val result = subtreeManager.applyNextAvailableChildAction(skipDirtyNodes)
+
+ if (result == ActionsExhausted) {
+ return eventActionsChannel.tryReceive().getOrNull()?.let { action ->
+ applyAction(action)
+ } ?: ActionsExhausted
+ }
+ return result
+ }
+
+ /**
+ * Cancels this state machine host, and any coroutines started as children of it.
+ *
+ * This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an
+ * error to call [registerTreeActionSelectors] after calling this method.
+ */
+ override fun cancel(cause: CancellationException?) {
+ super.cancel(cause)
+ lastRendering = NullableInitBox()
+ }
+
+ /**
+ * Call this after we have been passed any workflow instance, in [render] or [snapshot]. It may
+ * have changed and we should check to see if we need to update our cached instances.
+ */
+ private fun maybeUpdateCachedWorkflowInstance(
+ workflow: StatefulWorkflow
+ ) {
+ if (workflow !== cachedWorkflowInstance) {
+ // The instance has changed.
+ cachedWorkflowInstance = workflow
+ interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this)
+ }
+ }
+
+ /**
+ * Contains the actual logic for [render], after we've casted the passed-in [Workflow]'s
+ * state type to our `StateT`.
+ */
+ private fun renderWithStateType(
+ workflow: StatefulWorkflow,
+ props: PropsT
+ ): RenderingT {
+ updatePropsAndState(props, workflow)
+
+ if (!runtimeConfig.contains(PARTIAL_TREE_RENDERING) ||
+ !lastRendering.isInitialized ||
+ subtreeStateDirty
+ ) {
+ // If we haven't already updated the cached instance, better do it now!
+ maybeUpdateCachedWorkflowInstance(workflow)
+
+ baseRenderContext.unfreeze()
+ lastRendering = NullableInitBox(interceptedWorkflowInstance.render(props, state, context))
+ baseRenderContext.freeze()
+
+ workflowTracer.trace("UpdateRuntimeTree") {
+ // Tear down workflows and workers that are obsolete.
+ subtreeManager.commitRenderedChildren()
+ // Side effect jobs are launched lazily, since they can send actions to the sink, and can only
+ // be started after context is frozen.
+ sideEffects.forEachStaging { it.job.start() }
+ sideEffects.commitStaging { it.job.cancel() }
+ remembered.commitStaging { /* Nothing to clean up. */ }
+ }
+ // After we have rendered this subtree, we need another action in order for us to be
+ // considered dirty again.
+ subtreeStateDirty = false
+ selfStateDirty = false
+ }
+
+ return lastRendering.getOrThrow()
+ }
+
+ /**
+ * Update props if they have changed. If that happens, then check to see if we need
+ * to update the cached workflow instance, then call [StatefulWorkflow.onPropsChanged] and
+ * update the state from that. We consider any change to props as dirty because
+ * the props themselves are used in [StatefulWorkflow.render] (they are the 'external' part of
+ * the state) so we must re-render.
+ */
+ private fun updatePropsAndState(
+ newProps: PropsT,
+ workflow: StatefulWorkflow,
+ ) {
+ if (newProps != lastProps) {
+ maybeUpdateCachedWorkflowInstance(workflow)
+ val newState = interceptedWorkflowInstance.onPropsChanged(lastProps, newProps, state)
+ state = newState
+ subtreeStateDirty = true
+ selfStateDirty = true
+ }
+ lastProps = newProps
+ }
+
+ /**
+ * Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
+ * via [emitAppliedActionToParent] to the parent, with additional information as to whether or
+ * not this action has changed the current node's state.
+ */
+ private fun applyAction(
+ action: WorkflowAction,
+ childResult: ActionApplied<*>? = null
+ ): ActionProcessingResult {
+ val (newState: StateT, actionApplied: ActionApplied) = action.applyTo(lastProps, state)
+ state = newState
+ // Aggregate the action with the child result, if any.
+ val aggregateActionApplied = actionApplied.copy(
+ // Changing state is sticky, we pass it up if it ever changed.
+ stateChanged = actionApplied.stateChanged || (childResult?.stateChanged ?: false)
+ )
+ // Our state changed.
+ selfStateDirty = selfStateDirty || actionApplied.stateChanged
+ // Our state changed or one of our children's state changed.
+ subtreeStateDirty = subtreeStateDirty || aggregateActionApplied.stateChanged
+ return if (actionApplied.output != null ||
+ runtimeConfig.contains(PARTIAL_TREE_RENDERING)
+ ) {
+ // If we are using the optimization, always return to the parent, so we carry a path that
+ // notes that the subtree did change all the way to the root.
+ //
+ // We don't need that without the optimization because there is nothing
+ // to output from the root of the runtime -- the output has propagated
+ // as far as it needs to causing all corresponding state changes.
+ //
+ // However, the root and the path down to the changed nodes must always
+ // re-render now, so this is the implementation detail of how we get
+ // subtreeStateDirty = true on that entire path to the root.
+ emitAppliedActionToParent(aggregateActionApplied)
+ } else {
+ aggregateActionApplied
+ }
+ }
+
+ private fun createSideEffectNode(
+ key: String,
+ sideEffect: suspend CoroutineScope.() -> Unit
+ ): SideEffectNode {
+ return workflowTracer.trace("CreateSideEffectNode") {
+ val scope = scope + CoroutineName("sideEffect[$key] for $id")
+ val job = scope.launch(start = LAZY, block = sideEffect)
+ SideEffectNode(key, job)
+ }
+ }
+}
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt
index 7cf7ab163..60d79ccf5 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt
@@ -25,9 +25,9 @@ import kotlin.coroutines.CoroutineContext
*
* ## Rendering
*
- * This class implements [RealRenderContext.Renderer], and [WorkflowNode] will pass its instance
- * of this class to the [RealRenderContext] on each render pass to render children. That means that
- * when a workflow renders a child, this class does the actual work.
+ * This class implements [RealRenderContext.Renderer], and [StatefulWorkflowNode] will pass its
+ * instance of this class to the [RealRenderContext] on each render pass to render children. That
+ * means that when a workflow renders a child, this class does the actual work.
*
* This class keeps two lists:
* 1. Active list: All the children from the last render pass that have not yet been rendered in
@@ -55,7 +55,7 @@ import kotlin.coroutines.CoroutineContext
* active: [bar]
* staging: [foo, baz]
* ```
- * 4. When the workflow's render method returns, the [WorkflowNode] calls
+ * 4. When the workflow's render method returns, the [StatefulWorkflowNode] calls
* [commitRenderedChildren], which:
* 1. Tears down all the children remaining in the active list
* ```
@@ -143,7 +143,7 @@ internal class SubtreeManager(
)
}
stagedChild.setHandler(handler)
- return stagedChild.render(child.asStatefulWorkflow(), props)
+ return stagedChild.render(child, props)
}
/**
@@ -179,8 +179,7 @@ internal class SubtreeManager(
fun createChildSnapshots(): Map {
val snapshots = mutableMapOf()
children.forEachActive { child ->
- val childWorkflow = child.workflow.asStatefulWorkflow()
- snapshots[child.id] = child.workflowNode.snapshot(childWorkflow)
+ snapshots[child.id] = child.workflowNode.snapshot()
}
return snapshots
}
@@ -205,9 +204,9 @@ internal class SubtreeManager(
val childTreeSnapshots = snapshotCache?.get(id)
- val workflowNode = WorkflowNode(
+ val workflowNode = createWorkflowNode(
id = id,
- workflow = child.asStatefulWorkflow(),
+ workflow = child,
initialProps = initialProps,
snapshot = childTreeSnapshots,
baseContext = contextForChildren,
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt
index ea2d46876..87648454d 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowChildNode.kt
@@ -1,6 +1,5 @@
package com.squareup.workflow1.internal
-import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowTracer
@@ -22,7 +21,7 @@ internal class WorkflowChildNode<
>(
val workflow: Workflow<*, ChildOutputT, *>,
private var handler: (ChildOutputT) -> WorkflowAction,
- val workflowNode: WorkflowNode
+ val workflowNode: WorkflowNode
) : InlineListNode> {
override var nextListNode: WorkflowChildNode<*, *, *, *, *>? = null
@@ -51,12 +50,12 @@ internal class WorkflowChildNode<
* Wrapper around [WorkflowNode.render] that allows calling it with erased types.
*/
fun render(
- workflow: StatefulWorkflow<*, *, *, *>,
+ workflow: Workflow<*, *, *>,
props: Any?
): R {
@Suppress("UNCHECKED_CAST")
return workflowNode.render(
- workflow as StatefulWorkflow,
+ workflow as Workflow,
props as ChildPropsT
) as R
}
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
index 22b2d5d31..125349287 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt
@@ -4,240 +4,110 @@ import com.squareup.workflow1.ActionApplied
import com.squareup.workflow1.ActionProcessingResult
import com.squareup.workflow1.ActionsExhausted
import com.squareup.workflow1.NoopWorkflowInterceptor
-import com.squareup.workflow1.NullableInitBox
-import com.squareup.workflow1.RenderContext
import com.squareup.workflow1.RuntimeConfig
import com.squareup.workflow1.RuntimeConfigOptions
-import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
-import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.TreeSnapshot
import com.squareup.workflow1.Workflow
-import com.squareup.workflow1.WorkflowAction
-import com.squareup.workflow1.WorkflowExperimentalApi
-import com.squareup.workflow1.WorkflowExperimentalRuntime
-import com.squareup.workflow1.WorkflowIdentifier
import com.squareup.workflow1.WorkflowInterceptor
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
import com.squareup.workflow1.WorkflowTracer
-import com.squareup.workflow1.applyTo
-import com.squareup.workflow1.intercept
-import com.squareup.workflow1.internal.RealRenderContext.RememberStore
-import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner
-import com.squareup.workflow1.trace
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart.LAZY
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
import kotlinx.coroutines.selects.SelectBuilder
import kotlin.coroutines.CoroutineContext
-import kotlin.reflect.KType
-/**
- * A node in a state machine tree. Manages the actual state for a given [Workflow].
- *
- * @param emitAppliedActionToParent A function that this node will call to pass the result of
- * applying an action to its parent.
- * @param baseContext [CoroutineContext] that is appended to the end of the context used to launch
- * worker coroutines. This context will override anything from the workflow's scope and any other
- * hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate
- * structured concurrency).
- */
-@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
-internal class WorkflowNode(
- val id: WorkflowNodeId,
- workflow: StatefulWorkflow,
+internal fun createWorkflowNode(
+ id: WorkflowNodeId,
+ workflow: Workflow,
initialProps: PropsT,
snapshot: TreeSnapshot?,
baseContext: CoroutineContext,
// Providing default value so we don't need to specify in test.
- override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
- override val workflowTracer: WorkflowTracer? = null,
- private val emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult =
- { it },
- override val parent: WorkflowSession? = null,
- private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
+ runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
+ workflowTracer: WorkflowTracer? = null,
+ emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult = { it },
+ parent: WorkflowSession? = null,
+ interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
idCounter: IdCounter? = null
-) : CoroutineScope, SideEffectRunner, RememberStore, WorkflowSession {
+): WorkflowNode = StatefulWorkflowNode(
+ id = id,
+ workflow = workflow.asStatefulWorkflow(),
+ initialProps = initialProps,
+ snapshot = snapshot,
+ baseContext = baseContext,
+ runtimeConfig = runtimeConfig,
+ workflowTracer = workflowTracer,
+ emitAppliedActionToParent = emitAppliedActionToParent,
+ parent = parent,
+ interceptor = interceptor,
+ idCounter = idCounter,
+)
+
+internal abstract class WorkflowNode(
+ val id: WorkflowNodeId,
+ protected val interceptor: WorkflowInterceptor,
+ protected val emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult,
+ baseContext: CoroutineContext,
+) {
/**
- * Context that has a job that will live as long as this node.
+ * Scope that has a job that will live as long as this node and be cancelled when [cancel] is
+ * called.
* Also adds a debug name to this coroutine based on its ID.
*/
- override val coroutineContext = baseContext + Job(baseContext[Job]) + CoroutineName(id.toString())
-
- // WorkflowInstance properties
- override val identifier: WorkflowIdentifier get() = id.identifier
- override val renderKey: String get() = id.name
- override val sessionId: Long = idCounter.createId()
- private var cachedWorkflowInstance: StatefulWorkflow
- private var interceptedWorkflowInstance: StatefulWorkflow
-
- private val subtreeManager = SubtreeManager(
- snapshotCache = snapshot?.childTreeSnapshots,
- contextForChildren = coroutineContext,
- emitActionToParent = ::applyAction,
- runtimeConfig = runtimeConfig,
- workflowTracer = workflowTracer,
- workflowSession = this,
- interceptor = interceptor,
- idCounter = idCounter
+ val scope: CoroutineScope = CoroutineScope(
+ baseContext +
+ Job(parent = baseContext[Job]) +
+ CoroutineName(id.toString())
)
- private val sideEffects = ActiveStagingList()
- private val remembered = ActiveStagingList>()
- private var lastProps: PropsT = initialProps
- private var lastRendering: NullableInitBox = NullableInitBox()
- private val eventActionsChannel =
- Channel>(capacity = UNLIMITED)
- private var state: StateT
/**
- * The state of this node or that of one of our descendants changed since we last rendered.
+ * The [WorkflowSession] that represents this node to [WorkflowInterceptor]s.
*/
- private var subtreeStateDirty: Boolean = true
-
- /**
- * The state of this node changed since we last rendered.
- */
- private var selfStateDirty: Boolean = true
-
- private val baseRenderContext = RealRenderContext(
- renderer = subtreeManager,
- sideEffectRunner = this,
- rememberStore = this,
- eventActionsChannel = eventActionsChannel,
- workflowTracer = workflowTracer,
- runtimeConfig = runtimeConfig
- )
- private val context = RenderContext(baseRenderContext, workflow)
-
- init {
- interceptor.onSessionStarted(this, this)
-
- cachedWorkflowInstance = workflow
- interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this)
- state = interceptedWorkflowInstance.initialState(initialProps, snapshot?.workflowSnapshot, this)
- }
-
- override fun toString(): String {
- val parentDescription = parent?.let { "WorkflowInstance(…)" }
- return "WorkflowInstance(" +
- "identifier=$identifier, " +
- "renderKey=$renderKey, " +
- "instanceId=$sessionId, " +
- "parent=$parentDescription" +
- ")"
- }
+ abstract val session: WorkflowSession
/**
* Walk the tree of workflows, rendering each one and using
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
* render themselves and aggregate those child renderings.
+ *
+ * @param workflow The "template" workflow instance used in the current render pass. This isn't
+ * necessarily the same _instance_ every call, but will be the same _type_.
*/
- @Suppress("UNCHECKED_CAST")
- fun render(
- workflow: StatefulWorkflow,
+ abstract fun render(
+ workflow: Workflow,
input: PropsT
- ): RenderingT =
- renderWithStateType(workflow as StatefulWorkflow, input)
+ ): RenderingT
/**
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
* automatically.
*/
- fun snapshot(workflow: StatefulWorkflow<*, *, *, *>): TreeSnapshot {
- @Suppress("UNCHECKED_CAST")
- val typedWorkflow = workflow as StatefulWorkflow
- maybeUpdateCachedWorkflowInstance(typedWorkflow)
- return interceptor.onSnapshotStateWithChildren({
- val childSnapshots = subtreeManager.createChildSnapshots()
- val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
- TreeSnapshot(
- workflowSnapshot = rootSnapshot,
- // Create the snapshots eagerly since subtreeManager is mutable.
- childTreeSnapshots = { childSnapshots }
- )
- }, this)
- }
-
- override fun runningSideEffect(
- key: String,
- sideEffect: suspend CoroutineScope.() -> Unit
- ) {
- // Prevent duplicate side effects with the same key.
- sideEffects.forEachStaging {
- requireWithKey(key != it.key, key) { "Expected side effect keys to be unique: \"$key\"" }
- }
-
- sideEffects.retainOrCreate(
- predicate = { key == it.key },
- create = { createSideEffectNode(key, sideEffect) }
- )
- }
-
- override fun remember(
- key: String,
- resultType: KType,
- vararg inputs: Any?,
- calculation: () -> ResultT
- ): ResultT {
- remembered.forEachStaging {
- requireWithKey(
- key != it.key || resultType != it.resultType || !inputs.contentEquals(it.inputs),
- stackTraceKey = key
- ) {
- "Expected unique combination of key, input types and result type: \"$key\""
- }
- }
-
- val result = remembered.retainOrCreate(
- predicate = {
- key == it.key && it.resultType == resultType && inputs.contentEquals(it.inputs)
- },
- create = { RememberedNode(key, resultType, inputs, calculation()) }
- )
-
- @Suppress("UNCHECKED_CAST")
- return result.lastCalculated as ResultT
- }
+ abstract fun snapshot(): TreeSnapshot
/**
* Register select clauses for the next [result][ActionProcessingResult] from the state machine.
*
- * Walk the tree of state machines, asking each one to wait for its next event. If something happen
- * that results in an output, that output is returned. Null means something happened that requires
- * a re-render, e.g. my state changed or a child state changed.
+ * Walk the tree of state machines, asking each one to wait for its next event. If something
+ * happens that results in an output, that output is returned. Null means something happened that
+ * requires a re-render, e.g. my state changed or a child state changed.
*
* It is an error to call this method after calling [cancel].
*
* Contrast this to [applyNextAvailableTreeAction], which is used to check for an action
* that is already available without waiting, and then _immediately_ apply it.
- * The select clauses added here also call [applyAction] when one of them is selected.
*/
- fun registerTreeActionSelectors(selector: SelectBuilder) {
- // Listen for any child workflow updates.
- subtreeManager.registerChildActionSelectors(selector)
-
- // Listen for any events.
- with(selector) {
- eventActionsChannel.onReceive { action ->
- return@onReceive applyAction(action)
- }
- }
- }
+ abstract fun registerTreeActionSelectors(selector: SelectBuilder)
/**
* Will try to apply any immediately available actions in this action queue or any of our
* children's.
*
* Contrast this to [registerTreeActionSelectors] which will add select clauses that will await
- * the next action. That will also end up with [applyAction] being called when the clauses is
- * selected.
+ * the next action.
*
* @param skipDirtyNodes Whether or not this should skip over any workflow nodes that are already
* 'dirty' - that is, they had their own state changed as the result of a previous action before
@@ -246,152 +116,15 @@ internal class WorkflowNode(
* @return [ActionProcessingResult] of the action processed, or [ActionsExhausted] if there were
* none immediately available.
*/
- fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean = false): ActionProcessingResult {
- if (skipDirtyNodes && selfStateDirty) return ActionsExhausted
-
- val result = subtreeManager.applyNextAvailableChildAction(skipDirtyNodes)
-
- if (result == ActionsExhausted) {
- return eventActionsChannel.tryReceive().getOrNull()?.let { action ->
- applyAction(action)
- } ?: ActionsExhausted
- }
- return result
- }
+ abstract fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean = false): ActionProcessingResult
/**
* Cancels this state machine host, and any coroutines started as children of it.
*
- * This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an error to call [registerTreeActionSelectors]
- * after calling this method.
- */
- fun cancel(cause: CancellationException? = null) {
- coroutineContext.cancel(cause)
- lastRendering = NullableInitBox()
- }
-
- /**
- * Call this after we have been passed any workflow instance, in [render] or [snapshot]. It may
- * have changed and we should check to see if we need to update our cached instances.
- */
- private fun maybeUpdateCachedWorkflowInstance(
- workflow: StatefulWorkflow
- ) {
- if (workflow !== cachedWorkflowInstance) {
- // The instance has changed.
- cachedWorkflowInstance = workflow
- interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this)
- }
- }
-
- /**
- * Contains the actual logic for [render], after we've casted the passed-in [Workflow]'s
- * state type to our `StateT`.
- */
- private fun renderWithStateType(
- workflow: StatefulWorkflow,
- props: PropsT
- ): RenderingT {
- updatePropsAndState(props, workflow)
-
- if (!runtimeConfig.contains(PARTIAL_TREE_RENDERING) ||
- !lastRendering.isInitialized ||
- subtreeStateDirty
- ) {
- // If we haven't already updated the cached instance, better do it now!
- maybeUpdateCachedWorkflowInstance(workflow)
-
- baseRenderContext.unfreeze()
- lastRendering = NullableInitBox(interceptedWorkflowInstance.render(props, state, context))
- baseRenderContext.freeze()
-
- workflowTracer.trace("UpdateRuntimeTree") {
- // Tear down workflows and workers that are obsolete.
- subtreeManager.commitRenderedChildren()
- // Side effect jobs are launched lazily, since they can send actions to the sink, and can only
- // be started after context is frozen.
- sideEffects.forEachStaging { it.job.start() }
- sideEffects.commitStaging { it.job.cancel() }
- remembered.commitStaging { /* Nothing to clean up. */ }
- }
- // After we have rendered this subtree, we need another action in order for us to be
- // considered dirty again.
- subtreeStateDirty = false
- selfStateDirty = false
- }
-
- return lastRendering.getOrThrow()
- }
-
- /**
- * Update props if they have changed. If that happens, then check to see if we need
- * to update the cached workflow instance, then call [StatefulWorkflow.onPropsChanged] and
- * update the state from that. We consider any change to props as dirty because
- * the props themselves are used in [StatefulWorkflow.render] (they are the 'external' part of
- * the state) so we must re-render.
- */
- private fun updatePropsAndState(
- newProps: PropsT,
- workflow: StatefulWorkflow,
- ) {
- if (newProps != lastProps) {
- maybeUpdateCachedWorkflowInstance(workflow)
- val newState = interceptedWorkflowInstance.onPropsChanged(lastProps, newProps, state)
- state = newState
- subtreeStateDirty = true
- selfStateDirty = true
- }
- lastProps = newProps
- }
-
- /**
- * Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
- * via [emitAppliedActionToParent] to the parent, with additional information as to whether or
- * not this action has changed the current node's state.
- *
+ * This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an
+ * error to call [registerTreeActionSelectors] after calling this method.
*/
- private fun applyAction(
- action: WorkflowAction,
- childResult: ActionApplied<*>? = null
- ): ActionProcessingResult {
- val (newState: StateT, actionApplied: ActionApplied) = action.applyTo(lastProps, state)
- state = newState
- // Aggregate the action with the child result, if any.
- val aggregateActionApplied = actionApplied.copy(
- // Changing state is sticky, we pass it up if it ever changed.
- stateChanged = actionApplied.stateChanged || (childResult?.stateChanged ?: false)
- )
- // Our state changed.
- selfStateDirty = selfStateDirty || actionApplied.stateChanged
- // Our state changed or one of our children's state changed.
- subtreeStateDirty = subtreeStateDirty || aggregateActionApplied.stateChanged
- return if (actionApplied.output != null ||
- runtimeConfig.contains(PARTIAL_TREE_RENDERING)
- ) {
- // If we are using the optimization, always return to the parent, so we carry a path that
- // notes that the subtree did change all the way to the root.
- //
- // We don't need that without the optimization because there is nothing
- // to output from the root of the runtime -- the output has propagated
- // as far as it needs to causing all corresponding state changes.
- //
- // However, the root and the path down to the changed nodes must always
- // re-render now, so this is the implementation detail of how we get
- // subtreeStateDirty = true on that entire path to the root.
- emitAppliedActionToParent(aggregateActionApplied)
- } else {
- aggregateActionApplied
- }
- }
-
- private fun createSideEffectNode(
- key: String,
- sideEffect: suspend CoroutineScope.() -> Unit
- ): SideEffectNode {
- return workflowTracer.trace("CreateSideEffectNode") {
- val scope = this + CoroutineName("sideEffect[$key] for $id")
- val job = scope.launch(start = LAZY, block = sideEffect)
- SideEffectNode(key, job)
- }
+ open fun cancel(cause: CancellationException? = null) {
+ scope.cancel(cause)
}
}
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt
index 25e9b15f4..7761aacb3 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt
@@ -47,7 +47,7 @@ internal class WorkflowRunner(
private val propsChannel = props.dropWhile { it == currentProps }
.produceIn(scope)
- private val rootNode = WorkflowNode(
+ private val rootNode = createWorkflowNode(
id = workflow.id(),
workflow = workflow,
initialProps = currentProps,
@@ -66,11 +66,15 @@ internal class WorkflowRunner(
* between every subsequent call to [awaitAndApplyAction].
*/
fun nextRendering(): RenderingAndSnapshot {
- return interceptor.onRenderAndSnapshot(currentProps, { props ->
- val rendering = rootNode.render(workflow, props)
- val snapshot = rootNode.snapshot(workflow)
- RenderingAndSnapshot(rendering, snapshot)
- }, rootNode)
+ return interceptor.onRenderAndSnapshot(
+ renderProps = currentProps,
+ proceed = { props ->
+ val rendering = rootNode.render(workflow, props)
+ val snapshot = rootNode.snapshot()
+ RenderingAndSnapshot(rendering, snapshot)
+ },
+ session = rootNode.session,
+ )
}
/**
diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNodeTest.kt
similarity index 94%
rename from workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt
rename to workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNodeTest.kt
index 5658d59ee..574e4b902 100644
--- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt
+++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNodeTest.kt
@@ -55,7 +55,7 @@ import kotlin.test.assertTrue
import kotlin.test.fail
@Suppress("UNCHECKED_CAST")
-internal class WorkflowNodeTest {
+internal class StatefulWorkflowNodeTest {
abstract class StringWorkflow : StatefulWorkflow() {
override fun snapshotState(state: String): Snapshot = fail("not expected")
@@ -108,7 +108,7 @@ internal class WorkflowNodeTest {
oldAndNewProps += old to new
return@PropsRenderingWorkflow state
}
- val node = WorkflowNode(workflow.id(), workflow, "old", null, context)
+ val node = StatefulWorkflowNode(workflow.id(), workflow, "old", null, context)
node.render(workflow, "new")
@@ -121,7 +121,7 @@ internal class WorkflowNodeTest {
oldAndNewProps += old to new
return@PropsRenderingWorkflow state
}
- val node = WorkflowNode(workflow.id(), workflow, "old", null, context)
+ val node = StatefulWorkflowNode(workflow.id(), workflow, "old", null, context)
node.render(workflow, "old")
@@ -132,7 +132,7 @@ internal class WorkflowNodeTest {
val workflow = PropsRenderingWorkflow { old, new, _ ->
"$old->$new"
}
- val node = WorkflowNode(workflow.id(), workflow, "foo", null, context)
+ val node = StatefulWorkflowNode(workflow.id(), workflow, "foo", null, context)
val rendering = node.render(workflow, "foo2")
@@ -173,7 +173,7 @@ internal class WorkflowNodeTest {
return context.eventHandler("") { event -> setOutput(event) }
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow,
"",
@@ -215,7 +215,7 @@ internal class WorkflowNodeTest {
return context.eventHandler("") { event -> setOutput(event) }
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow,
"",
@@ -267,7 +267,7 @@ internal class WorkflowNodeTest {
return ""
}
}
- val node = WorkflowNode(workflow.id(), workflow, "", null, context)
+ val node = StatefulWorkflowNode(workflow.id(), workflow, "", null, context)
node.render(workflow, "")
sink.send(action("") { setOutput("event") })
@@ -284,7 +284,7 @@ internal class WorkflowNodeTest {
}
assertFalse(started)
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -305,7 +305,7 @@ internal class WorkflowNodeTest {
contextFromWorker = coroutineContext
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -314,7 +314,10 @@ internal class WorkflowNodeTest {
)
node.render(workflow.asStatefulWorkflow(), Unit)
- assertEquals(WorkflowNodeId(workflow).toString(), node.coroutineContext[CoroutineName]!!.name)
+ assertEquals(
+ WorkflowNodeId(workflow).toString(),
+ node.scope.coroutineContext[CoroutineName]!!.name
+ )
assertEquals(
"sideEffect[the key] for ${WorkflowNodeId(workflow)}",
contextFromWorker!![CoroutineName]!!.name
@@ -327,7 +330,7 @@ internal class WorkflowNodeTest {
actionSink.send(action("") { setOutput("result") })
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -359,7 +362,7 @@ internal class WorkflowNodeTest {
}
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = true,
@@ -388,7 +391,7 @@ internal class WorkflowNodeTest {
}
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -417,7 +420,7 @@ internal class WorkflowNodeTest {
}
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = 0,
@@ -445,7 +448,7 @@ internal class WorkflowNodeTest {
seenProps += props
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = 0,
@@ -469,7 +472,7 @@ internal class WorkflowNodeTest {
runningSideEffect("same") { fail() }
runningSideEffect("same") { fail() }
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -500,7 +503,7 @@ internal class WorkflowNodeTest {
if (props == 2) runningSideEffect("three", recordingSideEffect(events3))
}
.asStatefulWorkflow()
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow,
initialProps = 0,
@@ -536,7 +539,7 @@ internal class WorkflowNodeTest {
runningSideEffect("one") { started1 = true }
runningSideEffect("two") { started2 = true }
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -566,7 +569,7 @@ internal class WorkflowNodeTest {
}
}
)
- val originalNode = WorkflowNode(
+ val originalNode = StatefulWorkflowNode(
workflow.id(),
workflow,
initialProps = "initial props",
@@ -575,10 +578,10 @@ internal class WorkflowNodeTest {
)
assertEquals("initial props", originalNode.render(workflow, "foo"))
- val snapshot = originalNode.snapshot(workflow)
+ val snapshot = originalNode.snapshot()
assertNotEquals(0, snapshot.toByteString().size)
- val restoredNode = WorkflowNode(
+ val restoredNode = StatefulWorkflowNode(
workflow.id(),
workflow,
// These props should be ignored, since snapshot is non-null.
@@ -595,7 +598,7 @@ internal class WorkflowNodeTest {
render = { _, state -> state },
snapshot = { Snapshot.of("restored") }
)
- val originalNode = WorkflowNode(
+ val originalNode = StatefulWorkflowNode(
workflow.id(),
workflow,
initialProps = "initial props",
@@ -604,10 +607,10 @@ internal class WorkflowNodeTest {
)
assertEquals("initial props", originalNode.render(workflow, "foo"))
- val snapshot = originalNode.snapshot(workflow)
+ val snapshot = originalNode.snapshot()
assertNotEquals(0, snapshot.toByteString().size)
- val restoredNode = WorkflowNode(
+ val restoredNode = StatefulWorkflowNode(
workflow.id(),
workflow,
// These props should be ignored, since snapshot is non-null.
@@ -652,7 +655,7 @@ internal class WorkflowNodeTest {
}
)
- val originalNode = WorkflowNode(
+ val originalNode = StatefulWorkflowNode(
parentWorkflow.id(),
parentWorkflow,
initialProps = "initial props",
@@ -661,10 +664,10 @@ internal class WorkflowNodeTest {
)
assertEquals("initial props|child props", originalNode.render(parentWorkflow, "foo"))
- val snapshot = originalNode.snapshot(parentWorkflow)
+ val snapshot = originalNode.snapshot()
assertNotEquals(0, snapshot.toByteString().size)
- val restoredNode = WorkflowNode(
+ val restoredNode = StatefulWorkflowNode(
parentWorkflow.id(),
parentWorkflow,
// These props should be ignored, since snapshot is non-null.
@@ -695,13 +698,13 @@ internal class WorkflowNodeTest {
}
}
)
- val node = WorkflowNode(workflow.id(), workflow, Unit, null, Unconfined)
+ val node = StatefulWorkflowNode(workflow.id(), workflow, Unit, null, Unconfined)
assertEquals(0, snapshotCalls)
assertEquals(0, snapshotWrites)
assertEquals(0, restoreCalls)
- val snapshot = node.snapshot(workflow)
+ val snapshot = node.snapshot()
assertEquals(1, snapshotCalls)
assertEquals(0, snapshotWrites)
@@ -713,7 +716,7 @@ internal class WorkflowNodeTest {
assertEquals(1, snapshotWrites)
assertEquals(0, restoreCalls)
- WorkflowNode(workflow.id(), workflow, Unit, snapshot, Unconfined)
+ StatefulWorkflowNode(workflow.id(), workflow, Unit, snapshot, Unconfined)
assertEquals(1, snapshotCalls)
assertEquals(1, snapshotWrites)
@@ -732,7 +735,7 @@ internal class WorkflowNodeTest {
render = { _, state -> state },
snapshot = { state -> Snapshot.write { it.writeUtf8WithLength(state) } }
)
- val originalNode = WorkflowNode(
+ val originalNode = StatefulWorkflowNode(
workflow.id(),
workflow,
initialProps = "initial props",
@@ -741,10 +744,10 @@ internal class WorkflowNodeTest {
)
assertEquals("initial props", originalNode.render(workflow, "foo"))
- val snapshot = originalNode.snapshot(workflow)
+ val snapshot = originalNode.snapshot()
assertNotEquals(0, snapshot.toByteString().size)
- val restoredNode = WorkflowNode(
+ val restoredNode = StatefulWorkflowNode(
workflow.id(),
workflow,
initialProps = "new props",
@@ -756,7 +759,7 @@ internal class WorkflowNodeTest {
@Test fun toString_formats_as_WorkflowInstance_without_parent() {
val workflow = Workflow.rendering(Unit)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -774,7 +777,7 @@ internal class WorkflowNodeTest {
@Test fun toString_formats_as_WorkflowInstance_with_parent() {
val workflow = Workflow.rendering(Unit)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -807,7 +810,7 @@ internal class WorkflowNodeTest {
}
}
val workflow = Workflow.rendering(Unit)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -817,7 +820,7 @@ internal class WorkflowNodeTest {
parent = TestSession(42)
)
- assertSame(node.coroutineContext, interceptedScope.coroutineContext)
+ assertSame(node.scope.coroutineContext, interceptedScope.coroutineContext)
assertEquals(workflow.identifier, interceptedSession.identifier)
assertEquals(0, interceptedSession.sessionId)
assertEquals("foo", interceptedSession.renderKey)
@@ -848,7 +851,7 @@ internal class WorkflowNodeTest {
}
}
val workflow = Workflow.rendering(Unit)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -859,7 +862,7 @@ internal class WorkflowNodeTest {
parent = TestSession(42)
)
- assertSame(node.coroutineContext, interceptedScope.coroutineContext)
+ assertSame(node.scope.coroutineContext, interceptedScope.coroutineContext)
assertEquals(workflow.identifier, interceptedSession.identifier)
assertEquals(0, interceptedSession.sessionId)
assertEquals("foo", interceptedSession.renderKey)
@@ -895,7 +898,7 @@ internal class WorkflowNodeTest {
initialState = { props -> "state($props)" },
render = { _, _ -> fail() }
)
- WorkflowNode(
+ StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = "props",
@@ -941,7 +944,7 @@ internal class WorkflowNodeTest {
onPropsChanged = { old, new, state -> "onPropsChanged($old, $new, $state)" },
render = { _, state -> state }
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = "old",
@@ -987,7 +990,7 @@ internal class WorkflowNodeTest {
initialState = { "state" },
render = { props, state -> "render($props, $state)" }
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = "props",
@@ -1029,7 +1032,7 @@ internal class WorkflowNodeTest {
render = { _, state -> state },
snapshot = { state -> Snapshot.of("snapshot($state)") }
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = "old",
@@ -1038,7 +1041,7 @@ internal class WorkflowNodeTest {
baseContext = Unconfined,
parent = TestSession(42)
)
- val snapshot = node.snapshot(workflow)
+ val snapshot = node.snapshot()
assertEquals("state", interceptedState)
assertEquals(Snapshot.of("snapshot(state)"), interceptedSnapshot)
@@ -1070,7 +1073,7 @@ internal class WorkflowNodeTest {
render = { _, state -> state },
snapshot = { null }
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = workflow.id(key = "foo"),
workflow = workflow.asStatefulWorkflow(),
initialProps = "old",
@@ -1079,7 +1082,7 @@ internal class WorkflowNodeTest {
baseContext = Unconfined,
parent = TestSession(42)
)
- val snapshot = node.snapshot(workflow)
+ val snapshot = node.snapshot()
assertEquals("state", interceptedState)
assertNull(interceptedSnapshot)
@@ -1111,7 +1114,7 @@ internal class WorkflowNodeTest {
"root(${renderChild(leafWorkflow, props)})"
}
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
id = rootWorkflow.id(key = "foo"),
workflow = rootWorkflow.asStatefulWorkflow(),
initialProps = "props",
@@ -1131,7 +1134,7 @@ internal class WorkflowNodeTest {
val sink = eventHandler("eventHandler") { fail("Expected handler to fail.") }
sink()
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1159,7 +1162,7 @@ internal class WorkflowNodeTest {
val workflow = Workflow.stateless {
actionSink.send(TestAction())
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1186,7 +1189,7 @@ internal class WorkflowNodeTest {
}
}
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1211,7 +1214,7 @@ internal class WorkflowNodeTest {
val workflow = Workflow.stateless> {
actionSink.contraMap { action("") { setOutput(it) } }
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1238,7 +1241,7 @@ internal class WorkflowNodeTest {
val workflow = Workflow.stateless> {
actionSink.contraMap { action("") { setOutput(null) } }
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1269,7 +1272,7 @@ internal class WorkflowNodeTest {
return@stateful renderState
}
)
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1292,7 +1295,7 @@ internal class WorkflowNodeTest {
actionSink.send(action("") { setOutput("child:hello") })
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1318,7 +1321,7 @@ internal class WorkflowNodeTest {
actionSink.send(action("") { setOutput(null) })
}
}
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
workflow.asStatefulWorkflow(),
initialProps = Unit,
@@ -1343,7 +1346,7 @@ internal class WorkflowNodeTest {
}
val stateful = workflow.asStatefulWorkflow()
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
stateful,
initialProps = "",
@@ -1367,7 +1370,7 @@ internal class WorkflowNodeTest {
remember(key, typeOf(), input) { value }
}
val stateful = workflow.asStatefulWorkflow()
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
stateful,
initialProps = "",
@@ -1393,7 +1396,7 @@ internal class WorkflowNodeTest {
remember(key, returnType) { value }
}
val stateful = workflow.asStatefulWorkflow()
- val node = WorkflowNode(
+ val node = StatefulWorkflowNode(
workflow.id(),
stateful,
initialProps = "" to ("" as Any),