Skip to content

Commit 4f55451

Browse files
Pass acceptOutput function to WorkflowNode constructor instead of every tick pass.
This doesn't change any behavior, it is just more efficient: there's no need to define the function at the tick pass, so this is not only more efficient but also just easier to read, since there are fewer moving parts. This is also required for #910.
1 parent 6f59dc0 commit 4f55451

File tree

5 files changed

+224
-62
lines changed

5 files changed

+224
-62
lines changed

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import kotlin.coroutines.CoroutineContext
9292
*/
9393
internal class SubtreeManager<StateT, OutputT : Any>(
9494
private val contextForChildren: CoroutineContext,
95+
private val emitActionToParent: (WorkflowAction<StateT, OutputT>) -> Any?,
9596
private val parentDiagnosticId: Long,
9697
private val diagnosticListener: WorkflowDiagnosticListener? = null,
9798
private val idCounter: IdCounter? = null
@@ -150,16 +151,9 @@ internal class SubtreeManager<StateT, OutputT : Any>(
150151
* Uses [selector] to invoke [WorkflowNode.tick] for every running child workflow this instance
151152
* is managing.
152153
*/
153-
fun <T : Any> tickChildren(
154-
selector: SelectBuilder<T?>,
155-
handler: (WorkflowAction<StateT, OutputT>) -> T?
156-
) {
154+
fun <T : Any> tickChildren(selector: SelectBuilder<T?>) {
157155
children.forEachActive { child ->
158-
child.workflowNode.tick(selector) { output ->
159-
val componentUpdate = child.acceptChildOutput(output)
160-
@Suppress("UNCHECKED_CAST")
161-
return@tick handler(componentUpdate as WorkflowAction<StateT, OutputT>)
162-
}
156+
child.workflowNode.tick(selector)
163157
}
164158
}
165159

@@ -187,16 +181,25 @@ internal class SubtreeManager<StateT, OutputT : Any>(
187181
handler: (ChildOutputT) -> WorkflowAction<StateT, OutputT>
188182
): WorkflowChildNode<ChildPropsT, ChildOutputT, StateT, OutputT> {
189183
val id = child.id(key)
184+
lateinit var node: WorkflowChildNode<ChildPropsT, ChildOutputT, StateT, OutputT>
185+
186+
fun acceptChildOutput(output: ChildOutputT): Any? {
187+
val action = node.acceptChildOutput(output)
188+
return emitActionToParent(action)
189+
}
190+
190191
val workflowNode = WorkflowNode(
191192
id,
192193
child.asStatefulWorkflow(),
193194
initialProps,
194195
snapshotCache[id],
195196
contextForChildren,
197+
::acceptChildOutput,
196198
parentDiagnosticId,
197199
diagnosticListener,
198200
idCounter
199201
)
200202
return WorkflowChildNode(child, handler, workflowNode)
203+
.also { node = it }
201204
}
202205
}

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ internal open class RealWorkflowLoop : WorkflowLoop {
121121
}
122122

123123
// Tick the workflow tree.
124-
rootNode.tick(this) { it }
124+
rootNode.tick(this)
125125
}
126126
}
127127
// Compiler gets confused, and thinks both that this throw is unreachable, and without the

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import com.squareup.workflow.diagnostic.createId
2828
import com.squareup.workflow.internal.RealRenderContext.WorkerRunner
2929
import kotlinx.coroutines.CoroutineName
3030
import kotlinx.coroutines.CoroutineScope
31-
import kotlinx.coroutines.InternalCoroutinesApi
3231
import kotlinx.coroutines.Job
3332
import kotlinx.coroutines.cancel
3433
import kotlinx.coroutines.channels.Channel
@@ -40,6 +39,8 @@ import kotlin.coroutines.CoroutineContext
4039
/**
4140
* A node in a state machine tree. Manages the actual state for a given [Workflow].
4241
*
42+
* @param emitOutputToParent A function that this node will call when it needs to emit an output
43+
* value to its parent. Returns either the output to be emitted from the root workflow, or null.
4344
* @param initialState Allows unit tests to start the node from a given state, instead of calling
4445
* [StatefulWorkflow.initialState].
4546
*/
@@ -50,6 +51,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
5051
initialProps: PropsT,
5152
snapshot: ByteString?,
5253
baseContext: CoroutineContext,
54+
private val emitOutputToParent: (OutputT) -> Any? = { it },
5355
parentDiagnosticId: Long? = null,
5456
private val diagnosticListener: WorkflowDiagnosticListener? = null,
5557
private val idCounter: IdCounter? = null,
@@ -69,8 +71,9 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
6971
*/
7072
internal val diagnosticId = idCounter.createId()
7173

