use std::any::Any;
use indexmap::{IndexMap, IndexSet};
use serde::{Deserialize, Serialize};
use super::{
BspContext,
error::BspError,
graph::{EdgeId, InputPin, NodeId, OutputPin, PortIndex},
value::{BspOutputMapExt, BspValue},
};
#[derive(Debug, Default)]
pub struct ValueStore {
pub(crate) outputs: IndexMap<OutputPin, BspValue>,
pub(crate) injected: IndexMap<InputPin, BspValue>,
}
impl ValueStore {
#[must_use]
pub fn get_output(&self, pin: OutputPin) -> Option<&BspValue> {
self.outputs.get(&pin)
}
#[must_use]
pub fn get_output_typed<T: Any + Send + Sync + Clone>(&self, pin: OutputPin) -> Option<T> {
self.outputs.get(&pin)?.downcast_ref::<T>().cloned()
}
#[must_use]
pub const fn outputs(&self) -> &IndexMap<OutputPin, BspValue> {
&self.outputs
}
#[must_use]
pub fn get_injected(&self, pin: InputPin) -> Option<&BspValue> {
self.injected.get(&pin)
}
#[must_use]
pub const fn injected(&self) -> &IndexMap<InputPin, BspValue> {
&self.injected
}
pub fn set_output_typed<T: Any + Send + Sync + 'static>(&mut self, pin: OutputPin, value: T) {
self.outputs.insert(pin, BspValue::new(value));
}
pub fn set_injected_typed<T: Any + Send + Sync + 'static>(&mut self, pin: InputPin, value: T) {
self.injected.insert(pin, BspValue::new(value));
}
}
#[derive(Debug)]
pub struct RunState<S> {
pub user: S,
pub(crate) values: ValueStore,
pub(crate) pending_tokens: IndexMap<EdgeId, u64>,
pub(crate) iterations: IndexMap<EdgeId, u64>,
pub(crate) executions: IndexMap<NodeId, u64>,
pub errors: IndexMap<NodeId, BspError>,
pub(crate) frontier: IndexSet<NodeId>,
pub step: u64,
}
impl<S> RunState<S> {
pub(crate) fn new(user: S) -> Self {
Self {
user,
values: ValueStore::default(),
pending_tokens: IndexMap::new(),
iterations: IndexMap::new(),
executions: IndexMap::new(),
errors: IndexMap::new(),
frontier: IndexSet::new(),
step: 0,
}
}
#[must_use]
pub const fn values(&self) -> &ValueStore {
&self.values
}
pub const fn values_mut(&mut self) -> &mut ValueStore {
&mut self.values
}
#[must_use]
pub const fn iterations(&self) -> &IndexMap<EdgeId, u64> {
&self.iterations
}
#[must_use]
pub const fn executions(&self) -> &IndexMap<NodeId, u64> {
&self.executions
}
#[must_use]
pub fn execution_count(&self, node_id: NodeId) -> u64 {
self.executions.get(&node_id).copied().unwrap_or(0)
}
#[must_use]
pub fn iteration_count(&self, edge: EdgeId) -> u64 {
self.iterations.get(&edge).copied().unwrap_or(0)
}
#[must_use]
pub fn get_output_typed<T: Any + Send + Sync + Clone>(&self, pin: OutputPin) -> Option<T> {
self.values.get_output_typed(pin)
}
}
pub struct RunContext<'a, S> {
pub state: &'a mut S,
pub node_id: NodeId,
pub ctx: &'a BspContext,
pub execution_no: u64,
pub(crate) inputs: IndexMap<PortIndex, BspValue>,
pub(crate) outputs: IndexMap<PortIndex, BspValue>,
pub(crate) skip: bool,
}
impl<'a, S> RunContext<'a, S> {
pub(crate) fn new(
state: &'a mut S,
node_id: NodeId,
ctx: &'a BspContext,
execution_no: u64,
inputs: IndexMap<PortIndex, BspValue>,
) -> Self {
Self {
state,
node_id,
ctx,
execution_no,
inputs,
outputs: IndexMap::new(),
skip: false,
}
}
#[must_use]
pub fn input<T: Any + Send + Sync + Clone>(&self, port: PortIndex) -> Option<T> {
self.inputs.get(&port)?.downcast_ref::<T>().cloned()
}
#[must_use]
pub fn input_value(&self, port: PortIndex) -> Option<&BspValue> {
self.inputs.get(&port)
}
pub fn take_input(&mut self, port: PortIndex) -> Option<BspValue> {
self.inputs.swap_remove(&port)
}
pub fn set_output<T: Any + Send + Sync>(&mut self, port: PortIndex, value: T) {
self.outputs.insert(port, BspValue::new(value));
}
pub fn skip_outputs(&mut self) {
self.skip = true;
}
pub(crate) fn take_staged(&mut self) -> (IndexMap<PortIndex, BspValue>, bool) {
(std::mem::take(&mut self.outputs), self.skip)
}
}
#[derive(Debug)]
pub enum EngineResult<S = ()> {
Complete(Box<RunState<S>>),
Suspended(Box<SuspendedExecution<S>>),
StepLimitReached(Box<RunState<S>>),
}
#[derive(Debug)]
pub struct SuspendedExecution<S = ()> {
pub suspensions: Vec<PendingSuspension>,
pub(crate) state: RunState<S>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingSuspension {
pub node_id: NodeId,
pub waiting_inputs: Vec<PortIndex>,
pub payload: serde_json::Value,
}
impl<S> SuspendedExecution<S> {
#[must_use]
pub const fn state(&self) -> &RunState<S> {
&self.state
}
pub const fn state_mut(&mut self) -> &mut RunState<S> {
&mut self.state
}
#[must_use]
pub fn into_state(self) -> RunState<S> {
self.state
}
}
#[derive(Serialize, Deserialize)]
struct SuspensionSnapshot<S> {
suspensions: Vec<PendingSuspension>,
user: S,
pending_tokens: IndexMap<EdgeId, u64>,
iterations: IndexMap<EdgeId, u64>,
executions: IndexMap<NodeId, u64>,
frontier: Vec<NodeId>,
step: u64,
}
impl<S> SuspendedExecution<S>
where
S: Serialize + for<'de> Deserialize<'de>,
{
pub fn to_json(&self) -> Result<String, BspError> {
let snapshot = SuspensionSnapshot {
suspensions: self.suspensions.clone(),
user: &self.state.user,
pending_tokens: self.state.pending_tokens.clone(),
iterations: self.state.iterations.clone(),
executions: self.state.executions.clone(),
frontier: self.state.frontier.iter().copied().collect(),
step: self.state.step,
};
serde_json::to_string(&snapshot).map_err(|e| BspError::SuspensionSerde(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, BspError> {
let snapshot: SuspensionSnapshot<S> =
serde_json::from_str(json).map_err(|e| BspError::SuspensionSerde(e.to_string()))?;
Ok(Self {
suspensions: snapshot.suspensions,
state: RunState {
user: snapshot.user,
values: ValueStore::default(),
pending_tokens: snapshot.pending_tokens,
iterations: snapshot.iterations,
executions: snapshot.executions,
errors: IndexMap::new(),
frontier: snapshot.frontier.into_iter().collect(),
step: snapshot.step,
},
})
}
}
impl<S> BspOutputMapExt for RunState<S> {
fn get_typed<T: Any + Send + Sync + Clone>(&self, pin: OutputPin) -> Result<T, BspError> {
self.values.outputs().get_typed(pin)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_store_roundtrip() {
let mut store = ValueStore::default();
let pin = OutputPin {
node_id: 1,
port: 0,
};
store.set_output_typed(pin, 42_i64);
assert_eq!(store.get_output_typed::<i64>(pin), Some(42));
assert_eq!(store.get_output_typed::<String>(pin), None);
let input = InputPin {
node_id: 2,
port: 1,
};
store.set_injected_typed(input, true);
assert!(store.get_injected(input).is_some());
}
#[test]
fn test_run_state_counters() {
let mut state = RunState::new(());
state.executions.insert(3, 7);
state.iterations.insert(9, 2);
assert_eq!(state.execution_count(3), 7);
assert_eq!(state.execution_count(4), 0);
assert_eq!(state.iteration_count(9), 2);
assert_eq!(state.iteration_count(10), 0);
}
#[test]
fn test_run_context_io() {
let mut user = 0_u64;
let ctx = BspContext::new();
let mut inputs = IndexMap::new();
inputs.insert(0, BspValue::new(21_i64));
{
let mut run = RunContext::new(&mut user, 5, &ctx, 3, inputs);
assert_eq!(run.node_id, 5);
assert_eq!(run.execution_no, 3);
assert_eq!(run.input::<i64>(0), Some(21));
assert_eq!(
run.take_input(0)
.and_then(|v| v.downcast::<i64>().ok())
.map(|a| *a),
Some(21)
);
assert!(run.input_value(0).is_none());
run.set_output(1, "done".to_string());
run.skip_outputs();
let (staged, skip) = run.take_staged();
assert!(skip);
assert!(staged.contains_key(&1));
}
}
#[test]
fn test_suspension_snapshot_roundtrip_preserves_counters() {
let mut state: RunState<String> = RunState::new("consumer-state".to_string());
state.pending_tokens.insert(4, 2);
state.iterations.insert(4, 5);
state.executions.insert(1, 6);
state.frontier.insert(1);
state.step = 11;
let suspended = SuspendedExecution {
suspensions: vec![PendingSuspension {
node_id: 1,
waiting_inputs: vec![0],
payload: serde_json::json!({"why": "test"}),
}],
state,
};
let json = suspended.to_json().unwrap();
assert!(!json.contains("outputs"));
let restored = SuspendedExecution::<String>::from_json(&json).unwrap();
assert_eq!(restored.state.user, "consumer-state");
assert_eq!(restored.state.iteration_count(4), 5);
assert_eq!(restored.state.execution_count(1), 6);
assert_eq!(restored.state.pending_tokens.get(&4), Some(&2));
assert_eq!(restored.state.step, 11);
assert!(restored.state.frontier.contains(&1));
assert_eq!(restored.suspensions[0].waiting_inputs, vec![0]);
}
#[test]
fn test_suspension_snapshot_rejects_garbage() {
let err = SuspendedExecution::<String>::from_json("{not json").unwrap_err();
assert!(matches!(err, BspError::SuspensionSerde(_)));
}
}