Files
2026-07-13 12:49:10 +08:00

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
}