72-
private val subtreeManager =
73-
SubtreeManager<StateT, OutputT>(coroutineContext, diagnosticId, diagnosticListener, idCounter)
74+
private val subtreeManager = SubtreeManager<StateT, OutputT>(
75+
coroutineContext, ::applyAction, diagnosticId, diagnosticListener, idCounter
76+
)
7477

7578
private val workers = ActiveStagingList<WorkerChildNode<*, *, *>>()
7679

@@ -158,20 +161,9 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
158161
*
159162
* It is an error to call this method after calling [cancel].
160163
*/
161-
@UseExperimental(InternalCoroutinesApi::class)
162-
fun <T : Any> tick(
163-
selector: SelectBuilder<T?>,
164-
handler: (OutputT) -> T?
165-
) {
166-
fun acceptUpdate(action: WorkflowAction<StateT, OutputT>): T? {
167-
val (newState, output) = action.applyTo(state)
168-
diagnosticListener?.onWorkflowAction(diagnosticId, action, state, newState, output)
169-
state = newState
170-
return output?.let(handler)
171-
}
172-
164+
fun <T : Any> tick(selector: SelectBuilder<T?>) {
173165
// Listen for any child workflow updates.
174-
subtreeManager.tickChildren(selector, ::acceptUpdate)
166+
subtreeManager.tickChildren(selector)
175167

176168
// Listen for any subscription updates.
177169
workers.forEachActive { child ->
@@ -188,7 +180,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
188180
} else {
189181
val update = child.acceptUpdate(valueOrDone.value)
190182
@Suppress("UNCHECKED_CAST")
191-
acceptUpdate(update as WorkflowAction<StateT, OutputT>)
183+
return@onReceive applyAction(update as WorkflowAction<StateT, OutputT>)
192184
}
193185
}
194186
}
@@ -198,7 +190,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
198190
with(selector) {
199191
eventActionsChannel.onReceive { action ->
200192
diagnosticListener?.onSinkReceived(diagnosticId, action)
201-
acceptUpdate(action)
193+
return@onReceive applyAction(action)
202194
}
203195
}
204196
}
@@ -256,6 +248,18 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
256248
lastProps = newProps
257249
}
258250

