Skip to content

Commit 94ed465

Browse files
Refactor WorkflowNode into AbstractWorkflowNode <- StatefulWorkflowNode.
This is to prepare for go/compose-based-workflows.
1 parent 521c135 commit 94ed465

File tree

7 files changed

+288
-163
lines changed

7 files changed

+288
-163
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.RuntimeUpdate
45
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
56
import kotlinx.coroutines.CoroutineScope
67
import kotlinx.coroutines.Job
@@ -194,6 +195,8 @@ public interface WorkflowInterceptor {
194195
/**
195196
* Information about the session of a workflow in the runtime that a [WorkflowInterceptor] method
196197
* is intercepting.
198+
*
199+
* Implementations should override [toString] to call [WorkflowSession.workflowSessionToString].
197200
*/
198201
public interface WorkflowSession {
199202
/** The [WorkflowIdentifier] that represents the type of this workflow. */
@@ -419,6 +422,16 @@ internal fun <P, S, O, R> WorkflowInterceptor.intercept(
419422
}
420423
}
421424

425+
internal fun WorkflowSession.workflowSessionToString(): String {
426+
val parentDescription = parent?.let { "WorkflowInstance(…)" }
427+
return "WorkflowInstance(" +
428+
"identifier=$identifier, " +
429+
"renderKey=$renderKey, " +
430+
"instanceId=$sessionId, " +
431+
"parent=$parentDescription" +
432+
")"
433+
}
434+
422435
private class InterceptedRenderContext<P, S, O>(
423436
private val baseRenderContext: BaseRenderContext<P, S, O>,
424437
private val interceptor: RenderContextInterceptor<P, S, O>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package com.squareup.workflow1.internal
2+
3+
import com.squareup.workflow1.ActionApplied
4+
import com.squareup.workflow1.ActionProcessingResult
5+
import com.squareup.workflow1.ActionsExhausted
6+
import com.squareup.workflow1.NoopWorkflowInterceptor
7+
import com.squareup.workflow1.RuntimeConfig
8+
import com.squareup.workflow1.RuntimeConfigOptions
9+
import com.squareup.workflow1.TreeSnapshot
10+
import com.squareup.workflow1.Workflow
11+
import com.squareup.workflow1.WorkflowInterceptor
12+
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
13+
import com.squareup.workflow1.WorkflowTracer
14+
import kotlinx.coroutines.CancellationException
15+
import kotlinx.coroutines.CoroutineName
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.Job
18+
import kotlinx.coroutines.cancel
19+
import kotlinx.coroutines.selects.SelectBuilder
20+
import kotlin.coroutines.CoroutineContext
21+
22+
internal fun <PropsT, OutputT, RenderingT> createWorkflowNode(
23+
id: WorkflowNodeId,
24+
workflow: Workflow<PropsT, OutputT, RenderingT>,
25+
initialProps: PropsT,
26+
snapshot: TreeSnapshot?,
27+
baseContext: CoroutineContext,
28+
// Providing default value so we don't need to specify in test.
29+
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
30+
workflowTracer: WorkflowTracer? = null,
31+
emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult = { it },
32+
parent: WorkflowSession? = null,
33+
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
34+
idCounter: IdCounter? = null
35+
): AbstractWorkflowNode<PropsT, OutputT, RenderingT> = StatefulWorkflowNode(
36+
id = id,
37+
workflow = workflow.asStatefulWorkflow(),
38+
initialProps = initialProps,
39+
snapshot = snapshot,
40+
baseContext = baseContext,
41+
runtimeConfig = runtimeConfig,
42+
workflowTracer = workflowTracer,
43+
emitAppliedActionToParent = emitAppliedActionToParent,
44+
parent = parent,
45+
interceptor = interceptor,
46+
idCounter = idCounter,
47+
)
48+
49+
internal abstract class AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
50+
val id: WorkflowNodeId,
51+
protected val interceptor: WorkflowInterceptor,
52+
protected val emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult,
53+
baseContext: CoroutineContext,
54+
) {
55+
56+
/**
57+
* Scope that has a job that will live as long as this node and be cancelled when [cancel] is
58+
* called.
59+
* Also adds a debug name to this coroutine based on its ID.
60+
*/
61+
val scope: CoroutineScope = CoroutineScope(
62+
baseContext +
63+
Job(parent = baseContext[Job]) +
64+
CoroutineName(id.toString())
65+
)
66+
67+
/**
68+
* The [WorkflowSession] that represents this node to [WorkflowInterceptor]s.
69+
*/
70+
abstract val session: WorkflowSession
71+
72+
/**
73+
* Walk the tree of workflows, rendering each one and using
74+
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
75+
* render themselves and aggregate those child renderings.
76+
*
77+
* @param workflow The "template" workflow instance used in the current render pass. This isn't
78+
* necessarily the same _instance_ every call, but will be the same _type_.
79+
*/
80+
abstract fun render(
81+
workflow: Workflow<PropsT, OutputT, RenderingT>,
82+
input: PropsT
83+
): RenderingT
84+
85+
/**
86+
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
87+
* automatically.
88+
*/
89+
abstract fun snapshot(): TreeSnapshot
90+
91+
/**
92+
* Register select clauses for the next [result][ActionProcessingResult] from the state machine.
93+
*
94+
* Walk the tree of state machines, asking each one to wait for its next event. If something
95+
* happens 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+
* Contrast this to [applyNextAvailableTreeAction], which is used to check for an action
101+
* that is already available without waiting, and then _immediately_ apply it.
102+
*/
103+
abstract fun registerTreeActionSelectors(selector: SelectBuilder<ActionProcessingResult>)
104+
105+
/**
106+
* Will try to apply any immediately available actions in this action queue or any of our
107+
* children's.
108+
*
109+
* Contrast this to [registerTreeActionSelectors] which will add select clauses that will await
110+
* the next action.
111+
*
112+
* @param skipDirtyNodes Whether or not this should skip over any workflow nodes that are already
113+
* 'dirty' - that is, they had their own state changed as the result of a previous action before
114+
* the next render pass.
115+
*
116+
* @return [ActionProcessingResult] of the action processed, or [ActionsExhausted] if there were
117+
* none immediately available.
118+
*/
119+
abstract fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean = false): ActionProcessingResult
120+
121+
/**
122+
* Cancels this state machine host, and any coroutines started as children of it.
123+
*
124+
* This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an
125+
* error to call [registerTreeActionSelectors] after calling this method.
126+
*/
127+
open fun cancel(cause: CancellationException? = null) {
128+
scope.cancel(cause)
129+
}
130+
}

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: 58 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ import com.squareup.workflow1.intercept
2424
import com.squareup.workflow1.internal.RealRenderContext.RememberStore
2525
import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner
2626
import com.squareup.workflow1.trace
27+
import com.squareup.workflow1.workflowSessionToString
2728
import kotlinx.coroutines.CancellationException
2829
import kotlinx.coroutines.CoroutineName
2930
import kotlinx.coroutines.CoroutineScope
3031
import kotlinx.coroutines.CoroutineStart.LAZY
3132
import kotlinx.coroutines.Job
32-
import kotlinx.coroutines.cancel
3333
import kotlinx.coroutines.channels.Channel
3434
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
3535
import kotlinx.coroutines.launch
@@ -49,38 +49,42 @@ import kotlin.reflect.KType
4949
* structured concurrency).
5050
*/
5151
@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
52-
internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
53-
val id: WorkflowNodeId,
52+
internal class StatefulWorkflowNode<PropsT, StateT, OutputT, RenderingT>(
53+
id: WorkflowNodeId,
5454
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
5555
initialProps: PropsT,
5656
snapshot: TreeSnapshot?,
5757
baseContext: CoroutineContext,
5858
// Providing default value so we don't need to specify in test.
5959
override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
6060
override val workflowTracer: WorkflowTracer? = null,
61-
private val emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult =
62-
{ it },
61+
emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult = { it },
6362
override val parent: WorkflowSession? = null,
64-
private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
63+
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
6564
idCounter: IdCounter? = null
66-
) : CoroutineScope, SideEffectRunner, RememberStore, WorkflowSession {
67-
68-
/**
69-
* Context that has a job that will live as long as this node.
70-
* Also adds a debug name to this coroutine based on its ID.
71-
*/
72-
override val coroutineContext = baseContext + Job(baseContext[Job]) + CoroutineName(id.toString())
73-
74-
// WorkflowInstance properties
65+
) : AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
66+
id = id,
67+
baseContext = baseContext,
68+
interceptor = interceptor,
69+
emitAppliedActionToParent = emitAppliedActionToParent,
70+
),
71+
SideEffectRunner,
72+
RememberStore,
73+
WorkflowSession {
74+
75+
// WorkflowSession properties
7576
override val identifier: WorkflowIdentifier get() = id.identifier
7677
override val renderKey: String get() = id.name
7778
override val sessionId: Long = idCounter.createId()
7879
private var cachedWorkflowInstance: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
7980
private var interceptedWorkflowInstance: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
8081

82+
override val session: WorkflowSession
83+
get() = this
84+
8185
private val subtreeManager = SubtreeManager(
8286
snapshotCache = snapshot?.childTreeSnapshots,
83-
contextForChildren = coroutineContext,
87+
contextForChildren = scope.coroutineContext,
8488
emitActionToParent = ::applyAction,
8589
runtimeConfig = runtimeConfig,
8690
workflowTracer = workflowTracer,
@@ -117,52 +121,54 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
117121
private val context = RenderContext(baseRenderContext, workflow)
118122

119123
init {
120-
interceptor.onSessionStarted(this, this)
124+
interceptor.onSessionStarted(workflowScope = scope, session = this)
121125

122126
cachedWorkflowInstance = workflow
123-
interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this)
124-
state = interceptedWorkflowInstance.initialState(initialProps, snapshot?.workflowSnapshot, this)
127+
interceptedWorkflowInstance = interceptor.intercept(
128+
workflow = cachedWorkflowInstance,
129+
workflowSession = this
130+
)
131+
state = interceptedWorkflowInstance.initialState(
132+
props = initialProps,
133+
snapshot = snapshot?.workflowSnapshot,
134+
workflowScope = scope
135+
)
125136
}
126137

127-
override fun toString(): String {
128-
val parentDescription = parent?.let { "WorkflowInstance(…)" }
129-
return "WorkflowInstance(" +
130-
"identifier=$identifier, " +
131-
"renderKey=$renderKey, " +
132-
"instanceId=$sessionId, " +
133-
"parent=$parentDescription" +
134-
")"
135-
}
138+
override fun toString(): String = workflowSessionToString()
136139

137140
/**
138141
* Walk the tree of workflows, rendering each one and using
139142
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
140143
* render themselves and aggregate those child renderings.
141144
*/
142145
@Suppress("UNCHECKED_CAST")
143-
fun render(
144-
workflow: StatefulWorkflow<PropsT, *, OutputT, RenderingT>,
146+
override fun render(
147+
workflow: Workflow<PropsT, OutputT, RenderingT>,
145148
input: PropsT
146-
): RenderingT =
147-
renderWithStateType(workflow as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>, input)
149+
): RenderingT = renderWithStateType(
150+
workflow = workflow.asStatefulWorkflow() as
151+
StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
152+
props = input
153+
)
148154

149155
/**
150156
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
151157
* automatically.
152158
*/
153-
fun snapshot(workflow: StatefulWorkflow<*, *, *, *>): TreeSnapshot {
154-
@Suppress("UNCHECKED_CAST")
155-
val typedWorkflow = workflow as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
156-
maybeUpdateCachedWorkflowInstance(typedWorkflow)
157-
return interceptor.onSnapshotStateWithChildren({
158-
val childSnapshots = subtreeManager.createChildSnapshots()
159-
val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
160-
TreeSnapshot(
161-
workflowSnapshot = rootSnapshot,
162-
// Create the snapshots eagerly since subtreeManager is mutable.
163-
childTreeSnapshots = { childSnapshots }
164-
)
165-
}, this)
159+
override fun snapshot(): TreeSnapshot {
160+
return interceptor.onSnapshotStateWithChildren(
161+
proceed = {
162+
val childSnapshots = subtreeManager.createChildSnapshots()
163+
val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
164+
TreeSnapshot(
165+
workflowSnapshot = rootSnapshot,
166+
// Create the snapshots eagerly since subtreeManager is mutable.
167+
childTreeSnapshots = { childSnapshots }
168+
)
169+
},
170+
session = this
171+
)
166172
}
167173

168174
override fun runningSideEffect(
@@ -206,20 +212,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
206212
return result.lastCalculated as ResultT
207213
}
208214

209-
/**
210-
* Register select clauses for the next [result][ActionProcessingResult] from the state machine.
211-
*
212-
* Walk the tree of state machines, asking each one to wait for its next event. If something happen
213-
* that results in an output, that output is returned. Null means something happened that requires
214-
* a re-render, e.g. my state changed or a child state changed.
215-
*
216-
* It is an error to call this method after calling [cancel].
217-
*
218-
* Contrast this to [applyNextAvailableTreeAction], which is used to check for an action
219-
* that is already available without waiting, and then _immediately_ apply it.
220-
* The select clauses added here also call [applyAction] when one of them is selected.
221-
*/
222-
fun registerTreeActionSelectors(selector: SelectBuilder<ActionProcessingResult>) {
215+
override fun registerTreeActionSelectors(selector: SelectBuilder<ActionProcessingResult>) {
223216
// Listen for any child workflow updates.
224217
subtreeManager.registerChildActionSelectors(selector)
225218

@@ -231,22 +224,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
231224
}
232225
}
233226

234-
/**
235-
* Will try to apply any immediately available actions in this action queue or any of our
236-
* children's.
237-
*
238-
* Contrast this to [registerTreeActionSelectors] which will add select clauses that will await
239-
* the next action. That will also end up with [applyAction] being called when the clauses is
240-
* selected.
241-
*
242-
* @param skipDirtyNodes Whether or not this should skip over any workflow nodes that are already
243-
* 'dirty' - that is, they had their own state changed as the result of a previous action before
244-
* the next render pass.
245-
*
246-
* @return [ActionProcessingResult] of the action processed, or [ActionsExhausted] if there were
247-
* none immediately available.
248-
*/
249-
fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean = false): ActionProcessingResult {
227+
override fun applyNextAvailableTreeAction(skipDirtyNodes: Boolean): ActionProcessingResult {
250228
if (skipDirtyNodes && selfStateDirty) return ActionsExhausted
251229

252230
val result = subtreeManager.applyNextAvailableChildAction(skipDirtyNodes)
@@ -262,11 +240,11 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
262240
/**
263241
* Cancels this state machine host, and any coroutines started as children of it.
264242
*
265-
* This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an error to call [registerTreeActionSelectors]
266-
* after calling this method.
243+
* This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an
244+
* error to call [registerTreeActionSelectors] after calling this method.
267245
*/
268-
fun cancel(cause: CancellationException? = null) {
269-
coroutineContext.cancel(cause)
246+
override fun cancel(cause: CancellationException?) {
247+
super.cancel(cause)
270248
lastRendering = NullableInitBox()
271249
}
272250

@@ -348,7 +326,6 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
348326
* Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
349327
* via [emitAppliedActionToParent] to the parent, with additional information as to whether or
350328
* not this action has changed the current node's state.
351-
*
352329
*/
353330
private fun applyAction(
354331
action: WorkflowAction<PropsT, StateT, OutputT>,
@@ -389,7 +366,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
389366
sideEffect: suspend CoroutineScope.() -> Unit
390367
): SideEffectNode {
391368
return workflowTracer.trace("CreateSideEffectNode") {
392-
val scope = this + CoroutineName("sideEffect[$key] for $id")
369+
val scope = scope + CoroutineName("sideEffect[$key] for $id")
393370
val job = scope.launch(start = LAZY, block = sideEffect)
394371
SideEffectNode(key, job)
395372
}

0 commit comments

Comments
 (0)