From 94ed465404cc387aae1bdc40f4d216a43842f254 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Mon, 23 Jun 2025 17:30:15 -0700 Subject: [PATCH 1/2] Refactor WorkflowNode into AbstractWorkflowNode <- StatefulWorkflowNode. This is to prepare for go/compose-based-workflows. --- .../squareup/workflow1/WorkflowInterceptor.kt | 13 ++ .../internal/AbstractWorkflowNode.kt | 130 ++++++++++++++++ ...orkflowNode.kt => StatefulWorkflowNode.kt} | 139 ++++++++---------- .../workflow1/internal/SubtreeManager.kt | 21 ++- .../workflow1/internal/WorkflowChildNode.kt | 13 +- .../workflow1/internal/WorkflowRunner.kt | 16 +- ...odeTest.kt => StatefulWorkflowNodeTest.kt} | 119 +++++++-------- 7 files changed, 288 insertions(+), 163 deletions(-) create mode 100644 workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/AbstractWorkflowNode.kt rename workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/{WorkflowNode.kt => StatefulWorkflowNode.kt} (75%) rename workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/{WorkflowNodeTest.kt => StatefulWorkflowNodeTest.kt} (94%) 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 324ed39ae4..9035998718 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/AbstractWorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/AbstractWorkflowNode.kt new file mode 100644 index 0000000000..18df86a7c0 --- /dev/null +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/AbstractWorkflowNode.kt @@ -0,0 +1,130 @@ +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.RuntimeConfig +import com.squareup.workflow1.RuntimeConfigOptions +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.WorkflowTracer +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.selects.SelectBuilder +import kotlin.coroutines.CoroutineContext + +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. + runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG, + workflowTracer: WorkflowTracer? = null, + emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult = { it }, + parent: WorkflowSession? = null, + interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, + idCounter: IdCounter? = null +): AbstractWorkflowNode = 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 AbstractWorkflowNode( + val id: WorkflowNodeId, + protected val interceptor: WorkflowInterceptor, + protected val emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult, + baseContext: CoroutineContext, +) { + + /** + * 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. + */ + val scope: CoroutineScope = CoroutineScope( + baseContext + + Job(parent = baseContext[Job]) + + CoroutineName(id.toString()) + ) + + /** + * The [WorkflowSession] that represents this node to [WorkflowInterceptor]s. + */ + 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_. + */ + abstract fun render( + workflow: Workflow, + input: PropsT + ): RenderingT + + /** + * Walk the tree of state machines again, this time gathering snapshots and aggregating them + * automatically. + */ + 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 + * 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. + */ + 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. + * + * @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 + * the next render pass. + * + * @return [ActionProcessingResult] of the action processed, or [ActionsExhausted] if there were + * none immediately available. + */ + 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. + */ + open fun cancel(cause: CancellationException? = null) { + scope.cancel(cause) + } +} diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt similarity index 75% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt index 22b2d5d31c..b8f195b54b 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt @@ -24,12 +24,12 @@ 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.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.launch @@ -49,8 +49,8 @@ import kotlin.reflect.KType * structured concurrency). */ @OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class) -internal class WorkflowNode( - val id: WorkflowNodeId, +internal class StatefulWorkflowNode( + id: WorkflowNodeId, workflow: StatefulWorkflow, initialProps: PropsT, snapshot: TreeSnapshot?, @@ -58,29 +58,33 @@ internal class WorkflowNode( // 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 }, + emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult = { it }, override val parent: WorkflowSession? = null, - private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, + interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, idCounter: IdCounter? = null -) : CoroutineScope, SideEffectRunner, RememberStore, WorkflowSession { - - /** - * Context that has a job that will live as long as this node. - * Also adds a debug name to this coroutine based on its ID. - */ - override val coroutineContext = baseContext + Job(baseContext[Job]) + CoroutineName(id.toString()) - - // WorkflowInstance properties +) : AbstractWorkflowNode( + 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 = coroutineContext, + contextForChildren = scope.coroutineContext, emitActionToParent = ::applyAction, runtimeConfig = runtimeConfig, workflowTracer = workflowTracer, @@ -117,22 +121,21 @@ internal class WorkflowNode( private val context = RenderContext(baseRenderContext, workflow) init { - interceptor.onSessionStarted(this, this) + interceptor.onSessionStarted(workflowScope = scope, session = this) cachedWorkflowInstance = workflow - interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this) - state = interceptedWorkflowInstance.initialState(initialProps, snapshot?.workflowSnapshot, this) + interceptedWorkflowInstance = interceptor.intercept( + workflow = cachedWorkflowInstance, + workflowSession = this + ) + state = interceptedWorkflowInstance.initialState( + props = initialProps, + snapshot = snapshot?.workflowSnapshot, + workflowScope = scope + ) } - override fun toString(): String { - val parentDescription = parent?.let { "WorkflowInstance(…)" } - return "WorkflowInstance(" + - "identifier=$identifier, " + - "renderKey=$renderKey, " + - "instanceId=$sessionId, " + - "parent=$parentDescription" + - ")" - } + override fun toString(): String = workflowSessionToString() /** * Walk the tree of workflows, rendering each one and using @@ -140,29 +143,32 @@ internal class WorkflowNode( * render themselves and aggregate those child renderings. */ @Suppress("UNCHECKED_CAST") - fun render( - workflow: StatefulWorkflow, + override fun render( + workflow: Workflow, input: PropsT - ): RenderingT = - renderWithStateType(workflow as StatefulWorkflow, input) + ): RenderingT = renderWithStateType( + workflow = workflow.asStatefulWorkflow() as + StatefulWorkflow, + props = input + ) /** * 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 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( @@ -206,20 +212,7 @@ internal class WorkflowNode( return result.lastCalculated as ResultT } - /** - * 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. - * - * 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) { + override fun registerTreeActionSelectors(selector: SelectBuilder) { // Listen for any child workflow updates. subtreeManager.registerChildActionSelectors(selector) @@ -231,22 +224,7 @@ internal class WorkflowNode( } } - /** - * 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. - * - * @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 - * the next render pass. - * - * @return [ActionProcessingResult] of the action processed, or [ActionsExhausted] if there were - * none immediately available. - */ - fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean = false): ActionProcessingResult { + override fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean): ActionProcessingResult { if (skipDirtyNodes && selfStateDirty) return ActionsExhausted val result = subtreeManager.applyNextAvailableChildAction(skipDirtyNodes) @@ -262,11 +240,11 @@ internal class WorkflowNode( /** * 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. + * 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) + override fun cancel(cause: CancellationException?) { + super.cancel(cause) lastRendering = NullableInitBox() } @@ -348,7 +326,6 @@ internal class WorkflowNode( * 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, @@ -389,7 +366,7 @@ internal class WorkflowNode( sideEffect: suspend CoroutineScope.() -> Unit ): SideEffectNode { return workflowTracer.trace("CreateSideEffectNode") { - val scope = this + CoroutineName("sideEffect[$key] for $id") + 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 7cf7ab163b..62c0010ea9 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 @@ -20,14 +20,14 @@ import kotlin.coroutines.CoroutineContext * Responsible for tracking child workflows, starting them and tearing them down when necessary. * Also manages restoring children from snapshots. * - * Child workflows are stored in [WorkflowChildNode]s, which associate the child's [WorkflowNode] + * Child workflows are stored in [WorkflowChildNode]s, which associate the child's [AbstractWorkflowNode] * with its output handler. * * ## 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,11 +143,11 @@ internal class SubtreeManager( ) } stagedChild.setHandler(handler) - return stagedChild.render(child.asStatefulWorkflow(), props) + return stagedChild.render(child, props) } /** - * Uses [selector] to invoke [WorkflowNode.registerTreeActionSelectors] for every running child + * Uses [selector] to invoke [AbstractWorkflowNode.registerTreeActionSelectors] for every running child * workflow this instance is managing. */ fun registerChildActionSelectors(selector: SelectBuilder) { @@ -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 ea2d468766..329174c5bd 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 @@ -10,7 +9,7 @@ import com.squareup.workflow1.trace /** * Representation of a child workflow that has been rendered by another workflow. * - * Associates the child's [WorkflowNode] (which includes the key passed to `renderChild`) with the + * Associates the child's [AbstractWorkflowNode] (which includes the key passed to `renderChild`) with the * output handler function that was passed to `renderChild`. */ internal class WorkflowChildNode< @@ -22,11 +21,11 @@ internal class WorkflowChildNode< >( val workflow: Workflow<*, ChildOutputT, *>, private var handler: (ChildOutputT) -> WorkflowAction, - val workflowNode: WorkflowNode + val workflowNode: AbstractWorkflowNode ) : InlineListNode> { override var nextListNode: WorkflowChildNode<*, *, *, *, *>? = null - /** The [WorkflowNode]'s [WorkflowNodeId]. */ + /** The [AbstractWorkflowNode]'s [WorkflowNodeId]. */ val id get() = workflowNode.id /** @@ -48,15 +47,15 @@ internal class WorkflowChildNode< } /** - * Wrapper around [WorkflowNode.render] that allows calling it with erased types. + * Wrapper around [AbstractWorkflowNode.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/WorkflowRunner.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowRunner.kt index 25e9b15f49..7761aacb31 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 5658d59ee0..574e4b9026 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), From 05cad4438217a2cc6a0b7aabb7bccafe01a25205 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Mon, 14 Jul 2025 16:48:15 -0700 Subject: [PATCH 2/2] Rename AbstractWorkflowNode to WorkflowNode. This intermediate rename was to help git show the correct history for StatefulWorkflowNode. --- .../squareup/workflow1/internal/StatefulWorkflowNode.kt | 2 +- .../com/squareup/workflow1/internal/SubtreeManager.kt | 4 ++-- .../com/squareup/workflow1/internal/WorkflowChildNode.kt | 8 ++++---- .../internal/{AbstractWorkflowNode.kt => WorkflowNode.kt} | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/{AbstractWorkflowNode.kt => WorkflowNode.kt} (97%) 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 index b8f195b54b..aa09861d09 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt @@ -62,7 +62,7 @@ internal class StatefulWorkflowNode( override val parent: WorkflowSession? = null, interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, idCounter: IdCounter? = null -) : AbstractWorkflowNode( +) : WorkflowNode( id = id, baseContext = baseContext, interceptor = interceptor, 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 62c0010ea9..60d79ccf51 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 @@ -20,7 +20,7 @@ import kotlin.coroutines.CoroutineContext * Responsible for tracking child workflows, starting them and tearing them down when necessary. * Also manages restoring children from snapshots. * - * Child workflows are stored in [WorkflowChildNode]s, which associate the child's [AbstractWorkflowNode] + * Child workflows are stored in [WorkflowChildNode]s, which associate the child's [WorkflowNode] * with its output handler. * * ## Rendering @@ -147,7 +147,7 @@ internal class SubtreeManager( } /** - * Uses [selector] to invoke [AbstractWorkflowNode.registerTreeActionSelectors] for every running child + * Uses [selector] to invoke [WorkflowNode.registerTreeActionSelectors] for every running child * workflow this instance is managing. */ fun registerChildActionSelectors(selector: SelectBuilder) { 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 329174c5bd..87648454d4 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 @@ -9,7 +9,7 @@ import com.squareup.workflow1.trace /** * Representation of a child workflow that has been rendered by another workflow. * - * Associates the child's [AbstractWorkflowNode] (which includes the key passed to `renderChild`) with the + * Associates the child's [WorkflowNode] (which includes the key passed to `renderChild`) with the * output handler function that was passed to `renderChild`. */ internal class WorkflowChildNode< @@ -21,11 +21,11 @@ internal class WorkflowChildNode< >( val workflow: Workflow<*, ChildOutputT, *>, private var handler: (ChildOutputT) -> WorkflowAction, - val workflowNode: AbstractWorkflowNode + val workflowNode: WorkflowNode ) : InlineListNode> { override var nextListNode: WorkflowChildNode<*, *, *, *, *>? = null - /** The [AbstractWorkflowNode]'s [WorkflowNodeId]. */ + /** The [WorkflowNode]'s [WorkflowNodeId]. */ val id get() = workflowNode.id /** @@ -47,7 +47,7 @@ internal class WorkflowChildNode< } /** - * Wrapper around [AbstractWorkflowNode.render] that allows calling it with erased types. + * Wrapper around [WorkflowNode.render] that allows calling it with erased types. */ fun render( workflow: Workflow<*, *, *>, diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/AbstractWorkflowNode.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt similarity index 97% rename from workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/AbstractWorkflowNode.kt rename to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt index 18df86a7c0..1253492875 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/AbstractWorkflowNode.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt @@ -32,7 +32,7 @@ internal fun createWorkflowNode( parent: WorkflowSession? = null, interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, idCounter: IdCounter? = null -): AbstractWorkflowNode = StatefulWorkflowNode( +): WorkflowNode = StatefulWorkflowNode( id = id, workflow = workflow.asStatefulWorkflow(), initialProps = initialProps, @@ -46,7 +46,7 @@ internal fun createWorkflowNode( idCounter = idCounter, ) -internal abstract class AbstractWorkflowNode( +internal abstract class WorkflowNode( val id: WorkflowNodeId, protected val interceptor: WorkflowInterceptor, protected val emitAppliedActionToParent: (ActionApplied) -> ActionProcessingResult,