use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use futures::future::BoxFuture;
use serde_json::{Map, Value, json};
use super::flow::FlowPlan;
use super::{BasicPipelineEngine, NodesAccess, NodesRetentionPlan, emit_lifecycle, strip_private_markers};
use crate::pipeline::model::{
ExecutionBus, NodeTraceEntry, PipelineContext, PipelineError, PipelineGraph, PipelineNode,
PipelineOutput, Responder,
};
use crate::pipeline::nodes::NodeExecutionOutput;
use crate::pipeline::nodes::basic::logic;
use crate::pipeline::trace_capture::TraceCapture;
pub(super) enum Ran {
Answered(Vec<NodeExecutionOutput>),
Routed(Value),
}
pub(super) struct Delivery {
pub payload: Value,
pub input_pin: String,
pub series: Option<Value>,
pub loop_items: Option<Vec<Value>>,
}
pub(super) struct RunState<'a> {
pub ctx: &'a PipelineContext,
pub graph: &'a PipelineGraph,
pub bus: Option<Arc<ExecutionBus>>,
pub run_started: std::time::Instant,
pub project_timeout_secs: u64,
pub trace_capture: TraceCapture,
pub retention: NodesRetentionPlan,
pub responder: Option<Responder>,
pub trace: Vec<String>,
pub node_trace: Vec<NodeTraceEntry>,
pub response: Option<Value>,
pub frames: Vec<HashMap<usize, Value>>,
pub last_answer: Vec<Option<Value>>,
pub last_in_time: Option<Value>,
}
impl RunState<'_> {
pub fn answer_of(&self, node: usize) -> Option<&Value> {
self.frames.iter().rev().find_map(|frame| frame.get(&node))
}
pub fn store_answer(&mut self, node: &PipelineNode, idx: usize, value: Value) {
if self.retention.retained_nodes.contains(&node.id)
&& let Some(frame) = self.frames.last_mut()
{
frame.insert(idx, value);
}
}
pub fn nodes_scope(&self, plan: &FlowPlan, node: &PipelineNode) -> Value {
let Some(NodesAccess::Exact(ids)) = self.retention.consumer_access.get(&node.id) else {
return json!({});
};
let mut scope = Map::new();
for id in ids {
let answer = plan.index.get(id).and_then(|&i| self.answer_of(i)).cloned();
scope.insert(id.clone(), answer.unwrap_or(Value::Null));
}
Value::Object(scope)
}
fn record_skip(&mut self, idx: usize) {
let node = &self.graph.nodes[idx];
if let Some(frame) = self.frames.last_mut() {
frame.remove(&idx);
}
self.last_answer[idx] = None;
self.node_trace.push(NodeTraceEntry {
node_id: node.id.clone(),
node_kind: node.kind.clone(),
config: None,
duration_ms: 0,
input: Value::Null,
output: Value::Null,
error: None,
status: "skipped".to_string(),
error_code: None,
preview_snapshot: None,
});
emit_lifecycle(
&self.bus,
"node_skipped",
format!("{} {} skipped", node.id, node.kind),
Some((&node.id, &node.kind)),
None,
&self.run_started,
);
}
pub fn finish(self, plan: &FlowPlan) -> PipelineOutput {
let value = (0..self.last_answer.len())
.rev()
.filter(|&i| plan.sink[i])
.find_map(|i| self.last_answer[i].clone())
.or(self.last_in_time)
.unwrap_or(Value::Null);
PipelineOutput {
value: strip_private_markers(value),
response: self.response.map(strip_private_markers),
trace: self.trace,
node_trace: self.node_trace,
}
}
}
#[derive(Clone)]
enum EdgeState {
Delivered(Value),
Skipped,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum NodeState {
Done,
Skipped,
}
enum LoopOutcome {
Ran { base: Value, items: HashMap<usize, Vec<Value>> },
Skipped,
}
pub(super) struct ScopeRun {
members: Vec<usize>,
member: HashSet<usize>,
series: Option<Value>,
seeds: HashMap<usize, Value>,
edges: HashMap<usize, EdgeState>,
nodes: HashMap<usize, NodeState>,
loops: HashMap<usize, LoopOutcome>,
run_q: VecDeque<(usize, Option<(Value, String)>)>,
skip_q: VecDeque<usize>,
}
impl ScopeRun {
fn new(members: Vec<usize>, series: Option<Value>) -> Self {
Self {
member: members.iter().copied().collect(),
members,
series,
seeds: HashMap::new(),
edges: HashMap::new(),
nodes: HashMap::new(),
loops: HashMap::new(),
run_q: VecDeque::new(),
skip_q: VecDeque::new(),
}
}
pub fn top(plan: &FlowPlan, graph: &PipelineGraph, input: &Value) -> Self {
let members = (0..graph.nodes.len()).filter(|&i| plan.owner[i].is_none()).collect();
let mut scope = Self::new(members, None);
for &entry in &plan.entries {
scope.seeds.insert(entry, input.clone());
}
scope
}
fn pending(&self, node: usize) -> bool {
!self.nodes.contains_key(&node)
}
fn resolved(&self, plan: &FlowPlan, node: usize) -> Option<bool> {
if let Some(foreach) = plan.closes[node] {
return match self.loops.get(&foreach)? {
LoopOutcome::Ran { .. } => Some(true),
LoopOutcome::Skipped => Some(false),
};
}
let mut delivered = self.seeds.contains_key(&node);
for edge in &plan.incoming[node] {
match self.edges.get(edge)? {
EdgeState::Delivered(_) => delivered = true,
EdgeState::Skipped => {}
}
}
Some(delivered)
}
fn check(&mut self, plan: &FlowPlan, node: usize) {
if !self.member.contains(&node) || !self.pending(node) {
return;
}
match self.resolved(plan, node) {
Some(true) => self.run_q.push_back((node, None)),
Some(false) => self.skip_q.push_back(node),
None => {}
}
}
fn set_edge(&mut self, plan: &FlowPlan, edge: usize, state: EdgeState) {
self.edges.insert(edge, state);
self.check(plan, plan.edges[edge].to);
}
fn set_loop(&mut self, plan: &FlowPlan, foreach: usize, outcome: LoopOutcome) {
self.loops.insert(foreach, outcome);
if let Some(lp) = plan.loops.get(&foreach) {
for &close in &lp.closes {
self.check(plan, close);
}
}
}
fn delivery(&self, plan: &FlowPlan, graph: &PipelineGraph, node: usize) -> Delivery {
let first_pin = || graph.nodes[node].input_pins.first().cloned().unwrap_or_default();
if let Some(foreach) = plan.closes[node] {
let (payload, items) = match self.loops.get(&foreach) {
Some(LoopOutcome::Ran { base, items }) => {
(base.clone(), items.get(&node).cloned().unwrap_or_default())
}
_ => (Value::Null, Vec::new()),
};
let input_pin = plan.incoming[node].first().map(|&e| plan.edges[e].to_pin.clone()).unwrap_or_else(first_pin);
return Delivery { payload, input_pin, series: self.series.clone(), loop_items: Some(items) };
}
let mut delivered: Vec<Value> = Vec::new();
let mut input_pin = None;
if let Some(seed) = self.seeds.get(&node) {
delivered.push(seed.clone());
}
for &edge in &plan.incoming[node] {
if let Some(EdgeState::Delivered(payload)) = self.edges.get(&edge) {
delivered.push(payload.clone());
input_pin.get_or_insert_with(|| plan.edges[edge].to_pin.clone());
}
}
let payload = if graph.nodes[node].kind == logic::collect::NODE_KIND {
Value::Array(delivered)
} else {
merge(delivered)
};
Delivery { payload, input_pin: input_pin.unwrap_or_else(first_pin), series: self.series.clone(), loop_items: None }
}
fn reenter(&mut self, plan: &FlowPlan, target: usize, payload: Value, pin: String) {
for node in plan.downstream_of(target) {
if !self.member.contains(&node) {
continue;
}
self.nodes.remove(&node);
self.loops.remove(&node);
for edge in &plan.outgoing[node] {
self.edges.remove(edge);
}
}
self.run_q.push_back((target, Some((payload, pin))));
}
}
fn merge(mut payloads: Vec<Value>) -> Value {
if payloads.len() == 1 {
return payloads.pop().unwrap_or(Value::Null);
}
let mut merged = Value::Object(Map::new());
for payload in payloads {
match (&mut merged, payload) {
(Value::Object(into), Value::Object(from)) => into.extend(from),
(_, other) => merged = other,
}
}
merged
}
impl BasicPipelineEngine {
pub(super) fn run_scope<'s, 'r: 's>(
&'s self,
graph: &'s PipelineGraph,
plan: &'s FlowPlan,
scope: &'s mut ScopeRun,
run: &'s mut RunState<'r>,
) -> BoxFuture<'s, Result<(), PipelineError>> {
Box::pin(async move {
for node in scope.members.clone() {
scope.check(plan, node);
}
loop {
if let Some((node, reentry)) = scope.run_q.pop_front() {
if !scope.pending(node) {
continue;
}
let delivery = match reentry {
Some((payload, input_pin)) => Delivery { payload, input_pin, series: scope.series.clone(), loop_items: None },
None if scope.resolved(plan, node) == Some(true) => scope.delivery(plan, graph, node),
None => continue,
};
self.step(graph, plan, scope, run, node, delivery).await?;
continue;
}
if let Some(node) = scope.skip_q.pop_front() {
if scope.pending(node) && scope.resolved(plan, node) == Some(false) {
skip(plan, scope, run, node);
}
continue;
}
break;
}
Ok(())
})
}
async fn step(
&self,
graph: &PipelineGraph,
plan: &FlowPlan,
scope: &mut ScopeRun,
run: &mut RunState<'_>,
node: usize,
delivery: Delivery,
) -> Result<(), PipelineError> {
let base = plan.loops.contains_key(&node).then(|| delivery.payload.clone());
let ran = self.run_node(graph, plan, node, delivery, run).await?;
scope.nodes.insert(node, NodeState::Done);
match ran {
Ran::Routed(payload) => {
for &edge in &plan.outgoing[node] {
let state = if plan.edges[edge].from_pin == super::flow::ERROR_PIN {
EdgeState::Delivered(payload.clone())
} else {
EdgeState::Skipped
};
if !plan.edges[edge].reentry {
scope.set_edge(plan, edge, state);
}
}
if base.is_some() {
scope.set_loop(plan, node, LoopOutcome::Skipped);
}
}
Ran::Answered(outs) => {
if let Some(base) = base {
let items = self.run_loop(graph, plan, node, &outs, run).await?;
for &edge in &plan.outgoing[node] {
if plan.edges[edge].from_pin != logic::foreach_::OUTPUT_PIN_ITEM {
scope.set_edge(plan, edge, EdgeState::Skipped);
}
}
scope.set_loop(plan, node, LoopOutcome::Ran { base, items });
return Ok(());
}
let mut by_pin: HashMap<&str, &Value> = HashMap::new();
for out in &outs {
for pin in &out.output_pins {
by_pin.insert(pin.as_str(), &out.payload);
}
}
let mut reentry = None;
for &edge in &plan.outgoing[node] {
let e = &plan.edges[edge];
match (by_pin.get(e.from_pin.as_str()), e.reentry) {
(Some(payload), true) => reentry = Some((e.to, (*payload).clone(), e.to_pin.clone())),
(None, true) => {}
(Some(payload), false) => scope.set_edge(plan, edge, EdgeState::Delivered((*payload).clone())),
(None, false) => scope.set_edge(plan, edge, EdgeState::Skipped),
}
}
if let Some((target, payload, pin)) = reentry {
scope.reenter(plan, target, payload, pin);
}
}
}
Ok(())
}
async fn run_loop(
&self,
graph: &PipelineGraph,
plan: &FlowPlan,
foreach: usize,
outs: &[NodeExecutionOutput],
run: &mut RunState<'_>,
) -> Result<HashMap<usize, Vec<Value>>, PipelineError> {
let Some(lp) = plan.loops.get(&foreach) else {
return Ok(HashMap::new());
};
let mut items: HashMap<usize, Vec<Value>> = lp.closes.iter().map(|&c| (c, Vec::new())).collect();
for out in outs {
let field = |name: &str| out.payload.get(name).cloned().unwrap_or(Value::Null);
let series = json!({ "item": field("item"), "index": field("index"), "count": field("count") });
let mut frame = ScopeRun::new(lp.body.clone(), Some(series));
let emitted = out.output_pins.iter().any(|p| p == logic::foreach_::OUTPUT_PIN_ITEM);
for &edge in &plan.outgoing[foreach] {
if plan.edges[edge].from_pin == logic::foreach_::OUTPUT_PIN_ITEM {
let state = if emitted { EdgeState::Delivered(out.payload.clone()) } else { EdgeState::Skipped };
frame.edges.insert(edge, state);
}
}
run.frames.push(HashMap::new());
let result = self.run_scope(graph, plan, &mut frame, run).await;
run.frames.pop();
result?;
for &close in &lp.closes {
let delivered: Vec<Value> = plan.incoming[close]
.iter()
.filter_map(|edge| match frame.edges.get(edge) {
Some(EdgeState::Delivered(payload)) => Some(payload.clone()),
_ => None,
})
.collect();
if !delivered.is_empty()
&& let Some(list) = items.get_mut(&close)
{
list.push(merge(delivered));
}
}
}
Ok(items)
}
}
fn skip(plan: &FlowPlan, scope: &mut ScopeRun, run: &mut RunState<'_>, node: usize) {
scope.nodes.insert(node, NodeState::Skipped);
run.record_skip(node);
for &edge in &plan.outgoing[node] {
if !plan.edges[edge].reentry {
scope.set_edge(plan, edge, EdgeState::Skipped);
}
}
if plan.loops.contains_key(&node) {
scope.set_loop(plan, node, LoopOutcome::Skipped);
}
}