110 lines
4.8 KiB
Go
110 lines
4.8 KiB
Go
/*
|
|
* Copyright 2025 CloudWeGo Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package core
|
|
|
|
import "context"
|
|
|
|
// GetInterruptState provides a type-safe way to check for and retrieve the persisted state from a previous interruption.
|
|
// It is the primary function a component should use to understand its past state.
|
|
//
|
|
// It returns three values:
|
|
// - wasInterrupted (bool): True if the node was part of a previous interruption, regardless of whether state was provided.
|
|
// - state (T): The typed state object, if it was provided and matches type `T`.
|
|
// - hasState (bool): True if state was provided during the original interrupt and successfully cast to type `T`.
|
|
func GetInterruptState[T any](ctx context.Context) (wasInterrupted bool, hasState bool, state T) {
|
|
rCtx, ok := getRunCtx(ctx)
|
|
if !ok || rCtx.interruptState == nil {
|
|
return
|
|
}
|
|
|
|
wasInterrupted = true
|
|
if rCtx.interruptState.State == nil {
|
|
return
|
|
}
|
|
|
|
state, hasState = rCtx.interruptState.State.(T)
|
|
return
|
|
}
|
|
|
|
// GetResumeContext checks if the current component is the target of a resume operation
|
|
// and retrieves any data provided by the user for that resumption.
|
|
//
|
|
// This function is typically called *after* a component has already determined it is in a
|
|
// resumed state by calling GetInterruptState.
|
|
//
|
|
// It returns three values:
|
|
// - isResumeTarget: A boolean that is true if the current component's address OR any of its
|
|
// descendant addresses was explicitly targeted by a call to Resume() or ResumeWithData().
|
|
// This allows composite components (like tools containing nested graphs) to know they should
|
|
// execute their children to reach the actual resume target.
|
|
// - hasData: A boolean that is true if data was provided for this specific component (i.e., not nil).
|
|
// - data: The typed data provided by the user.
|
|
//
|
|
// ### How to Use This Function: A Decision Framework
|
|
//
|
|
// The correct usage pattern depends on the application's desired resume strategy.
|
|
//
|
|
// #### Strategy 1: Implicit "Resume All"
|
|
// In some use cases, any resume operation implies that *all* interrupted points should proceed.
|
|
// For example, if an application's UI only provides a single "Continue" button for a set of
|
|
// interruptions. In this model, a component can often just use `GetInterruptState` to see if
|
|
// `wasInterrupted` is true and then proceed with its logic, as it can assume it is an intended target.
|
|
// It may still call `GetResumeContext` to check for optional data, but the `isResumeFlow` flag is less critical.
|
|
//
|
|
// #### Strategy 2: Explicit "Targeted Resume" (Most Common)
|
|
// For applications with multiple, distinct interrupt points that must be resumed independently, it is
|
|
// crucial to differentiate which point is being resumed. This is the primary use case for the `isResumeTarget` flag.
|
|
// - If `isResumeTarget` is `true`: Your component (or one of its descendants) is the target.
|
|
// If `hasData` is true, you are the direct target and should consume the data.
|
|
// If `hasData` is false, a descendant is the target—execute your children to reach it.
|
|
// - If `isResumeTarget` is `false`: Neither you nor your descendants are the target. You MUST
|
|
// re-interrupt (e.g., by returning `StatefulInterrupt(...)`) to preserve your state.
|
|
//
|
|
// ### Guidance for Composite Components
|
|
//
|
|
// Composite components (like `Graph` or other `Runnable`s that contain sub-processes) have a dual role:
|
|
// 1. Check for Self-Targeting: A composite component can itself be the target of a resume
|
|
// operation, for instance, to modify its internal state. It may call `GetResumeContext`
|
|
// to check for data targeted at its own address.
|
|
// 2. Act as a Conduit: After checking for itself, its primary role is to re-execute its children,
|
|
// allowing the resume context to flow down to them. It must not consume a resume signal
|
|
// intended for one of its descendants.
|
|
func GetResumeContext[T any](ctx context.Context) (isResumeTarget bool, hasData bool, data T) {
|
|
rCtx, ok := getRunCtx(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
isResumeTarget = rCtx.isResumeTarget
|
|
if !isResumeTarget {
|
|
return
|
|
}
|
|
|
|
// It is a resume flow, now check for data
|
|
if rCtx.resumeData == nil {
|
|
return // hasData is false
|
|
}
|
|
|
|
data, hasData = rCtx.resumeData.(T)
|
|
return
|
|
}
|
|
|
|
func getRunCtx(ctx context.Context) (*addrCtx, bool) {
|
|
rCtx, ok := ctx.Value(addrCtxKey{}).(*addrCtx)
|
|
return rCtx, ok
|
|
}
|