251+
/**
252+
* Applies [action] to this workflow's [state] and
253+
* [emits an output to its parent][emitOutputToParent] if necessary.
254+
*/
255+
private fun <T : Any> applyAction(action: WorkflowAction<StateT, OutputT>): T? {
256+
val (newState, output) = action.applyTo(state)
257+
diagnosticListener?.onWorkflowAction(diagnosticId, action, state, newState, output)
258+
state = newState
259+
@Suppress("UNCHECKED_CAST")
260+
return output?.let(emitOutputToParent) as T?
261+
}
262+
259263
private fun <T> createWorkerNode(
260264
worker: Worker<T>,
261265
key: String,

kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,15 @@ class SubtreeManagerTest {
9999
private val context = Unconfined
100100

101101
@Test fun `render starts new child`() {
102-
val manager =
103-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
102+
val manager = subtreeManagerForTest<String, String>()
104103
val workflow = TestWorkflow()
105104

106105
manager.render(workflow, "props", key = "", handler = { fail() })
107106
assertEquals(1, workflow.started)
108107
}
109108

110109
@Test fun `render doesn't start existing child`() {
111-
val manager =
112-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
110+
val manager = subtreeManagerForTest<String, String>()
113111
val workflow = TestWorkflow()
114112
fun render() = manager.render(workflow, "props", key = "", handler = { fail() })
115113
.also { manager.commitRenderedChildren() }
@@ -121,8 +119,7 @@ class SubtreeManagerTest {
121119
}
122120

123121
@Test fun `render restarts child after tearing down`() {
124-
val manager =
125-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
122+
val manager = subtreeManagerForTest<String, String>()
126123
val workflow = TestWorkflow()
127124
fun render() = manager.render(workflow, "props", key = "", handler = { fail() })
128125
.also { manager.commitRenderedChildren() }
@@ -138,8 +135,7 @@ class SubtreeManagerTest {
138135
}
139136

140137
@Test fun `render throws on duplicate key`() {
141-
val manager =
142-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
138+
val manager = subtreeManagerForTest<String, String>()
143139
val workflow = TestWorkflow()
144140
manager.render(workflow, "props", "foo", handler = { fail() })
145141

@@ -153,8 +149,7 @@ class SubtreeManagerTest {
153149
}
154150

155151
@Test fun `render returns child rendering`() {
156-
val manager =
157-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
152+
val manager = subtreeManagerForTest<String, String>()
158153
val workflow = TestWorkflow()
159154

160155
val (composeProps, composeState) = manager.render(
@@ -165,8 +160,7 @@ class SubtreeManagerTest {
165160
}
166161

167162
@Test fun `tick children handles child output`() {
168-
val manager =
169-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
163+
val manager = subtreeManagerForTest<String, String>()
170164
val workflow = TestWorkflow()
171165
val handler: StringHandler = { output ->
172166
action { setOutput("case output:$output") }
@@ -189,8 +183,7 @@ class SubtreeManagerTest {
189183
}
190184

191185
@Test fun `render updates child's output handler`() {
192-
val manager =
193-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
186+
val manager = subtreeManagerForTest<String, String>()
194187
val workflow = TestWorkflow()
195188
fun render(handler: StringHandler) =
196189
manager.render(workflow, "props", key = "", handler = handler)
@@ -219,7 +212,7 @@ class SubtreeManagerTest {
219212

220213
// See https://github.com/square/workflow/issues/404
221214
@Test fun `createChildSnapshot snapshots eagerly`() {
222-
val manager = SubtreeManager<Unit, Nothing>(Unconfined, parentDiagnosticId = 0)
215+
val manager = subtreeManagerForTest<Unit, Nothing>()
223216
val workflow = SnapshotTestWorkflow()
224217
assertEquals(0, workflow.snapshots)
225218

@@ -232,7 +225,7 @@ class SubtreeManagerTest {
232225

233226
// See https://github.com/square/workflow/issues/404
234227
@Test fun `createChildSnapshot serializes lazily`() {
235-
val manager = SubtreeManager<Unit, Nothing>(Unconfined, parentDiagnosticId = 0)
228+
val manager = subtreeManagerForTest<Unit, Nothing>()
236229
val workflow = SnapshotTestWorkflow()
237230
assertEquals(0, workflow.serializes)
238231

@@ -246,11 +239,9 @@ class SubtreeManagerTest {
246239
assertEquals(1, workflow.serializes)
247240
}
248241

249-
private suspend fun <S, O : Any> SubtreeManager<S, O>.tickAction(): WorkflowAction<S, O>? {
250-
return select {
251-
tickChildren(this) { update ->
252-
return@tickChildren update
253-
}
254-
}
255-
}
242+
private suspend fun <S, O : Any> SubtreeManager<S, O>.tickAction(): WorkflowAction<S, O>? =
243+
select { tickChildren(this) }
244+
245+
private fun <S, O : Any> subtreeManagerForTest() =
246+
SubtreeManager<S, O>(context, emitActionToParent = { it }, parentDiagnosticId = 0)
256247
}

0 commit comments

Comments
 (0)