Skip to content

Commit fbc9cf7

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

File tree

7 files changed

+320
-126
lines changed

7 files changed

+320
-126
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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.WorkflowIdentifier
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> = when (workflow) {
36+
// is ComposeWorkflow<*, *, *> -> ComposeWorkflowNode(
37+
// id = id,
38+
// workflow = workflow,
39+
// initialProps = initialProps,
40+
// snapshot = snapshot,
41+
// baseContext = baseContext,
42+
// runtimeConfig = runtimeConfig,
43+
// workflowTracer = workflowTracer,
44+
// emitAppliedActionToParent = emitAppliedActionToParent,
45+
// parent = parent,
46+
// interceptor = interceptor,
47+
// idCounter = idCounter,
48+
// )
49+
50+
else -> StatefulWorkflowNode(
51+
id = id,
52+
workflow = workflow.asStatefulWorkflow(),
53+
initialProps = initialProps,
54+
snapshot = snapshot,
55+
baseContext = baseContext,
56+
runtimeConfig = runtimeConfig,
57+
workflowTracer = workflowTracer,
58+
emitAppliedActionToParent = emitAppliedActionToParent,
59+
parent = parent,
60+
interceptor = interceptor,
61+
idCounter = idCounter,
62+
)
63+
}
64+
65+
internal abstract class AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
66+
val id: WorkflowNodeId,
67+
final override val parent: WorkflowSession?,
68+
final override val workflowTracer: WorkflowTracer?,
69+
final override val runtimeConfig: RuntimeConfig,
70+
protected val interceptor: WorkflowInterceptor,
71+
protected val emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult,
72+
baseContext: CoroutineContext,
73+
idCounter: IdCounter?,
74+
) : WorkflowSession,
75+
CoroutineScope {
76+
77+
/**
78+
* Context that has a job that will live as long as this node.
79+
* Also adds a debug name to this coroutine based on its ID.
80+
*/
81+
final override val coroutineContext = baseContext +
82+
Job(baseContext[Job]) +
83+
CoroutineName(id.toString())
84+
85+
// WorkflowSession properties
86+
final override val identifier: WorkflowIdentifier get() = id.identifier
87+
final override val renderKey: String get() = id.name
88+
final override val sessionId: Long = idCounter.createId()
89+
90+
final override fun toString(): String {
91+
val parentDescription = parent?.let { "WorkflowInstance(…)" }
92+
return "WorkflowInstance(" +
93+
"identifier=$identifier, " +
94+
"renderKey=$renderKey, " +
95+
"instanceId=$sessionId, " +
96+
"parent=$parentDescription" +
97+
")"
98+
}
99+
100+
/**
101+
* Walk the tree of workflows, rendering each one and using
102+
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
103+
* render themselves and aggregate those child renderings.
104+
*
105+
* @param workflow The "template" workflow instance used in the current render pass. This isn't
106+
* necessarily the same _instance_ every call, but will be the same _type_.
107+
*/
108+
abstract fun render(
109+
workflow: Workflow<PropsT, OutputT, RenderingT>,
110+
input: PropsT
111+
): RenderingT
112+
113+
/**
114+
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
115+
* automatically.
116+
*/
117+
abstract fun snapshot(): TreeSnapshot
118+
119+
/**
120+
* Gets the next [result][ActionProcessingResult] from the state machine. This will be an
121+
* [OutputT] or null.
122+
*
123+
* Walk the tree of state machines, asking each one to wait for its next event. If something happen
124+
* that results in an output, that output is returned. Null means something happened that requires
125+
* a re-render, e.g. my state changed or a child state changed.
126+
*
127+
* It is an error to call this method after calling [cancel].
128+
*
129+
* @return [Boolean] whether or not the queues were empty for this node and its children at the
130+
* time of suspending.
131+
*/
132+
abstract fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean
133+
134+
/**
135+
* Cancels this state machine host, and any coroutines started as children of it.
136+
*
137+
* This must be called when the caller will no longer call [onNextAction]. It is an error to call [onNextAction]
138+
* after calling this method.
139+
*/
140+
open fun cancel(cause: CancellationException? = null) {
141+
coroutineContext.cancel(cause)
142+
}
143+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.selects.SelectBuilder
15+
import kotlin.coroutines.CoroutineContext
16+
17+
internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
18+
id: WorkflowNodeId,
19+
// workflow: ComposeWorkflow<PropsT, OutputT, RenderingT>,
20+
initialProps: PropsT,
21+
snapshot: TreeSnapshot?,
22+
baseContext: CoroutineContext,
23+
// Providing default value so we don't need to specify in test.
24+
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
25+
workflowTracer: WorkflowTracer? = null,
26+
emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult = { it },
27+
parent: WorkflowSession? = null,
28+
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
29+
idCounter: IdCounter? = null
30+
) : AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
31+
id = id,
32+
runtimeConfig = runtimeConfig,
33+
workflowTracer = workflowTracer,
34+
parent = parent,
35+
baseContext = baseContext,
36+
idCounter = idCounter,
37+
interceptor = interceptor,
38+
emitAppliedActionToParent = emitAppliedActionToParent,
39+
) {
40+
41+
init {
42+
interceptor.onSessionStarted(workflowScope = this, session = this)
43+
}
44+
45+
override fun render(
46+
workflow: Workflow<PropsT, OutputT, RenderingT>,
47+
input: PropsT
48+
): RenderingT {
49+
TODO("Not yet implemented")
50+
}
51+
52+
override fun snapshot(): TreeSnapshot {
53+
TODO("Not yet implemented")
54+
}
55+
56+
override fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
57+
TODO("Not yet implemented")
58+
}
59+
60+
override fun cancel(cause: CancellationException?) {
61+
TODO("Not yet implemented")
62+
}
63+
}

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: 43 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import com.squareup.workflow1.Workflow
1414
import com.squareup.workflow1.WorkflowAction
1515
import com.squareup.workflow1.WorkflowExperimentalApi
1616
import com.squareup.workflow1.WorkflowExperimentalRuntime
17-
import com.squareup.workflow1.WorkflowIdentifier
1817
import com.squareup.workflow1.WorkflowInterceptor
1918
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
2019
import com.squareup.workflow1.WorkflowTracer
@@ -50,32 +49,32 @@ import kotlin.reflect.KType
5049
* structured concurrency).
5150
*/
5251
@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
53-
internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
54-
val id: WorkflowNodeId,
52+
internal class StatefulWorkflowNode<PropsT, StateT, OutputT, RenderingT>(
53+
id: WorkflowNodeId,
5554
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
5655
initialProps: PropsT,
5756
snapshot: TreeSnapshot?,
5857
baseContext: CoroutineContext,
5958
// Providing default value so we don't need to specify in test.
60-
override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
61-
override val workflowTracer: WorkflowTracer? = null,
62-
private val emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult =
63-
{ it },
64-
override val parent: WorkflowSession? = null,
65-
private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
59+
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
60+
workflowTracer: WorkflowTracer? = null,
61+
emitAppliedActionToParent: (ActionApplied<OutputT>) -> ActionProcessingResult = { it },
62+
parent: WorkflowSession? = null,
63+
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
6664
idCounter: IdCounter? = null
67-
) : CoroutineScope, SideEffectRunner, RememberStore, WorkflowSession {
65+
) : AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
66+
id = id,
67+
runtimeConfig = runtimeConfig,
68+
workflowTracer = workflowTracer,
69+
parent = parent,
70+
baseContext = baseContext,
71+
idCounter = idCounter,
72+
interceptor = interceptor,
73+
emitAppliedActionToParent = emitAppliedActionToParent,
74+
),
75+
SideEffectRunner,
76+
RememberStore {
6877

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
76-
override val identifier: WorkflowIdentifier get() = id.identifier
77-
override val renderKey: String get() = id.name
78-
override val sessionId: Long = idCounter.createId()
7978
private var cachedWorkflowInstance: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
8079
private var interceptedWorkflowInstance: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
8180

@@ -116,45 +115,37 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
116115
state = interceptedWorkflowInstance.initialState(initialProps, snapshot?.workflowSnapshot, this)
117116
}
118117

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-
}
128-
129118
/**
130119
* Walk the tree of workflows, rendering each one and using
131120
* [RenderContext][com.squareup.workflow1.BaseRenderContext] to give its children a chance to
132121
* render themselves and aggregate those child renderings.
133122
*/
134123
@Suppress("UNCHECKED_CAST")
135-
fun render(
136-
workflow: StatefulWorkflow<PropsT, *, OutputT, RenderingT>,
124+
override fun render(
125+
workflow: Workflow<PropsT, OutputT, RenderingT>,
137126
input: PropsT
138-
): RenderingT =
139-
renderWithStateType(workflow as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>, input)
127+
): RenderingT = renderWithStateType(
128+
workflow = workflow.asStatefulWorkflow() as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
129+
props = input
130+
)
140131

