mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
feat(web): initialize Next.js project with Tailwind CSS and TypeScript setup
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/lyzrai/flow/pkg/models"
|
||||
)
|
||||
|
||||
// retryableNodeTypes lists node types where retry makes sense (I/O, external calls).
|
||||
// Deterministic nodes (If, Set, Merge, etc.) never retry.
|
||||
var retryableNodeTypes = map[string]bool{
|
||||
"flow-nodes-base.httpRequest": true,
|
||||
"flow-nodes-base.code": true,
|
||||
"flow-nodes-base.executeWorkflow": true,
|
||||
}
|
||||
|
||||
// executeWithRetry wraps an executor call with optional retry logic from node.Settings.
|
||||
// Only applies to retryable node types. Deterministic nodes skip retry.
|
||||
func executeWithRetry(ctx context.Context, executorFn NodeExecutorFunc, node models.NodeDef, inputs [][]models.Item, execCtx *ExecutionContext) (map[int][]models.Item, error) {
|
||||
retryOnFail, _ := node.Settings["retryOnFail"].(bool)
|
||||
if !retryOnFail || !retryableNodeTypes[node.Type] {
|
||||
return executorFn(ctx, node, inputs, execCtx)
|
||||
}
|
||||
|
||||
maxTries := 3
|
||||
if v, ok := node.Settings["maxTries"].(float64); ok && v > 0 {
|
||||
maxTries = int(v)
|
||||
}
|
||||
waitMs := 1000
|
||||
if v, ok := node.Settings["waitBetweenTries"].(float64); ok && v > 0 {
|
||||
waitMs = int(v)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= maxTries; attempt++ {
|
||||
outputs, err := executorFn(ctx, node, inputs, execCtx)
|
||||
if err == nil {
|
||||
if attempt > 0 {
|
||||
slog.InfoContext(ctx, "node_retry_succeeded",
|
||||
slog.String("node", node.Name),
|
||||
slog.Int("attempt", attempt+1),
|
||||
)
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
lastErr = err
|
||||
if attempt < maxTries {
|
||||
slog.WarnContext(ctx, "node_retry",
|
||||
slog.String("node", node.Name),
|
||||
slog.Int("attempt", attempt+1),
|
||||
slog.Int("max", maxTries+1),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
time.Sleep(time.Duration(waitMs) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// allInputsEmpty returns true if all input slots are empty.
|
||||
func allInputsEmpty(inputs [][]models.Item) bool {
|
||||
for _, group := range inputs {
|
||||
if len(group) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// forwardInputs stores the node's inputs as its outputs so downstream nodes
|
||||
// aren't starved when an executor fails or is missing.
|
||||
func forwardInputs(ctx *ExecutionContext, nodeName string, inputs [][]models.Item) {
|
||||
var all []models.Item
|
||||
for _, input := range inputs {
|
||||
all = append(all, input...)
|
||||
}
|
||||
if len(all) > 0 {
|
||||
ctx.SetOutput(nodeName, 0, all)
|
||||
}
|
||||
}
|
||||
|
||||
func getTerminalOutputs(ctx *ExecutionContext, dag *DAG) map[string]map[int][]models.Item {
|
||||
result := make(map[string]map[int][]models.Item)
|
||||
allOutputs := ctx.AllOutputs()
|
||||
|
||||
for nodeName := range dag.Nodes {
|
||||
if ctx.IsDelegated(nodeName) {
|
||||
continue
|
||||
}
|
||||
if isEffectiveTerminal(ctx, dag, nodeName) {
|
||||
if out, ok := allOutputs[nodeName]; ok {
|
||||
result[nodeName] = out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func isEffectiveTerminal(ctx *ExecutionContext, dag *DAG, nodeName string) bool {
|
||||
edges := dag.Adjacency[nodeName]
|
||||
if len(edges) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, edge := range edges {
|
||||
if !ctx.IsDelegated(edge.Target) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user