Skip to main content

jellyflow_runtime/runtime/
commit.rs

1//! Canonical runtime commit payloads.
2//!
3//! Store commits are represented as reversible graph transactions. Adapter-specific projections,
4//! such as XyFlow-style node/edge changes, are derived outside this module.
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8use jellyflow_core::ops::{GraphMutationFootprint, GraphOp, GraphTransaction};
9
10/// Full-fidelity committed graph patch.
11///
12/// This is the primary commit payload for controlled integrations. It preserves every
13/// `GraphOp`, including ports, groups, sticky notes, imports, symbols, and other resources.
14#[derive(Debug, Clone, Default, Serialize)]
15pub struct NodeGraphPatch {
16    /// Reversible transaction committed by the store.
17    transaction: GraphTransaction,
18    /// Cached ids touched by `transaction`.
19    #[serde(default, skip_serializing_if = "GraphMutationFootprint::is_empty")]
20    footprint: GraphMutationFootprint,
21}
22
23impl NodeGraphPatch {
24    pub fn new(transaction: GraphTransaction) -> Self {
25        let footprint = transaction.footprint();
26        Self {
27            transaction,
28            footprint,
29        }
30    }
31
32    pub fn transaction(&self) -> &GraphTransaction {
33        &self.transaction
34    }
35
36    pub fn footprint(&self) -> &GraphMutationFootprint {
37        &self.footprint
38    }
39
40    pub fn into_transaction(self) -> GraphTransaction {
41        self.transaction
42    }
43
44    pub fn into_parts(self) -> (GraphTransaction, GraphMutationFootprint) {
45        (self.transaction, self.footprint)
46    }
47
48    pub fn ops(&self) -> &[GraphOp] {
49        self.transaction.ops()
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.transaction.is_empty()
54    }
55}
56
57impl From<GraphTransaction> for NodeGraphPatch {
58    fn from(transaction: GraphTransaction) -> Self {
59        Self::new(transaction)
60    }
61}
62
63impl<'de> Deserialize<'de> for NodeGraphPatch {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: Deserializer<'de>,
67    {
68        #[derive(Deserialize)]
69        struct NodeGraphPatchWire {
70            #[serde(default)]
71            transaction: GraphTransaction,
72            #[serde(default)]
73            footprint: Option<GraphMutationFootprint>,
74        }
75
76        let wire = NodeGraphPatchWire::deserialize(deserializer)?;
77        let _ = wire.footprint;
78        Ok(Self::new(wire.transaction))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use jellyflow_core::core::{CanvasPoint, NodeId};
86
87    #[test]
88    fn patch_caches_transaction_footprint() {
89        let node = NodeId::from_u128(1);
90        let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
91            id: node,
92            from: CanvasPoint { x: 0.0, y: 0.0 },
93            to: CanvasPoint { x: 10.0, y: 20.0 },
94        }]);
95
96        let patch = NodeGraphPatch::new(tx.clone());
97
98        assert_eq!(patch.footprint(), &tx.footprint());
99        assert!(patch.footprint().nodes.contains(&node));
100    }
101
102    #[test]
103    fn patch_deserialization_rebuilds_missing_or_stale_footprint() {
104        let node = NodeId::from_u128(1);
105        let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
106            id: node,
107            from: CanvasPoint { x: 0.0, y: 0.0 },
108            to: CanvasPoint { x: 10.0, y: 20.0 },
109        }]);
110        let encoded = serde_json::json!({ "transaction": tx });
111
112        let patch: NodeGraphPatch = serde_json::from_value(encoded).expect("deserialize patch");
113
114        assert!(patch.footprint().nodes.contains(&node));
115    }
116}