Skip to content

Commit

Permalink
Merge pull request #1106 from square/sedwards/session-workflow-scope
Browse files Browse the repository at this point in the history
Add CoroutineScope to initialState; SessionWorkflow to aid rollout
  • Loading branch information
steve-the-edwards authored Sep 5, 2023
2 parents a69a23c + 40e106e commit 93dd162
Show file tree
Hide file tree
Showing 22 changed files with 836 additions and 38 deletions.
19 changes: 19 additions & 0 deletions workflow-core/api/workflow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ public final class com/squareup/workflow1/PropsUpdated : com/squareup/workflow1/
public static final field INSTANCE Lcom/squareup/workflow1/PropsUpdated;
}

public abstract class com/squareup/workflow1/SessionWorkflow : com/squareup/workflow1/StatefulWorkflow {
public fun <init> ()V
public final fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;)Ljava/lang/Object;
public abstract fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/Object;
}

public final class com/squareup/workflow1/SessionWorkflowKt {
public static final fun sessionWorkflow (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/SessionWorkflow;
public static final fun sessionWorkflow (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/SessionWorkflow;
public static final fun sessionWorkflow (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/SessionWorkflow;
public static final fun sessionWorkflow (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/SessionWorkflow;
public static synthetic fun sessionWorkflow$default (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/squareup/workflow1/SessionWorkflow;
public static synthetic fun sessionWorkflow$default (Lcom/squareup/workflow1/Workflow$Companion;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/squareup/workflow1/SessionWorkflow;
}

public abstract interface class com/squareup/workflow1/Sink {
public abstract fun send (Ljava/lang/Object;)V
}
Expand Down Expand Up @@ -142,6 +157,7 @@ public abstract class com/squareup/workflow1/StatefulWorkflow : com/squareup/wor
public final fun asStatefulWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow;
public fun getCachedIdentifier ()Lcom/squareup/workflow1/WorkflowIdentifier;
public abstract fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;)Ljava/lang/Object;
public fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/Object;
public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public abstract fun render (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/StatefulWorkflow$RenderContext;)Ljava/lang/Object;
public fun setCachedIdentifier (Lcom/squareup/workflow1/WorkflowIdentifier;)V
Expand Down Expand Up @@ -240,6 +256,9 @@ public final class com/squareup/workflow1/WorkflowAction$Updater {
public final fun setState (Ljava/lang/Object;)V
}

public abstract interface annotation class com/squareup/workflow1/WorkflowExperimentalApi : java/lang/annotation/Annotation {
}

public final class com/squareup/workflow1/WorkflowIdentifier {
public static final field Companion Lcom/squareup/workflow1/WorkflowIdentifier$Companion;
public fun equals (Ljava/lang/Object;)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,23 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
* that the workflow runtime is running in. The side effect coroutine will not be started until
* _after_ the first render call than runs it returns.
*
* Note that there is currently an [issue](https://github.com/square/workflow-kotlin/issues/1093)
* when a [runningSideEffect] (and thus also [runningWorker], or the parent Workflow of either
* via [renderChild]) is declared as running (or rendering) in one render pass and
* then not declared in the next render pass and both those consecutive render passes happen
* synchronously - i.e. without the [CoroutineDispatcher][kotlinx.coroutines.CoroutineDispatcher]
* for the Workflow runtime being able to dispatch asynchronously. This is because the jobs for
* side effects are launched lazily in order to ensure they happen after the render pass, but if
* the [CoroutineScope]'s job (the parent for all these jobs) is cancelled before these lazy
* coroutines have a chance to dispatch, then they will never run at all. For more details, and
* to report problems with this, see the [issue](https://github.com/square/workflow-kotlin/issues/1093).
* If you need guaranteed execution for some code in this scenario (like for cleanup),
* please use a [SessionWorkflow] and the [SessionWorkflow.initialState] that provides the
* [CoroutineScope] which is equivalent to the lifetime of the Workflow node in the tree. The
* [Job][kotlinx.coroutines.Job] can be extracted from that and used to get guaranteed to be
* executed lifecycle hooks, e.g. via [Job.invokeOnCompletion][kotlinx.coroutines.Job.invokeOnCompletion].
*
*
* @param key The string key that is used to distinguish between side effects.
* @param sideEffect The suspend function that will be launched in a coroutine to perform the
* side effect.
Expand Down Expand Up @@ -281,6 +298,10 @@ public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
* pass a worker stored in a variable to this function, the type that will be used to compare the
* worker will be the type of the variable, not the type of the object the variable refers to.
*
* Note that there is currently an [issue](https://github.com/square/workflow-kotlin/issues/1093)
* which can effect whether a [Worker] is ever executed.
* See more details at [BaseRenderContext.runningSideEffect].
*
* @param key An optional string key that is used to distinguish between identical [Worker]s.
*/
public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import kotlin.jvm.JvmName
*
* A [Worker] is stopped when its parent [Workflow] finishes a render pass without running the
* worker, or when the parent workflow is itself torn down.
*
* Note that there is currently an [issue](https://github.com/square/workflow-kotlin/issues/1093)
* which can effect whether a [LifecycleWorker] is ever executed.
* See more details at [BaseRenderContext.runningSideEffect].
*/
public abstract class LifecycleWorker : Worker<Nothing> {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.squareup.workflow1

import kotlinx.coroutines.CoroutineScope

/**
* An extension of [StatefulWorkflow] that gives [initialState] a [CoroutineScope]
* that corresponds with the lifetime of _session_ driven by this Workflow.
*
* A session begins the first time a parent passes a child [Workflow] of a particular type to
* [renderChild] with a particular [key] parameter. It ends when the parent executes [render]
* without making a matching [renderChild] call. The [CoroutineScope] that is passed to
* [initialState] is created when a session starts (when [renderChild] is first called), and
* [cancelled][kotlinx.coroutines.Job.cancel] when the session ends.
*
* This API extension exists on [StatefulWorkflow] as well, but it is confusing because the version
* of [initialState] that does not have the [CoroutineScope] must also be implemented as it is
* an abstract fun, even though it would never be used.
* With this version, that confusion is removed and only the version of [initialState] with the
* [CoroutineScope] must be implemented.
*/
@WorkflowExperimentalApi
public abstract class SessionWorkflow<
in PropsT,
StateT,
out OutputT,
out RenderingT
> : StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>() {

/**
* @see [StatefulWorkflow.initialState] for kdoc on the base function of this method.
*
* This version adds the following:
* @param workflowScope A [CoroutineScope] that has been created when this Workflow is first
* rendered and canceled when it is no longer rendered.
*
* This [CoroutineScope] can be used to:
*
* - set reliable teardown hooks, e.g. via [Job.invokeOnCompletion][kotlinx.coroutines.Job.invokeOnCompletion].
*
* - own the transforms on a [StateFlow][kotlinx.coroutines.flow.StateFlow],
* linking them to the lifetime of a Workflow session. For example,
* here is how you might safely combine two `StateFlow`s:
*
* data class MyState(
* val derivedValue: String,
* val derivedWorker: Worker<String>
* )
*
* override fun initialState(
* props: Unit,
* snapshot: Snapshot?,
* workflowScope: CoroutineScope
* ): MyState {
* val transformedStateFlow = stateFlow1.combine(stateFlow2, {val1, val2 -> val1 - val2}).
* stateIn(workflowScope, SharingStarted.Eagerly, ${stateFlow1.value}-${stateFlow2.value})
*
* return MyState(
* transformedStateFlow.value,
* transformedStateFlow.asWorker()
* )
* }
*
* **Note Carefully**: Neither [workflowScope] nor any of these transformed/computed dependencies
* should be stored by this Workflow instance. This could be re-created, or re-used unexpectedly
* and should not have its own state. Instead, the transformed/computed dependencies must be
* put into the [StateT] of this Workflow in order to be properly maintained.
*/
public abstract override fun initialState(
props: PropsT,
snapshot: Snapshot?,
workflowScope: CoroutineScope
): StateT

/**
* Do not use this in favor of the version of [initialState] above that includes the Workflow's
* [CoroutineScope]
*/
public final override fun initialState(
props: PropsT,
snapshot: Snapshot?
): StateT {
error("SessionWorkflow should never call initialState without the CoroutineScope.")
}
}

/**
* Returns a [SessionWorkflow] implemented via the given functions.
*/
@WorkflowExperimentalApi
public inline fun <PropsT, StateT, OutputT, RenderingT> Workflow.Companion.sessionWorkflow(
crossinline initialState: (PropsT, Snapshot?, CoroutineScope) -> StateT,
crossinline render: BaseRenderContext<PropsT, StateT, OutputT>.(
props: PropsT,
state: StateT
) -> RenderingT,
crossinline snapshot: (StateT) -> Snapshot?,
crossinline onPropsChanged: (
old: PropsT,
new: PropsT,
state: StateT
) -> StateT = { _, _, state -> state }
): SessionWorkflow<PropsT, StateT, OutputT, RenderingT> =
object : SessionWorkflow<PropsT, StateT, OutputT, RenderingT>() {
override fun initialState(
props: PropsT,
snapshot: Snapshot?,
workflowScope: CoroutineScope
): StateT = initialState(props, snapshot, workflowScope)

override fun onPropsChanged(
old: PropsT,
new: PropsT,
state: StateT
): StateT = onPropsChanged(old, new, state)

override fun render(
renderProps: PropsT,
renderState: StateT,
context: RenderContext
): RenderingT = render(context, renderProps, renderState)

override fun snapshotState(state: StateT) = snapshot(state)
}

/**
* Returns a [SessionWorkflow], with no props, implemented via the given functions.
*/
@WorkflowExperimentalApi
public inline fun <StateT, OutputT, RenderingT> Workflow.Companion.sessionWorkflow(
crossinline initialState: (Snapshot?, CoroutineScope) -> StateT,
crossinline render: BaseRenderContext<Unit, StateT, OutputT>.(state: StateT) -> RenderingT,
crossinline snapshot: (StateT) -> Snapshot?
): SessionWorkflow<Unit, StateT, OutputT, RenderingT> = sessionWorkflow(
{ _, initialSnapshot, workflowScope -> initialState(initialSnapshot, workflowScope) },
{ _, state -> render(state) },
snapshot
)

/**
* Returns a [SessionWorkflow] implemented via the given functions.
*
* This overload does not support snapshotting, but there are other overloads that do.
*/
@WorkflowExperimentalApi
public inline fun <PropsT, StateT, OutputT, RenderingT> Workflow.Companion.sessionWorkflow(
crossinline initialState: (PropsT, CoroutineScope) -> StateT,
crossinline render: BaseRenderContext<PropsT, StateT, OutputT>.(
props: PropsT,
state: StateT
) -> RenderingT,
crossinline onPropsChanged: (
old: PropsT,
new: PropsT,
state: StateT
) -> StateT = { _, _, state -> state }
): SessionWorkflow<PropsT, StateT, OutputT, RenderingT> = sessionWorkflow(
{ props, _, workflowScope -> initialState(props, workflowScope) },
render,
{ null },
onPropsChanged
)

/**
* Returns a [SessionWorkflow], with no props, implemented via the given function.
*
* This overload does not support snapshots, but there are others that do.
*/
@WorkflowExperimentalApi
public inline fun <StateT, OutputT, RenderingT> Workflow.Companion.sessionWorkflow(
crossinline initialState: (CoroutineScope) -> StateT,
crossinline render: BaseRenderContext<Unit, StateT, OutputT>.(state: StateT) -> RenderingT
): SessionWorkflow<Unit, StateT, OutputT, RenderingT> = sessionWorkflow(
{ _, workflowScope -> initialState(workflowScope) },
{ _, state -> render(state) }
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
@file:Suppress("DEPRECATION")
@file:JvmMultifileClass
@file:JvmName("Workflows")

package com.squareup.workflow1

import com.squareup.workflow1.StatefulWorkflow.RenderContext
import com.squareup.workflow1.WorkflowAction.Companion.toString
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineScope
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

Expand Down Expand Up @@ -93,6 +92,18 @@ public abstract class StatefulWorkflow<
snapshot: Snapshot?
): StateT

/**
* @see [SessionWorkflow.initialState].
* This method should only be used with a [SessionWorkflow]. It's just a pass through here so
* that we can add this behavior for [SessionWorkflow] without disrupting all [StatefulWorkflow]s.
*/
@WorkflowExperimentalApi
public open fun initialState(
props: PropsT,
snapshot: Snapshot?,
workflowScope: CoroutineScope
): StateT = initialState(props, snapshot)

/**
* Called from [RenderContext.renderChild] instead of [initialState] when the workflow is already
* running. This allows the workflow to detect changes in props, and possibly change its state in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.squareup.workflow1

import kotlin.RequiresOptIn.Level.ERROR
import kotlin.annotation.AnnotationRetention.BINARY

/**
* This is used to mark new core Workflow API that is still considered experimental.
*/
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.PROPERTY,
AnnotationTarget.FUNCTION,
AnnotationTarget.TYPEALIAS
)
@MustBeDocumented
@Retention(value = BINARY)
@RequiresOptIn(level = ERROR)
public annotation class WorkflowExperimentalApi
8 changes: 4 additions & 4 deletions workflow-runtime/api/workflow-runtime.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
public final class com/squareup/workflow1/NoopWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor {
public static final field INSTANCE Lcom/squareup/workflow1/NoopWorkflowInterceptor;
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
Expand Down Expand Up @@ -41,7 +41,7 @@ public class com/squareup/workflow1/SimpleLoggingWorkflowInterceptor : com/squar
protected fun logAfterMethod (Ljava/lang/String;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;[Lkotlin/Pair;)V
protected fun logBeforeMethod (Ljava/lang/String;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;[Lkotlin/Pair;)V
protected fun logError (Ljava/lang/String;)V
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
Expand All @@ -66,7 +66,7 @@ public abstract interface annotation class com/squareup/workflow1/WorkflowExperi
}

public abstract interface class com/squareup/workflow1/WorkflowInterceptor {
public abstract fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public abstract fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public abstract fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public abstract fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public abstract fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
Expand All @@ -76,7 +76,7 @@ public abstract interface class com/squareup/workflow1/WorkflowInterceptor {
}

public final class com/squareup/workflow1/WorkflowInterceptor$DefaultImpls {
public static fun onInitialState (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public static fun onInitialState (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public static fun onPropsChanged (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public static fun onRender (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
public static fun onRenderAndSnapshot (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
Expand Down
Loading

0 comments on commit 93dd162

Please sign in to comment.