Skip to content

Add CoroutineScope to initialState; SessionWorkflow to aid rollout #1106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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