@@ -6,11 +6,13 @@ import com.squareup.workflow1.NoopWorkflowInterceptor
66import com.squareup.workflow1.RenderContext
77import com.squareup.workflow1.RuntimeConfig
88import com.squareup.workflow1.RuntimeConfigOptions
9+ import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
910import com.squareup.workflow1.StatefulWorkflow
1011import com.squareup.workflow1.TreeSnapshot
1112import com.squareup.workflow1.Workflow
1213import com.squareup.workflow1.WorkflowAction
1314import com.squareup.workflow1.WorkflowExperimentalApi
15+ import com.squareup.workflow1.WorkflowExperimentalRuntime
1416import com.squareup.workflow1.WorkflowIdentifier
1517import com.squareup.workflow1.WorkflowInterceptor
1618import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
@@ -33,6 +35,7 @@ import kotlinx.coroutines.launch
3335import kotlinx.coroutines.plus
3436import kotlinx.coroutines.selects.SelectBuilder
3537import 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 )
4851internal 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}
0 commit comments