, proceed: (P, S, RenderContextInterceptor
?) -> R, session: WorkflowSession + ): R = traceRender(session) { + proceed(renderProps, renderState, null) + } + + @OptIn(WorkflowExperimentalApi::class) + @Composable + override fun
onRenderComposeWorkflow(
+ renderProps: P,
+ emitOutput: (O) -> Unit,
+ proceed: @Composable (P, (O) -> Unit) -> R,
+ session: WorkflowSession
+ ): R = traceRender(session) {
+ proceed(renderProps, emitOutput)
+ }
+
+ private inline fun onRenderComposeWorkflow(
+ renderProps: P,
+ emitOutput: (O) -> Unit,
+ proceed: @Composable (P, (O) -> Unit) -> R,
+ session: WorkflowSession
+ ): R = logMethod("onRenderComposeWorkflow", session, "renderProps" to renderProps) {
+ val childRenderer = LocalWorkflowComposableRenderer.current
+ val loggingRenderer = androidx.compose.runtime.remember(childRenderer) {
+ SimpleLoggingWorkflowComposableRenderer(session, childRenderer)
+ }
+ withCompositionLocals(LocalWorkflowComposableRenderer provides loggingRenderer) {
+ proceed(renderProps, /* emitOutput= */{ output ->
+ logMethod("onEmitOutput", session, "output" to output) {
+ emitOutput(output)
+ }
+ })
+ }
+ }
+
+ @OptIn(WorkflowExperimentalApi::class)
+ private inner class SimpleLoggingWorkflowComposableRenderer(
+ val session: WorkflowSession,
+ val childRenderer: WorkflowComposableRenderer
+ ) : WorkflowComposableRenderer {
+ @Composable
+ override fun onRenderComposeWorkflow(
+ renderProps: P,
+ emitOutput: (O) -> Unit,
+ proceed: @WorkflowComposable @Composable (P, (O) -> Unit) -> R,
+ session: WorkflowSession
+ ): R = proceed(renderProps, emitOutput)
+
/**
* Intercept calls to [StatefulWorkflow.snapshotState] including the children calls.
* This is useful to intercept a rendering + snapshot traversal for tracing purposes.
@@ -403,8 +415,9 @@ internal fun WorkflowInterceptor.intercept(
if (cachedInterceptedRenderContext == null || canonicalRenderContext !== context ||
canonicalRenderContextInterceptor != interceptor
) {
- val interceptedRenderContext = interceptor?.let { InterceptedRenderContext(context, it) }
- ?: context
+ val interceptedRenderContext =
+ interceptor?.let { InterceptedRenderContext(context, it) }
+ ?: context
cachedInterceptedRenderContext = RenderContext(interceptedRenderContext, this)
}
canonicalRenderContext = context
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt
index f5c405227..1f4167b0a 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt
@@ -1,5 +1,6 @@
package com.squareup.workflow1.internal
+import androidx.compose.runtime.Composable
import com.squareup.workflow1.BaseRenderContext
import com.squareup.workflow1.NoopWorkflowInterceptor
import com.squareup.workflow1.RenderingAndSnapshot
@@ -7,6 +8,7 @@ import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.TreeSnapshot
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction
+import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowInterceptor
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate
@@ -99,6 +101,56 @@ internal class ChainedWorkflowInterceptor(
return chainedProceed(renderProps, renderState, null)
}
+ @OptIn(WorkflowExperimentalApi::class)
+ @Composable
+ override fun onRenderComposeWorkflow(
+ renderProps: P,
+ emitOutput: (O) -> Unit,
+ proceed: @Composable (P, (O) -> Unit) -> R,
+ session: WorkflowSession
+ ): R = onRenderComposeWorkflowStep(
+ index = 0,
+ stepProps = renderProps,
+ stepEmitOutput = emitOutput,
+ stepProceed = proceed,
+ session = session
+ )
+
+ /**
+ * Recursive function for nesting chained interceptors' [onRenderComposeWorkflow] calls. We use
+ * recursion for the compose call since it avoids creating a new list in the composition on every
+ * render call.
+ */
+ @OptIn(WorkflowExperimentalApi::class)
+ @Composable
+ private fun onRenderComposeWorkflowStep(
+ index: Int,
+ stepProps: P,
+ stepEmitOutput: (O) -> Unit,
+ stepProceed: @Composable (P, (O) -> Unit) -> R,
+ session: WorkflowSession
+ ): R {
+ if (index >= interceptors.size) {
+ return stepProceed(stepProps, stepEmitOutput)
+ }
+
+ val interceptor = interceptors[index]
+ return interceptor.onRenderComposeWorkflow(
+ renderProps = stepProps,
+ emitOutput = stepEmitOutput,
+ proceed = { innerProps, innerEmitOutput ->
+ onRenderComposeWorkflowStep(
+ index = index + 1,
+ stepProps = innerProps,
+ stepEmitOutput = innerEmitOutput,
+ stepProceed = stepProceed,
+ session = session
+ )
+ },
+ session = session
+ )
+ }
+
override fun onSnapshotStateWithChildren(
proceed: () -> TreeSnapshot,
session: WorkflowSession
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Channels.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Channels.kt
new file mode 100644
index 000000000..ce32c8966
--- /dev/null
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Channels.kt
@@ -0,0 +1,20 @@
+package com.squareup.workflow1.internal
+
+import kotlinx.coroutines.channels.SendChannel
+
+/**
+ * Tries to send [element] to this channel and throws an [IllegalStateException] if the channel is
+ * full or closed.
+ */
+internal fun onRenderComposeWorkflow(
+ renderProps: P,
+ emitOutput: (O) -> Unit,
+ proceed: @Composable (P, (O) -> Unit) -> R,
+ session: WorkflowSession
+ ): R {
+ if (session.isRootWorkflow) {
+ // Track the overall render pass for the whole tree.
+ onBeforeRenderPass(renderProps)
+ }
+ onBeforeWorkflowRendered(session.sessionId, renderProps, null)
+
+ val rendering = proceed(renderProps, /*emitOutput=*/ { output ->
+ onComposeWorkflowOutput(session.sessionId, output)
+ emitOutput(output)
+ })
+
+ onAfterWorkflowRendered(session.sessionId, rendering)
+ if (session.isRootWorkflow) {
+ onAfterRenderPass(rendering)
+ }
+ return rendering
+ }
+
override fun onSnapshotState(
state: S,
proceed: (S) -> Snapshot?,
diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt
index 903599871..4f65447e6 100644
--- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt
+++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt
@@ -1,8 +1,10 @@
package com.squareup.workflow1
+import androidx.compose.runtime.Composable
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
+import com.squareup.workflow1.compose.WorkflowComposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
@@ -121,6 +123,16 @@ public interface WorkflowInterceptor {
session: WorkflowSession
): R = proceed(renderProps, renderState, null)
+ @WorkflowExperimentalApi
+ @WorkflowComposable
+ @Composable
+ public fun onSnapshotState(
state: S,
proceed: (S) -> Snapshot?,
@@ -446,6 +474,20 @@ public class TracingWorkflowInterceptor internal constructor(
)
}
+ private fun onComposeWorkflowOutput(
+ workflowId: Long,
+ output: Any?,
+ ) {
+ val name = workflowNamesById.getValue(workflowId)
+ logger?.log(
+ Instant(
+ name = "emitOutput received: $name",
+ category = "update",
+ args = mapOf("output" to output)
+ ),
+ )
+ }
+
private fun createMemoryEvent(): Counter {
val freeMemory = memoryStats.freeMemory()
val usedMemory = memoryStats.totalMemory() - freeMemory