using System.Text.Json.Nodes; using w4c_workflows.Models.Credentials; using w4c_workflows.Models.Nodes; using w4c_workflows.Services.Nodes.Interpolation; using w4c_workflows.Services.Security; namespace w4c_workflows.Services.Nodes; // Routing half of the graph runner: forwarding produced items along outgoing // edges (readiness accounting incl. loop-back edges), emitting listener events // and shaping an error-port payload when a failure is routed. public sealed partial class NodeGraphRunner { // ------------------------------------------------------------------ routing private static void Propagate( NodeGraph graph, string nodeId, IReadOnlyList> ports, IReadOnlyDictionary[]> inputs, IDictionary arrived, IReadOnlyDictionary incoming, Queue ready, ISet queued, ISet exhausted, bool skipEmptyPorts) { for (var port = 0; port < ports.Count; port++) { var produced = ports[port]; // On a loop node's final ("done") invocation the loop port is empty: // skip it so the body is not re-triggered for a no-op iteration. if (skipEmptyPorts && produced.Count == 0) continue; foreach (var edge in graph.EdgesFrom(nodeId, port)) { // An exhausted loop node is not re-triggered by its loop-back edge. if (exhausted.Contains(edge.ToNodeId) && edge.IsLoopBack) continue; var targetInputs = inputs[edge.ToNodeId]; if (edge.ToInput >= 0 && edge.ToInput < targetInputs.Length) targetInputs[edge.ToInput].AddRange(produced); arrived[edge.ToNodeId]++; // Max(incoming, 1): a node with no non-loop-back predecessor (an // entry loop node) must still be re-triggered by its loop-back // edge. The queued guard prevents a double enqueue when several // inputs arrive before the node next runs. if (arrived[edge.ToNodeId] >= Math.Max(incoming[edge.ToNodeId], 1) && queued.Add(edge.ToNodeId)) ready.Enqueue(edge.ToNodeId); } } } // ------------------------------------------------------------------ listeners private static Task NotifyStarted( INodeRunListener? listener, NodeGraphNode node, IReadOnlyList> inputs, CancellationToken ct) => listener == null ? Task.CompletedTask : listener.NodeStartedAsync(node, inputs, ct); private static Task NotifyFinished( INodeRunListener? listener, NodeGraphNode node, IReadOnlyList> outputs, NodeFailure? failure, CancellationToken ct) => listener == null ? Task.CompletedTask : listener.NodeFinishedAsync(node, outputs, failure, ct); private List> BuildErrorOutputs( NodeGraphNode node, IReadOnlyList> inputs, NodeFailure failure, int errorPort) { var portCount = Math.Max(1, node.Blueprint.Outputs.Count); var ports = Enumerable.Range(0, portCount).Select(_ => (IReadOnlyList)new List()).ToList(); if (errorPort < 0 || errorPort >= ports.Count) return ports; var errorJson = new JsonObject { ["message"] = failure.Message, ["code"] = failure.Code, ["description"] = failure.Description, ["httpStatus"] = failure.HttpStatus, }; var source = inputs.FirstOrDefault(list => list.Count > 0); var items = new List(); if (source == null || source.Count == 0) { items.Add(new FlowItem { Json = new JsonObject { ["error"] = errorJson } }); } else { foreach (var item in source) { var json = (JsonObject)item.Json.DeepClone(); json["error"] = errorJson.DeepClone(); items.Add(new FlowItem { Json = json, Binary = item.Binary, Origin = item.Origin }); } } ports[errorPort] = items; return ports; } }