141132
/**
142133
* Walk the tree of state machines again, this time gathering snapshots and aggregating them
143134
* automatically.
144135
*/
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)
136+
override fun snapshot(): TreeSnapshot {
137+
return interceptor.onSnapshotStateWithChildren(
138+
proceed = {
139+
val childSnapshots = subtreeManager.createChildSnapshots()
140+
val rootSnapshot = interceptedWorkflowInstance.snapshotState(state)
141+
TreeSnapshot(
142+
workflowSnapshot = rootSnapshot,
143+
// Create the snapshots eagerly since subtreeManager is mutable.
144+
childTreeSnapshots = { childSnapshots }
145+
)
146+
},
147+
session = this
148+
)
158149
}
159150

160151
override fun runningSideEffect(
@@ -212,7 +203,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
212203
* time of suspending.
213204
*/
214205
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
215-
fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
206+
override fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
216207
// Listen for any child workflow updates.
217208
var empty = subtreeManager.onNextChildAction(selector)
218209

@@ -230,11 +221,11 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
230221
/**
231222
* Cancels this state machine host, and any coroutines started as children of it.
232223
*
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.
224+
* This must be called when the caller will no longer call [onNextAction]. It is an error to call
225+
* [onNextAction] after calling this method.
235226
*/
236-
fun cancel(cause: CancellationException? = null) {
237-
coroutineContext.cancel(cause)
227+
override fun cancel(cause: CancellationException?) {
228+
super.cancel(cause)
238229
lastRendering = NullableInitBox()
239230
}
240231

@@ -314,7 +305,6 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
314305
* Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
315306
* via [emitAppliedActionToParent] to the parent, with additional information as to whether or
316307
* not this action has changed the current node's state.
317-
*
318308
*/
319309
private fun applyAction(
320310
action: WorkflowAction<PropsT, StateT, OutputT>,

0 commit comments

Comments
 (0)