Skip to content

Commit 9f0a537

Browse files
1247: Partial Tree Rerendering
Track whether or not the state (or state of a child) has changed in the WorkflowNode. Pass lastRendering if its not.
1 parent ce54070 commit 9f0a537

File tree

2 files changed

+78
-21
lines changed

2 files changed

+78
-21
lines changed

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

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import com.squareup.workflow1.NoopWorkflowInterceptor
66
import com.squareup.workflow1.RenderContext
77
import com.squareup.workflow1.RuntimeConfig
88
import com.squareup.workflow1.RuntimeConfigOptions
9+
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
910
import com.squareup.workflow1.StatefulWorkflow
1011
import com.squareup.workflow1.TreeSnapshot
1112
import com.squareup.workflow1.Workflow
1213
import com.squareup.workflow1.WorkflowAction
1314
import com.squareup.workflow1.WorkflowExperimentalApi
15+
import com.squareup.workflow1.WorkflowExperimentalRuntime
1416
import com.squareup.workflow1.WorkflowIdentifier
1517
import com.squareup.workflow1.WorkflowInterceptor
1618
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
@@ -33,6 +35,7 @@ import kotlinx.coroutines.launch
3335
import kotlinx.coroutines.plus
3436
import kotlinx.coroutines.selects.SelectBuilder
3537
import kotlin.coroutines.CoroutineContext
38+
import kotlin.jvm.JvmInline
3639

3740
/**
3841
* A node in a state machine tree. Manages the actual state for a given [Workflow].
@@ -44,7 +47,7 @@ import kotlin.coroutines.CoroutineContext
4447
* hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate
4548
* structured concurrency).
4649
*/
47-
@OptIn(WorkflowExperimentalApi::class)
50+
@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
4851
internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
4952
val id: WorkflowNodeId,
5053
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
@@ -86,9 +89,11 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
8689
)
8790
private val sideEffects = ActiveStagingList<SideEffectNode>()
8891
private var lastProps: PropsT = initialProps
92+
private var lastRendering: Box<RenderingT> = Box()
8993
private val eventActionsChannel =
9094
Channel<WorkflowAction<PropsT, StateT, OutputT>>(capacity = UNLIMITED)
9195
private var state: StateT
96+
private var subtreeStateDidChange: Boolean = true
9297

9398
private val baseRenderContext = RealRenderContext(
9499
renderer = subtreeManager,
@@ -211,12 +216,14 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
211216
*/
212217
private fun updateCachedWorkflowInstance(
213218
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
214-
) {
219+
): Boolean {
215220
if (workflow !== cachedWorkflowInstance) {
216221
// The instance has changed.
217222
cachedWorkflowInstance = workflow
218223
interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this)
224+
return true
219225
}
226+
return false
220227
}
221228

222229
/**
@@ -227,39 +234,56 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
227234
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
228235
props: PropsT
229236
): RenderingT {
230-
updateCachedWorkflowInstance(workflow)
231-
updatePropsAndState(props)
237+
val didUpdateCachedInstance = updatePropsAndState(props, workflow)
232238

233-
baseRenderContext.unfreeze()
234-
val rendering = interceptedWorkflowInstance.render(props, state, context)
235-
baseRenderContext.freeze()
239+
if (!runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) ||
240+
!lastRendering.isInitialized ||
241+
subtreeStateDidChange
242+
) {
243+
if (!didUpdateCachedInstance) {
244+
// If we haven't already updated the cached instance, better do it now!
245+
updateCachedWorkflowInstance(workflow)
246+
}
247+
baseRenderContext.unfreeze()
248+
lastRendering = Box(interceptedWorkflowInstance.render(props, state, context))
249+
baseRenderContext.freeze()
236250

237-
workflowTracer.trace("UpdateRuntimeTree") {
238-
// Tear down workflows and workers that are obsolete.
239-
subtreeManager.commitRenderedChildren()
240-
// Side effect jobs are launched lazily, since they can send actions to the sink, and can only
241-
// be started after context is frozen.
242-
sideEffects.forEachStaging { it.job.start() }
243-
sideEffects.commitStaging { it.job.cancel() }
251+
workflowTracer.trace("UpdateRuntimeTree") {
252+
// Tear down workflows and workers that are obsolete.
253+
subtreeManager.commitRenderedChildren()
254+
// Side effect jobs are launched lazily, since they can send actions to the sink, and can only
255+
// be started after context is frozen.
256+
sideEffects.forEachStaging { it.job.start() }
257+
sideEffects.commitStaging { it.job.cancel() }
258+
}
244259
}
245260

246-
return rendering
261+
return lastRendering.getOrThrow()
247262
}
248263

264+
/**
265+
* @return true if the [interceptedWorkflowInstance] has been updated, false otherwise.
266+
*/
249267
private fun updatePropsAndState(
250-
newProps: PropsT
251-
) {
268+
newProps: PropsT,
269+
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
270+
): Boolean {
271+
var didUpdateCachedInstance = false
252272
if (newProps != lastProps) {
273+
didUpdateCachedInstance = updateCachedWorkflowInstance(workflow)
253274
val newState = interceptedWorkflowInstance.onPropsChanged(lastProps, newProps, state)
254275
state = newState
276+
subtreeStateDidChange = true
255277
}
256278
lastProps = newProps
279+
return didUpdateCachedInstance
257280
}
258281

