w4c-workflows-api/Services/Nodes/NodeGraphLinks.cs
2026-09-12 01:02:46 +03:00

79 lines
2.9 KiB
C#

using w4c_workflows.Models.Nodes;
namespace w4c_workflows.Services.Nodes;
/// <summary>
/// Loop support for the node graph. An edge that closes a cycle is a
/// <b>loop-back</b> edge: it must target a node whose blueprint declares
/// <see cref="NodeBlueprint.LoopBack"/> (currently <c>core.splitInBatches</c>),
/// and at run time it re-triggers that node for the next iteration instead of
/// counting toward its readiness.
///
/// The classification is derived from the edge set (a DFS back edge), so it is
/// recomputed identically by the compiler and when a persisted graph is rebuilt.
/// </summary>
public static class NodeGraphLinks
{
/// <summary>
/// Returns the edges with <see cref="NodeGraphEdge.IsLoopBack"/> set, or an
/// error when a cycle passes through a node that is not loop-capable.
/// </summary>
public static (List<NodeGraphEdge> Edges, string? Error) MarkLoopBackEdges(
IReadOnlyList<NodeGraphNode> nodes, IReadOnlyList<NodeGraphEdge> edges)
{
var byId = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
var adjacency = new Dictionary<string, List<int>>(StringComparer.Ordinal);
for (var i = 0; i < edges.Count; i++)
{
if (!adjacency.TryGetValue(edges[i].FromNodeId, out var outgoing))
adjacency[edges[i].FromNodeId] = outgoing = new List<int>();
outgoing.Add(i);
}
var color = new Dictionary<string, int>(StringComparer.Ordinal); // 0 = white, 1 = on stack, 2 = done
var backEdges = new HashSet<int>();
void Visit(string nodeId)
{
color[nodeId] = 1;
if (adjacency.TryGetValue(nodeId, out var outgoing))
{
foreach (var index in outgoing)
{
var target = edges[index].ToNodeId;
var targetColor = color.GetValueOrDefault(target, 0);
if (targetColor == 1)
backEdges.Add(index);
else if (targetColor == 0)
Visit(target);
}
}
color[nodeId] = 2;
}
foreach (var node in nodes)
{
if (color.GetValueOrDefault(node.Id, 0) == 0)
Visit(node.Id);
}
var marked = new List<NodeGraphEdge>(edges.Count);
string? error = null;
for (var i = 0; i < edges.Count; i++)
{
var edge = edges[i];
var isLoopBack = backEdges.Contains(i);
if (isLoopBack && !byId[edge.ToNodeId].Blueprint.LoopBack)
{
error ??=
$"a cycle through '{edge.FromNodeId}' → '{edge.ToNodeId}' is not allowed: " +
$"'{byId[edge.ToNodeId].Blueprint.Type}' is not a loop node";
}
marked.Add(edge with { IsLoopBack = isLoopBack });
}
return (marked, error);
}
}