Skip to content

Commit

Permalink
988: Cache RenderContext per instance
Browse files Browse the repository at this point in the history
For the InterceptedRenderContext and for the StatelessWorkflow.RenderContext.
  • Loading branch information
steve-the-edwards committed Jan 28, 2025
1 parent 2e43d2c commit 646ebf7
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package com.squareup.workflow1

import kotlin.LazyThreadSafetyMode.NONE
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

Expand Down Expand Up @@ -33,11 +32,55 @@ public abstract class StatelessWorkflow<in PropsT, out OutputT, out RenderingT>
) : BaseRenderContext<@UnsafeVariance PropsT, Nothing, @UnsafeVariance OutputT> by
baseContext as BaseRenderContext<PropsT, Nothing, OutputT>

@Suppress("UNCHECKED_CAST")
private val statefulWorkflow = Workflow.stateful<PropsT, Unit, OutputT, RenderingT>(
initialState = { Unit },
render = { props, _ -> render(props, RenderContext(this, this@StatelessWorkflow)) }
)
/**
* Class type returned by [asStatefulWorkflow].
* See [statefulWorkflow] for the instance.
*/
private inner class StatelessAsStatefulWorkflow :
StatefulWorkflow<PropsT, Unit, OutputT, RenderingT>() {

/**
* We want to cache the render context so that we don't have to recreate it each time
* render() is called.
*/
private var cachedStatelessRenderContext:
StatelessWorkflow<PropsT, OutputT, RenderingT>.RenderContext? = null

/**
* We must know if the RenderContext we are passed (which is a StatefulWorkflow.RenderContext)
* has changed, so keep track of it.
*/
private var canonicalStatefulRenderContext:
StatefulWorkflow<PropsT, Unit, OutputT, RenderingT>.RenderContext? = null

override fun initialState(
props: PropsT,
snapshot: Snapshot?
) = Unit

override fun render(
renderProps: PropsT,
renderState: Unit,
context: RenderContext
): RenderingT {
// The `RenderContext` used *might* change - primarily in the case of our tests. E.g., The
// `RenderTester` uses a special NoOp context to render twice to test for idempotency.
// In order to support a changed render context but keep caching, we check to see if the
// instance passed in has changed.
if (cachedStatelessRenderContext == null || context !== canonicalStatefulRenderContext) {
// Recreate it if the StatefulWorkflow.RenderContext we are passed has changed.
cachedStatelessRenderContext = RenderContext(context, this@StatelessWorkflow)
}
canonicalStatefulRenderContext = context
// Pass the StatelessWorkflow.RenderContext to our StatelessWorkflow.
return render(renderProps, cachedStatelessRenderContext!!)
}

override fun snapshotState(state: Unit): Snapshot? = null
}

private val statefulWorkflow: StatefulWorkflow<PropsT, Unit, OutputT, RenderingT> =
StatelessAsStatefulWorkflow()

/**
* Called at least once any time one of the following things happens:
Expand Down Expand Up @@ -69,9 +112,6 @@ public abstract class StatelessWorkflow<in PropsT, out OutputT, out RenderingT>
/**
* Satisfies the [Workflow] interface by wrapping `this` in a [StatefulWorkflow] with `Unit`
* state.
*
* This method is called a few times per instance, but we don't need to allocate a new
* [StatefulWorkflow] every time, so we store it in a private property.
*/
final override fun asStatefulWorkflow(): StatefulWorkflow<PropsT, *, OutputT, RenderingT> =
statefulWorkflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ public object NoopWorkflowInterceptor : WorkflowInterceptor
/**
* Returns a [StatefulWorkflow] that will intercept all calls to [workflow] via this
* [WorkflowInterceptor].
*
* This is called once for each instance/session of a Workflow being intercepted. So we cache the
* render context for re-use within that [WorkflowSession].
*/
@OptIn(WorkflowExperimentalApi::class)
internal fun <P, S, O, R> WorkflowInterceptor.intercept(
Expand All @@ -277,6 +280,22 @@ internal fun <P, S, O, R> WorkflowInterceptor.intercept(
workflow
} else {
object : SessionWorkflow<P, S, O, R>() {

/**
* Render context that we are passed.
*/
private var canonicalRenderContext: StatefulWorkflow<P, S, O, R>.RenderContext? = null

/**
* Render context interceptor that we are passed.
*/
private var canonicalRenderContextInterceptor: RenderContextInterceptor<P, S, O>? = null

/**
* Cache of the intercepted render context.
*/
private var cachedInterceptedRenderContext: StatefulWorkflow<P, S, O, R>.RenderContext? = null

override fun initialState(
props: P,
snapshot: Snapshot?,
Expand All @@ -298,9 +317,21 @@ internal fun <P, S, O, R> WorkflowInterceptor.intercept(
renderState,
context,
proceed = { props, state, interceptor ->
val interceptedContext = interceptor?.let { InterceptedRenderContext(context, it) }
?: context
workflow.render(props, state, RenderContext(interceptedContext, this))
// The `RenderContext` used *might* change - primarily in the case of our tests. E.g., The
// `RenderTester` uses a special NoOp context to render twice to test for idempotency.
// In order to support a changed render context but keep caching, we check to see if the
// instance passed in has changed.
if (cachedInterceptedRenderContext == null || canonicalRenderContext !== context ||
canonicalRenderContextInterceptor != interceptor
) {
val interceptedRenderContext = interceptor?.let { InterceptedRenderContext(context, it) }
?: context
cachedInterceptedRenderContext = RenderContext(interceptedRenderContext, this)
}
canonicalRenderContext = context
canonicalRenderContextInterceptor = interceptor
// Use the intercepted RenderContext for rendering.
workflow.render(props, state, cachedInterceptedRenderContext!!)
},
session = workflowSession,
)
Expand Down

0 comments on commit 646ebf7

Please sign in to comment.