use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub trait GraphState: Send + Sync + Clone {
fn merge(&mut self, other: Self);
}
#[async_trait]
pub trait GraphNode<S: GraphState>: Send + Sync {
async fn execute(&self, state: &mut S) -> Result<()>;
fn name(&self) -> &str;
}
pub enum Edge<S: GraphState> {
Static { target: String },
Conditional {
router: Arc<dyn Fn(&S) -> String + Send + Sync>,
},
}
pub const NODE_START: &str = "__start__";
pub const NODE_END: &str = "__end__";
pub struct Graph<S: GraphState> {
nodes: HashMap<String, Arc<dyn GraphNode<S>>>,
edges: HashMap<String, Vec<Edge<S>>>,
entry_point: String,
max_iterations: usize,
}
impl<S: GraphState + 'static> Graph<S> {
pub async fn execute(&self, state: &mut S) -> Result<()> {
let mut current = self.entry_point.clone();
let mut iterations = 0;
loop {
if current == NODE_END || iterations >= self.max_iterations {
break;
}
iterations += 1;
if let Some(node) = self.nodes.get(¤t) {
tracing::debug!(node = current, iteration = iterations, "executing graph node");
node.execute(state).await?;
} else if current != NODE_START {
return Err(anyhow!("graph node not found: {}", current));
}
current = self.resolve_next(¤t, state)?;
}
if iterations >= self.max_iterations {
tracing::warn!("graph reached max_iterations={}", self.max_iterations);
}
Ok(())
}
fn resolve_next(&self, current: &str, state: &S) -> Result<String> {
let edges = match self.edges.get(current) {
Some(e) => e,
None => return Ok(NODE_END.to_string()),
};
if let Some(edge) = edges.first() {
match edge {
Edge::Static { target } => return Ok(target.clone()),
Edge::Conditional { router } => {
let next = (router)(state);
return Ok(next);
}
}
}
Ok(NODE_END.to_string())
}
}
pub struct GraphBuilder<S: GraphState> {
nodes: HashMap<String, Arc<dyn GraphNode<S>>>,
edges: HashMap<String, Vec<Edge<S>>>,
entry_point: Option<String>,
max_iterations: usize,
}
impl<S: GraphState + 'static> GraphBuilder<S> {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: HashMap::new(),
entry_point: None,
max_iterations: 100,
}
}
pub fn add_node(mut self, name: impl Into<String>, node: Arc<dyn GraphNode<S>>) -> Self {
self.nodes.insert(name.into(), node);
self
}
pub fn set_entry(mut self, name: impl Into<String>) -> Self {
self.entry_point = Some(name.into());
self
}
pub fn add_edge(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.edges
.entry(from.into())
.or_default()
.push(Edge::Static { target: to.into() });
self
}
pub fn add_conditional_edge(
mut self,
from: impl Into<String>,
router: impl Fn(&S) -> String + Send + Sync + 'static,
) -> Self {
self.edges
.entry(from.into())
.or_default()
.push(Edge::Conditional {
router: Arc::new(router),
});
self
}
pub fn max_iterations(mut self, n: usize) -> Self {
self.max_iterations = n;
self
}
pub fn compile(self) -> Result<Graph<S>> {
let entry = self
.entry_point
.ok_or_else(|| anyhow!("entry point not set"))?;
for (from, edges) in &self.edges {
for edge in edges {
if let Edge::Static { target } = edge {
if target != NODE_END && !self.nodes.contains_key(target) {
return Err(anyhow!(
"edge from '{}' points to unknown node '{}'",
from,
target
));
}
}
}
}
Ok(Graph {
nodes: self.nodes,
edges: self.edges,
entry_point: entry,
max_iterations: self.max_iterations,
})
}
}
impl<S: GraphState + 'static> Default for GraphBuilder<S> {
fn default() -> Self {
Self::new()
}
}
pub struct ChainNode<S: GraphState> {
name: String,
nodes: Vec<Arc<dyn GraphNode<S>>>,
}
impl<S: GraphState> ChainNode<S> {
pub fn new(name: impl Into<String>, nodes: Vec<Arc<dyn GraphNode<S>>>) -> Arc<Self> {
Arc::new(Self {
name: name.into(),
nodes,
})
}
}
#[async_trait]
impl<S: GraphState + 'static> GraphNode<S> for ChainNode<S> {
async fn execute(&self, state: &mut S) -> Result<()> {
for node in &self.nodes {
node.execute(state).await?;
}
Ok(())
}
fn name(&self) -> &str {
&self.name
}
}
pub struct ParallelNode<S: GraphState + Clone + Send + 'static> {
name: String,
nodes: Vec<Arc<dyn GraphNode<S>>>,
}
impl<S: GraphState + Clone + Send + 'static> ParallelNode<S> {
pub fn new(name: impl Into<String>, nodes: Vec<Arc<dyn GraphNode<S>>>) -> Arc<Self> {
Arc::new(Self {
name: name.into(),
nodes,
})
}
}
#[async_trait]
impl<S: GraphState + Clone + Send + 'static> GraphNode<S> for ParallelNode<S> {
async fn execute(&self, state: &mut S) -> Result<()> {
let mut handles = vec![];
for node in &self.nodes {
let mut s = state.clone();
let node = node.clone();
handles.push(tokio::spawn(async move {
node.execute(&mut s).await?;
Ok::<S, anyhow::Error>(s)
}));
}
for handle in handles {
let result_state = handle.await??;
state.merge(result_state);
}
Ok(())
}
fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelCheckpoint {
pub kind: String,
pub data: Value,
}
pub trait Channel: Send + Sync {
type Value: Clone + Send + Sync;
type Update: Send;
fn get(&self) -> Option<&Self::Value>;
fn update(&mut self, updates: Vec<Self::Update>) -> bool;
fn checkpoint(&self) -> ChannelCheckpoint;
fn consume(&mut self) -> bool;
fn is_available(&self) -> bool;
}
pub struct LastValue<T> {
pub value: Option<T>,
pub consumed: bool,
}
impl<T: Clone + Send + Sync> LastValue<T> {
pub fn new() -> Self {
Self { value: None, consumed: false }
}
pub fn with_value(v: T) -> Self {
Self { value: Some(v), consumed: false }
}
}
impl<T: Clone + Send + Sync + Default> Default for LastValue<T> {
fn default() -> Self { Self::new() }
}
impl<T: Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static> Channel for LastValue<T> {
type Value = T;
type Update = T;
fn get(&self) -> Option<&T> { self.value.as_ref() }
fn update(&mut self, updates: Vec<T>) -> bool {
if let Some(last) = updates.into_iter().last() {
self.value = Some(last);
self.consumed = false;
true
} else {
false
}
}
fn checkpoint(&self) -> ChannelCheckpoint {
ChannelCheckpoint {
kind: "LastValue".to_string(),
data: serde_json::to_value(&self.value).unwrap_or(Value::Null),
}
}
fn consume(&mut self) -> bool {
if !self.consumed && self.value.is_some() {
self.consumed = true;
true
} else {
false
}
}
fn is_available(&self) -> bool {
self.value.is_some() && !self.consumed
}
}
pub struct Aggregate<T, F> {
pub value: T,
reducer: F,
dirty: bool,
consumed: bool,
}
impl<T, F> Aggregate<T, F>
where
T: Clone + Send + Sync,
F: Fn(T, T) -> T + Send + Sync,
{
pub fn new(init: T, reducer: F) -> Self {
Self { value: init, reducer, dirty: false, consumed: false }
}
}
impl<T, F> Channel for Aggregate<T, F>
where
T: Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
F: Fn(T, T) -> T + Send + Sync + 'static,
{
type Value = T;
type Update = T;
fn get(&self) -> Option<&T> { Some(&self.value) }
fn update(&mut self, updates: Vec<T>) -> bool {
if updates.is_empty() { return false; }
for u in updates {
let acc = self.value.clone();
self.value = (self.reducer)(acc, u);
}
self.dirty = true;
self.consumed = false;
true
}
fn checkpoint(&self) -> ChannelCheckpoint {
ChannelCheckpoint {
kind: "Aggregate".to_string(),
data: serde_json::to_value(&self.value).unwrap_or(Value::Null),
}
}
fn consume(&mut self) -> bool {
if !self.consumed && self.dirty {
self.consumed = true;
true
} else {
false
}
}
fn is_available(&self) -> bool { self.dirty && !self.consumed }
}
pub struct Topic<T> {
pub items: Vec<T>,
consumed: bool,
}
impl<T: Clone + Send + Sync> Topic<T> {
pub fn new() -> Self { Self { items: vec![], consumed: false } }
}
impl<T: Clone + Send + Sync> Default for Topic<T> {
fn default() -> Self { Self::new() }
}
impl<T: Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static> Channel for Topic<T> {
type Value = Vec<T>;
type Update = T;
fn get(&self) -> Option<&Vec<T>> { if self.items.is_empty() { None } else { Some(&self.items) } }
fn update(&mut self, updates: Vec<T>) -> bool {
if updates.is_empty() { return false; }
self.items.extend(updates);
self.consumed = false;
true
}
fn checkpoint(&self) -> ChannelCheckpoint {
ChannelCheckpoint {
kind: "Topic".to_string(),
data: serde_json::to_value(&self.items).unwrap_or(Value::Null),
}
}
fn consume(&mut self) -> bool {
if !self.consumed && !self.items.is_empty() {
self.consumed = true;
true
} else {
false
}
}
fn is_available(&self) -> bool { !self.items.is_empty() && !self.consumed }
}
pub struct Barrier {
required: std::collections::HashSet<String>,
received: std::collections::HashSet<String>,
pub value: Option<Value>,
consumed: bool,
}
impl Barrier {
pub fn new(required: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
required: required.into_iter().map(|s| s.into()).collect(),
received: std::collections::HashSet::new(),
value: None,
consumed: false,
}
}
pub fn write_from(&mut self, source: impl Into<String>, val: Value) {
self.received.insert(source.into());
self.value = Some(val);
self.consumed = false;
}
pub fn is_complete(&self) -> bool {
self.required.iter().all(|r| self.received.contains(r))
}
pub fn consume_barrier(&mut self) -> bool {
if self.is_complete() && !self.consumed {
self.consumed = true;
true
} else {
false
}
}
}
pub struct Ephemeral<T> {
pub value: Option<T>,
}
impl<T: Clone + Send + Sync> Ephemeral<T> {
pub fn new() -> Self { Self { value: None } }
pub fn set(&mut self, v: T) { self.value = Some(v); }
pub fn take(&mut self) -> Option<T> { self.value.take() }
}
impl<T: Clone + Send + Sync> Default for Ephemeral<T> {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Checkpoint {
pub id: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub channel_values: HashMap<String, ChannelCheckpoint>,
pub channel_versions: HashMap<String, u64>,
pub versions_seen: HashMap<String, HashMap<String, u64>>,
pub interrupt_resume: Vec<Value>,
}
impl Checkpoint {
pub fn new() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now(),
channel_values: HashMap::new(),
channel_versions: HashMap::new(),
versions_seen: HashMap::new(),
interrupt_resume: vec![],
}
}
}
impl Default for Checkpoint {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointMeta {
pub id: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DurabilityMode {
Sync,
Async,
Exit,
}
#[async_trait]
pub trait CheckpointSaver: Send + Sync {
async fn put(&self, checkpoint: &Checkpoint) -> Result<()>;
async fn get(&self, id: &str) -> Result<Option<Checkpoint>>;
async fn list(&self, limit: usize) -> Result<Vec<CheckpointMeta>>;
async fn latest(&self) -> Result<Option<Checkpoint>>;
}
pub struct InMemoryCheckpointSaver {
inner: tokio::sync::Mutex<Vec<Checkpoint>>,
}
impl InMemoryCheckpointSaver {
pub fn new() -> Arc<Self> {
Arc::new(Self { inner: tokio::sync::Mutex::new(vec![]) })
}
}
impl Default for InMemoryCheckpointSaver {
fn default() -> Self {
Self { inner: tokio::sync::Mutex::new(vec![]) }
}
}
#[async_trait]
impl CheckpointSaver for InMemoryCheckpointSaver {
async fn put(&self, checkpoint: &Checkpoint) -> Result<()> {
self.inner.lock().await.push(checkpoint.clone());
Ok(())
}
async fn get(&self, id: &str) -> Result<Option<Checkpoint>> {
Ok(self.inner.lock().await.iter().find(|c| c.id == id).cloned())
}
async fn list(&self, limit: usize) -> Result<Vec<CheckpointMeta>> {
let guard = self.inner.lock().await;
Ok(guard.iter().rev().take(limit).map(|c| CheckpointMeta {
id: c.id.clone(),
timestamp: c.timestamp,
}).collect())
}
async fn latest(&self) -> Result<Option<Checkpoint>> {
Ok(self.inner.lock().await.last().cloned())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphInterrupt {
pub id: String,
pub value: Value,
pub index: usize,
}
#[derive(Debug)]
pub enum GraphExecutionResult {
Done,
Interrupted { interrupt: GraphInterrupt, checkpoint_id: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResumeCommand {
pub resume: Value,
pub goto: Option<String>,
pub update: Option<Value>,
}
#[derive(Debug, Default, Clone)]
pub struct InterruptScratchpad {
pub interrupt_counter: usize,
pub resume_values: Vec<Value>,
}
impl InterruptScratchpad {
pub fn new(resume_values: Vec<Value>) -> Self {
Self { interrupt_counter: 0, resume_values }
}
pub fn interrupt(&mut self, value: impl Serialize) -> Result<Value> {
let idx = self.interrupt_counter;
self.interrupt_counter += 1;
if let Some(resume_val) = self.resume_values.get(idx).cloned() {
return Ok(resume_val);
}
let serialized = serde_json::to_value(&value)
.unwrap_or(Value::String("interrupt".to_string()));
Err(anyhow::Error::new(InterruptError {
interrupt: GraphInterrupt {
id: uuid::Uuid::new_v4().to_string(),
value: serialized,
index: idx,
},
}))
}
}
#[derive(Debug, thiserror::Error)]
#[error("Graph interrupted at index {}: {}", interrupt.index, interrupt.id)]
pub struct InterruptError {
pub interrupt: GraphInterrupt,
}
#[derive(Debug)]
pub enum TickResult {
Continue,
Done,
Interrupted(GraphInterrupt),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelWrite {
pub channel: String,
pub value: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskStatus {
Pending,
Completed,
Failed,
Interrupted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRecord {
pub node_name: String,
pub writes: Vec<ChannelWrite>,
pub status: TaskStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendTarget {
pub node: String,
pub arg: Value,
}
pub struct GraphRunConfig {
pub checkpoint_saver: Option<Arc<dyn CheckpointSaver>>,
pub durability: DurabilityMode,
pub resume_command: Option<ResumeCommand>,
pub interrupt_before: Vec<String>,
pub interrupt_after: Vec<String>,
pub recursion_limit: usize,
}
impl Default for GraphRunConfig {
fn default() -> Self {
Self {
checkpoint_saver: None,
durability: DurabilityMode::Exit,
resume_command: None,
interrupt_before: vec![],
interrupt_after: vec![],
recursion_limit: 25,
}
}
}
impl<S: GraphState + 'static> Graph<S> {
pub async fn execute_with_config(
&self,
state: &mut S,
config: &GraphRunConfig,
) -> Result<GraphExecutionResult> {
let resume_values = config.resume_command.as_ref()
.map(|c| vec![c.resume.clone()])
.unwrap_or_default();
let mut scratchpad = InterruptScratchpad::new(resume_values);
let mut current = config.resume_command.as_ref()
.and_then(|c| c.goto.clone())
.unwrap_or_else(|| self.entry_point.clone());
let mut iterations = 0;
loop {
if current == NODE_END || iterations >= config.recursion_limit {
break;
}
iterations += 1;
if config.interrupt_before.contains(¤t) {
let interrupt = GraphInterrupt {
id: uuid::Uuid::new_v4().to_string(),
value: Value::String(format!("before:{}", current)),
index: scratchpad.interrupt_counter,
};
let cp_id = if let Some(saver) = &config.checkpoint_saver {
let cp = Checkpoint::new();
saver.put(&cp).await?;
cp.id
} else {
"no-checkpoint".to_string()
};
return Ok(GraphExecutionResult::Interrupted { interrupt, checkpoint_id: cp_id });
}
if let Some(node) = self.nodes.get(¤t) {
tracing::debug!(node = current, iteration = iterations, "executing graph node (with config)");
match node.execute(state).await {
Ok(()) => {}
Err(e) => {
if let Some(ie) = e.downcast_ref::<InterruptError>() {
let interrupt = ie.interrupt.clone();
let cp_id = if let Some(saver) = &config.checkpoint_saver {
let cp = Checkpoint::new();
saver.put(&cp).await?;
cp.id
} else {
"no-checkpoint".to_string()
};
return Ok(GraphExecutionResult::Interrupted { interrupt, checkpoint_id: cp_id });
}
return Err(e);
}
}
} else if current != NODE_START {
return Err(anyhow!("graph node not found: {}", current));
}
if config.durability == DurabilityMode::Sync {
if let Some(saver) = &config.checkpoint_saver {
let cp = Checkpoint::new();
saver.put(&cp).await?;
}
}
if config.interrupt_after.contains(¤t) {
let interrupt = GraphInterrupt {
id: uuid::Uuid::new_v4().to_string(),
value: Value::String(format!("after:{}", current)),
index: scratchpad.interrupt_counter,
};
let cp_id = if let Some(saver) = &config.checkpoint_saver {
let cp = Checkpoint::new();
saver.put(&cp).await?;
cp.id
} else {
"no-checkpoint".to_string()
};
return Ok(GraphExecutionResult::Interrupted { interrupt, checkpoint_id: cp_id });
}
let _ = &mut scratchpad; current = self.resolve_next(¤t, state)?;
}
if config.durability == DurabilityMode::Exit {
if let Some(saver) = &config.checkpoint_saver {
let cp = Checkpoint::new();
saver.put(&cp).await?;
}
}
if iterations >= config.recursion_limit {
tracing::warn!("graph reached recursion_limit={}", config.recursion_limit);
}
Ok(GraphExecutionResult::Done)
}
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: usize,
pub initial_delay_ms: u64,
pub backoff_factor: f64,
pub jitter: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay_ms: 100,
backoff_factor: 2.0,
jitter: true,
}
}
}
impl RetryPolicy {
pub async fn execute<F, Fut>(&self, mut f: F) -> Result<()>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
let mut attempt = 0;
loop {
match f().await {
Ok(()) => return Ok(()),
Err(e) => {
attempt += 1;
if attempt >= self.max_attempts {
return Err(e);
}
let delay = (self.initial_delay_ms as f64
* self.backoff_factor.powi(attempt as i32 - 1)) as u64;
let jitter_ms = if self.jitter {
(delay as f64 * 0.25 * rand_f64(attempt)) as u64
} else {
0
};
tokio::time::sleep(std::time::Duration::from_millis(delay + jitter_ms)).await;
}
}
}
}
}
fn rand_f64(seed: usize) -> f64 {
let x = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
(x >> 33) as f64 / (u32::MAX as f64)
}
pub type StateFn<S> = Arc<
dyn Fn(&mut S) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>
+ Send
+ Sync,
>;
pub struct FunctionNode<S: GraphState> {
name: String,
f: StateFn<S>,
}
impl<S: GraphState + 'static> FunctionNode<S> {
pub fn new<F, Fut>(name: impl Into<String>, f: F) -> Arc<Self>
where
F: Fn(&mut S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
Arc::new(Self {
name: name.into(),
f: Arc::new(move |s| Box::pin(f(s)) as _),
})
}
}
#[async_trait]
impl<S: GraphState + 'static> GraphNode<S> for FunctionNode<S> {
async fn execute(&self, state: &mut S) -> Result<()> {
(self.f)(state).await
}
fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Default, Debug)]
struct TestState {
log: Vec<String>,
}
impl GraphState for TestState {
fn merge(&mut self, mut other: Self) {
self.log.append(&mut other.log);
}
}
struct LogNode(String);
#[async_trait]
impl GraphNode<TestState> for LogNode {
async fn execute(&self, state: &mut TestState) -> Result<()> {
state.log.push(self.0.clone());
Ok(())
}
fn name(&self) -> &str { &self.0 }
}
#[test]
fn test_graph_builder_compile_valid() {
let graph = GraphBuilder::<TestState>::new()
.add_node("a", Arc::new(LogNode("a".into())))
.add_node("b", Arc::new(LogNode("b".into())))
.add_edge("a", "b")
.add_edge("b", NODE_END)
.set_entry("a")
.compile();
assert!(graph.is_ok());
}
#[test]
fn test_graph_builder_compile_missing_target() {
let graph = GraphBuilder::<TestState>::new()
.add_node("a", Arc::new(LogNode("a".into())))
.add_edge("a", "nonexistent")
.set_entry("a")
.compile();
assert!(graph.is_err());
let err_msg = format!("{}", graph.err().unwrap());
assert!(err_msg.contains("nonexistent"), "error: {err_msg}");
}
#[test]
fn test_graph_builder_compile_no_entry() {
let graph = GraphBuilder::<TestState>::new()
.add_node("a", Arc::new(LogNode("a".into())))
.compile();
assert!(graph.is_err());
assert!(format!("{}", graph.err().unwrap()).contains("entry point"));
}
#[tokio::test]
async fn test_graph_execute_simple_chain() {
let graph = GraphBuilder::<TestState>::new()
.add_node("a", Arc::new(LogNode("a".into())))
.add_node("b", Arc::new(LogNode("b".into())))
.add_edge("a", "b")
.add_edge("b", NODE_END)
.set_entry("a")
.compile()
.unwrap();
let mut state = TestState::default();
graph.execute(&mut state).await.unwrap();
assert_eq!(state.log, vec!["a", "b"]);
}
#[tokio::test]
async fn test_graph_execute_conditional_edge() {
let graph = GraphBuilder::<TestState>::new()
.add_node("decide", Arc::new(LogNode("decide".into())))
.add_node("path_a", Arc::new(LogNode("path_a".into())))
.add_node("path_b", Arc::new(LogNode("path_b".into())))
.add_conditional_edge("decide", |s: &TestState| {
if s.log.contains(&"decide".to_string()) { "path_a".into() } else { "path_b".into() }
})
.add_edge("path_a", NODE_END)
.add_edge("path_b", NODE_END)
.set_entry("decide")
.compile()
.unwrap();
let mut state = TestState::default();
graph.execute(&mut state).await.unwrap();
assert_eq!(state.log, vec!["decide", "path_a"]);
}
#[test]
fn test_last_value_update_consume() {
let mut lv = LastValue::<i32>::new();
assert!(lv.get().is_none());
assert!(!lv.is_available());
let changed = lv.update(vec![42]);
assert!(changed);
assert_eq!(lv.get(), Some(&42));
assert!(lv.is_available());
assert!(lv.consume());
assert!(!lv.is_available());
assert!(!lv.consume());
}
#[test]
fn test_last_value_last_writer_wins() {
let mut lv = LastValue::<i32>::new();
lv.update(vec![1, 2, 3]);
assert_eq!(lv.get(), Some(&3));
}
#[test]
fn test_last_value_update_empty_vec() {
let mut lv = LastValue::<i32>::new();
let changed = lv.update(vec![]);
assert!(!changed);
}
#[test]
fn test_last_value_checkpoint() {
let lv = LastValue::with_value(99);
let ckpt = lv.checkpoint();
assert_eq!(ckpt.kind, "LastValue");
assert_eq!(ckpt.data, serde_json::json!(99));
}
#[test]
fn test_aggregate_reducer_sum() {
let mut agg = Aggregate::new(0, |acc, val| acc + val);
assert_eq!(agg.get(), Some(&0));
agg.update(vec![10, 20, 30]);
assert_eq!(agg.get(), Some(&60));
assert!(agg.is_available());
}
#[test]
fn test_aggregate_consume() {
let mut agg = Aggregate::new(0, |acc, val| acc + val);
agg.update(vec![5]);
assert!(agg.consume());
assert!(!agg.is_available());
assert!(!agg.consume()); }
#[test]
fn test_aggregate_update_empty() {
let mut agg = Aggregate::new(0, |acc, val| acc + val);
let changed = agg.update(vec![]);
assert!(!changed);
}
#[test]
fn test_aggregate_checkpoint() {
let mut agg = Aggregate::new(0, |acc, val| acc + val);
agg.update(vec![1, 2]);
let ckpt = agg.checkpoint();
assert_eq!(ckpt.kind, "Aggregate");
assert_eq!(ckpt.data, serde_json::json!(3));
}
}