259282
/**
260283
* Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
261284
* via [emitAppliedActionToParent] to the parent, with additional information as to whether or
262285
* not this action has changed the current node's state.
286+
*
263287
*/
264288
private fun applyAction(
265289
action: WorkflowAction<PropsT, StateT, OutputT>,
@@ -272,7 +296,13 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
272296
// Changing state is sticky, we pass it up if it ever changed.
273297
stateChanged = actionApplied.stateChanged || (childResult?.stateChanged ?: false)
274298
)
275-
return if (actionApplied.output != null) {
299+
// Our state changed or one of our children's state changed.
300+
subtreeStateDidChange = aggregateActionApplied.stateChanged
301+
return if (actionApplied.output != null ||
302+
runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)
303+
) {
304+
// If we are using the optimization, always return to the parent, so we carry a path that
305+
// notes that the subtree did change all the way to the root.
276306
emitAppliedActionToParent(aggregateActionApplied)
277307
} else {
278308
aggregateActionApplied
@@ -289,4 +319,17 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
289319
SideEffectNode(key, job)
290320
}
291321
}
322+
323+
@JvmInline
324+
internal value class Box<T>(private val _value: Any? = Uninitialized) {
325+
val isInitialized: Boolean get() = _value !== Uninitialized
326+
327+
@Suppress("UNCHECKED_CAST")
328+
fun getOrThrow(): T {
329+
check(isInitialized)
330+
return _value as T
331+
}
332+
}
333+
334+
internal object Uninitialized
292335
}

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowOperatorsTest.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,23 +207,37 @@ class WorkflowOperatorsTest {
207207
private abstract class StateFlowWorkflow<T>(
208208
val name: String,
209209
val flow: StateFlow<T>
210-
) : StatelessWorkflow<Unit, Nothing, T>() {
210+
) : StatefulWorkflow<Unit, T, Nothing, T>() {
211211
var starts: Int = 0
212212
private set
213213

214+
override fun initialState(
215+
props: Unit,
216+
snapshot: Snapshot?
217+
): T {
218+
return flow.value
219+
}
220+
214221
private val rerenderWorker = object : Worker<T> {
215222
override fun run(): Flow<T> = flow.onStart { starts++ }
216223
}
217224

218225
override fun render(
219226
renderProps: Unit,
227+
renderState: T,
220228
context: RenderContext
221229
): T {
222230
// Listen to the flow to trigger a re-render when it updates.
223-
context.runningWorker(rerenderWorker as Worker<Any?>) { WorkflowAction.noAction() }
224-
return flow.value
231+
context.runningWorker(rerenderWorker) { output: T ->
232+
action("rerenderUpdate") {
233+
state = output
234+
}
235+
}
236+
return renderState
225237
}
226238

239+
override fun snapshotState(state: T): Snapshot? = null
240+
227241
override fun toString(): String = "StateFlowWorkflow($name)"
228242
}
229243
}

0 commit comments

Comments
 (0)