Skip to content

Commit c65938e

Browse files
Refactor WorkflowNode into AbstractWorkflowNode <- StatefulWorkflowNode.
This is to prepare for go/compose-based-workflows.
1 parent 22cf4a3 commit c65938e

File tree

7 files changed

+271
-133
lines changed

7 files changed

+271
-133
lines changed

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.squareup.workflow1
22

33
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
4+
import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopOutcome
45
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
56
import kotlinx.coroutines.CoroutineScope
67
import kotlinx.coroutines.Job
@@ -181,6 +182,8 @@ public interface WorkflowInterceptor {
181182
/**
182183
* Information about the session of a workflow in the runtime that a [WorkflowInterceptor] method
183184
* is intercepting.
185+
*
186+
* Implementations should override [toString] to call [WorkflowSession.workflowSessionToString].
184187
*/
185188
public interface WorkflowSession {
186189
/** The [WorkflowIdentifier] that represents the type of this workflow. */
@@ -406,6 +409,16 @@ internal fun <P, S, O, R> WorkflowInterceptor.intercept(
406409
}
407410
}
408411

412+
internal fun WorkflowSession.workflowSessionToString(): String {
413+
val parentDescription = parent?.let { "WorkflowInstance(…)" }
414+
return "WorkflowInstance(" +
415+
"identifier=$identifier, " +
416+
"renderKey=$renderKey, " +
417+
"instanceId=$sessionId, " +
418+
"parent=$parentDescription" +
419+
")"
420+
}
421+
409422
private class InterceptedRenderContext<P, S, O>(
410423
private val baseRenderContext: BaseRenderContext<P, S, O>,
411424
private val interceptor: RenderContextInterceptor<P, S, O>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.squareup.workflow1.internal
2+
3+
import com.squareup.workflow1.ActionApplied
4+
import com.squareup.workflow1.ActionProcessingResult
5+
import com.squareup.workflow1.NoopWorkflowInterceptor
6+
import com.squareup.workflow1.RuntimeConfig
7+
import com.squareup.workflow1.RuntimeConfigOptions
8+
import com.squareup.workflow1.TreeSnapshot
9+
import com.squareup.workflow1.Workflow
10+
import com.squareup.workflow1.WorkflowInterceptor
11+
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
12+
import com.squareup.workflow1.WorkflowTracer
13+
import kotlinx.coroutines.CancellationException
14+
import kotlinx.coroutines.CoroutineName
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Job
17+
import kotlinx.coroutines.cancel
18+
import kotlinx.coroutines.selects.SelectBuilder
19+
import kotlin.coroutines.CoroutineContext
20+
21+
internal fun <PropsT, OutputT, RenderingT> createWorkflowNode(
22+
id: WorkflowNodeId,
23+
workflow: Workflow<PropsT, OutputT, RenderingT>,
24+
initialProps: PropsT,
25+
snapshot: TreeSnapshot?,
26+
baseContext: CoroutineContext,
27+
// Providing default value so we don't need to specify in test.
28+
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
29+
workflowTracer: WorkflowTracer? = null,
30+
emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult = { it },
31+
parent: WorkflowSession? = null,
32+
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
33+
idCounter: IdCounter? = null
34+
): AbstractWorkflowNode<PropsT, OutputT, RenderingT> = StatefulWorkflowNode(
35+
id = id,
36+
workflow = workflow.asStatefulWorkflow(),
37+
initialProps = initialProps,
38+
snapshot = snapshot,
39+
baseContext = baseContext,
40+
runtimeConfig = runtimeConfig,
41+
workflowTracer = workflowTracer,
42+
emitAppliedActionToParent = emitAppliedActionToParent,
43+
parent = parent,
44+
interceptor = interceptor,
45+
idCounter = idCounter,
46+
)
47+
48+
internal abstract class AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
49+
val id: WorkflowNodeId,
50+
protected val interceptor: WorkflowInterceptor,
51+
protected val emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult,
52+
baseContext: CoroutineContext,
53+
) {
54+
55+
/**
56+
* Scope that has a job that will live as long as this node and be cancelled when [cancel] is
57+
* called.
58+
* Also adds a debug name to this coroutine based on its ID.
59+
*/
60+
val scope: CoroutineScope = CoroutineScope(
61+
baseContext +
62+
Job(parent = baseContext[Job]) +
63+
CoroutineName(id.toString())
64+
)
65+
66+
/**
67+
* The [WorkflowSession] that represents this node to [WorkflowInterceptor]s.
68+
*/
69+
abstract val session: WorkflowSession
70+
71+
/**
72+
* Walk the tree of workflows, rendering each one and using
73+
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
74+
* render themselves and aggregate those child renderings.
75+
*
76+
* @param workflow The "template" workflow instance used in the current render pass. This isn't
77+
* necessarily the same _instance_ every call, but will be the same _type_.
78+
*/
79+
abstract fun render(
80+
workflow: Workflow<PropsT, OutputT, RenderingT>,
81+
input: PropsT
82+
): RenderingT
83+
84+
/**
85+
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
86+
* automatically.
87+
*/
88+
abstract fun snapshot(): TreeSnapshot
89+
90+
/**
91+
* Gets the next [result][ActionProcessingResult] from the state machine. This will be an
92+
* [OutputT] or null.
93+
*
94+
* Walk the tree of state machines, asking each one to wait for its next event. If something
95+
* happen that results in an output, that output is returned. Null means something happened that
96+
* requires a re-render, e.g. my state changed or a child state changed.
97+
*
98+
* It is an error to call this method after calling [cancel].
99+
*
100+
* @return [Boolean] whether or not the queues were empty for this node and its children at the
101+
* time of suspending.
102+
*/
103+
abstract fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean
104+
105+
/**
106+
* Cancels this state machine host, and any coroutines started as children of it.
107+
*
108+
* This must be called when the caller will no longer call [onNextAction]. It is an error to call
109+
* [onNextAction] after calling this method.
110+
*/
111+
open fun cancel(cause: CancellationException? = null) {
112+
scope.cancel(cause)
113+
}
114+
}

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkflowNode.kt renamed to workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/StatefulWorkflowNode.kt

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.squareup.workflow1.intercept
2323
import com.squareup.workflow1.internal.RealRenderContext.RememberStore
2424
import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner
2525
import com.squareup.workflow1.trace
26+
import com.squareup.workflow1.workflowSessionToString
2627
import kotlinx.coroutines.CancellationException
2728
import kotlinx.coroutines.CoroutineName
2829
import kotlinx.coroutines.CoroutineScope
@@ -50,38 +51,42 @@ import kotlin.reflect.KType
5051
* structured concurrency).
5152
*/
5253
@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
53-
internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
54-
val id: WorkflowNodeId,
54+
internal class StatefulWorkflowNode<PropsT, StateT, OutputT, RenderingT>(
55+
id: WorkflowNodeId,
5556
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
5657
initialProps: PropsT,
5758
snapshot: TreeSnapshot?,
5859
baseContext: CoroutineContext,
5960
// Providing default value so we don't need to specify in test.
6061
override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
6162
override val workflowTracer: WorkflowTracer? = null,
62-
private val emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult =
63-
{ it },
63+
emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult = { it },
6464
override val parent: WorkflowSession? = null,
65-
private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
65+
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
6666
idCounter: IdCounter? = null
67-
) : CoroutineScope, SideEffectRunner, RememberStore, WorkflowSession {
67+
) : AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
68+
id = id,
69+
baseContext = baseContext,
70+
interceptor = interceptor,
71+
emitAppliedActionToParent = emitAppliedActionToParent,
72+
),
73+
SideEffectRunner,
74+
RememberStore,
75+
WorkflowSession {
6876

69-
/**
70-
* Context that has a job that will live as long as this node.
71-
* Also adds a debug name to this coroutine based on its ID.
72-
*/
73-
override val coroutineContext = baseContext + Job(baseContext[Job]) + CoroutineName(id.toString())
74-
75-
// WorkflowInstance properties
77+
// WorkflowSession properties
7678
override val identifier: WorkflowIdentifier get() = id.identifier
7779
override val renderKey: String get() = id.name
7880
override val sessionId: Long = idCounter.createId()
7981
private var cachedWorkflowInstance: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
8082
private var interceptedWorkflowInstance: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
8183

84+
override val session: WorkflowSession
85+
get() = this
86+
8287
private val subtreeManager = SubtreeManager(
8388
snapshotCache = snapshot?.childTreeSnapshots,
84-
contextForChildren = coroutineContext,
89+
contextForChildren = scope.coroutineContext,
8590
emitActionToParent = ::applyAction,
8691
runtimeConfig = runtimeConfig,
8792
workflowTracer = workflowTracer,
@@ -109,52 +114,54 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
109114
private val context = RenderContext(baseRenderContext, workflow)
110115

111116
init {
112-
interceptor.onSessionStarted(this, this)
117+
interceptor.onSessionStarted(workflowScope = scope, session = this)
113118

114119
cachedWorkflowInstance = workflow
115-
interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this)
116-
state = interceptedWorkflowInstance.initialState(initialProps, snapshot?.workflowSnapshot, this)
120+
interceptedWorkflowInstance = interceptor.intercept(
121+
workflow = cachedWorkflowInstance,
122+
workflowSession = this
123+
)
124+
state = interceptedWorkflowInstance.initialState(
125+
props = initialProps,
126+
snapshot = snapshot?.workflowSnapshot,
127+
workflowScope = scope
128+
)
117129
}
118130

119-
override fun toString(): String {
120-
val parentDescription = parent?.let { "WorkflowInstance(…)" }
121-
return "WorkflowInstance(" +
122-
"identifier=$identifier, " +
123-
"renderKey=$renderKey, " +
124-
"instanceId=$sessionId, " +
125-
"parent=$parentDescription" +
126-
")"
127-
}
131+
override fun toString(): String = workflowSessionToString()
128132

129133
/**
130134
* Walk the tree of workflows, rendering each one and using
131135
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
132136
* render themselves and aggregate those child renderings.
133137
*/
134138
@Suppress("UNCHECKED_CAST")
135-
fun render(
136-
workflow: StatefulWorkflow<PropsT, *, OutputT, RenderingT>,
139+
override fun render(
140+
workflow: Workflow<PropsT, OutputT, RenderingT>,
137141
input: PropsT
138-
): RenderingT =
139-
renderWithStateType(workflow as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>, input)
142+
): RenderingT = renderWithStateType(
143+
workflow = workflow.asStatefulWorkflow() as
144+
StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
145+
props = input
146+
)
140147

141148
/**
142149
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
143150
* automatically.
144151
*/
145-
fun snapshot(workflow: StatefulWorkflow<*, *, *, *>): TreeSnapshot {
146-
@Suppress("UNCHECKED_CAST")
147-
val typedWorkflow = workflow as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
148-
maybeUpdateCachedWorkflowInstance(typedWorkflow)
149-
return interceptor.onSnapshotStateWithChildren({
150-
val childSnapshots = subtreeManager.createChildSnapshots()
151-
val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
152-
TreeSnapshot(
153-
workflowSnapshot = rootSnapshot,
154-
// Create the snapshots eagerly since subtreeManager is mutable.
155-
childTreeSnapshots = { childSnapshots }
156-
)
157-
}, this)
152+
override fun snapshot(): TreeSnapshot {
153+
return interceptor.onSnapshotStateWithChildren(
154+
proceed = {
155+
val childSnapshots = subtreeManager.createChildSnapshots()
156+
val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
157+
TreeSnapshot(
158+
workflowSnapshot = rootSnapshot,
159+
// Create the snapshots eagerly since subtreeManager is mutable.
160+
childTreeSnapshots = { childSnapshots }
161+
)
162+
},
163+
session = this
164+
)
158165
}
159166

160167
override fun runningSideEffect(
@@ -212,7 +219,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
212219
* time of suspending.
213220
*/
214221
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
215-
fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
222+
override fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
216223
// Listen for any child workflow updates.
217224
var empty = subtreeManager.onNextChildAction(selector)
218225

@@ -230,11 +237,11 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
230237
/**
231238
* Cancels this state machine host, and any coroutines started as children of it.
232239
*
233-
* This must be called when the caller will no longer call [onNextAction]. It is an error to call [onNextAction]
234-
* after calling this method.
240+
* This must be called when the caller will no longer call [onNextAction]. It is an error to call
241+
* [onNextAction] after calling this method.
235242
*/
236-
fun cancel(cause: CancellationException? = null) {
237-
coroutineContext.cancel(cause)
243+
override fun cancel(cause: CancellationException?) {
244+
super.cancel(cause)
238245
lastRendering = NullableInitBox()
239246
}
240247

@@ -314,7 +321,6 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
314321
* Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
315322
* via [emitAppliedActionToParent] to the parent, with additional information as to whether or
316323
* not this action has changed the current node's state.
317-
*
318324
*/
319325
private fun applyAction(
320326
action: WorkflowAction<PropsT, StateT, OutputT>,
@@ -353,7 +359,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
353359
sideEffect: suspend CoroutineScope.() -> Unit
354360
): SideEffectNode {
355361
return workflowTracer.trace("CreateSideEffectNode") {
356-
val scope = this + CoroutineName("sideEffect[$key] for $id")
362+
val scope = scope + CoroutineName("sideEffect[$key] for $id")
357363
val job = scope.launch(start = LAZY, block = sideEffect)
358364
SideEffectNode(key, job)
359365
}

0 commit comments

Comments
 (0)