Skip to main content

jellyflow_core/ops/transaction/
footprint.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::core::{
6    Binding, BindingEndpoint, BindingId, Edge, EdgeId, GraphId, GraphLocalBindingTarget, GroupId,
7    Node, NodeId, Port, PortId, StickyNoteId, SymbolId,
8};
9
10use super::{batch::GraphTransaction, endpoints::EdgeEndpoints, op::GraphOp};
11
12fn is_false(value: &bool) -> bool {
13    !*value
14}
15
16/// Model ids touched by one graph mutation.
17///
18/// This is intentionally separate from graph storage. Hosts can use it to invalidate indexes,
19/// identify collaboration conflict/dependency boundaries, or derive more specific downstream work.
20#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
21pub struct GraphMutationFootprint {
22    /// The graph document itself was touched.
23    #[serde(default, skip_serializing_if = "is_false")]
24    pub graph: bool,
25    /// Import records or import references touched by the mutation.
26    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
27    pub imports: BTreeSet<GraphId>,
28    /// Symbol records touched by the mutation.
29    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
30    pub symbols: BTreeSet<SymbolId>,
31    /// Node records or node references touched by the mutation.
32    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
33    pub nodes: BTreeSet<NodeId>,
34    /// Port records or port references touched by the mutation.
35    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
36    pub ports: BTreeSet<PortId>,
37    /// Edge records or edge references touched by the mutation.
38    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
39    pub edges: BTreeSet<EdgeId>,
40    /// Group records or group references touched by the mutation.
41    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
42    pub groups: BTreeSet<GroupId>,
43    /// Sticky note records or sticky note references touched by the mutation.
44    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
45    pub sticky_notes: BTreeSet<StickyNoteId>,
46    /// Binding records touched by the mutation.
47    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
48    pub bindings: BTreeSet<BindingId>,
49}
50
51impl GraphMutationFootprint {
52    /// Creates an empty footprint.
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Returns `true` when this footprint does not touch any graph-local object.
58    pub fn is_empty(&self) -> bool {
59        !self.graph
60            && self.imports.is_empty()
61            && self.symbols.is_empty()
62            && self.nodes.is_empty()
63            && self.ports.is_empty()
64            && self.edges.is_empty()
65            && self.groups.is_empty()
66            && self.sticky_notes.is_empty()
67            && self.bindings.is_empty()
68    }
69
70    /// Merges another footprint into this one.
71    pub fn extend(&mut self, other: Self) {
72        self.graph |= other.graph;
73        self.imports.extend(other.imports);
74        self.symbols.extend(other.symbols);
75        self.nodes.extend(other.nodes);
76        self.ports.extend(other.ports);
77        self.edges.extend(other.edges);
78        self.groups.extend(other.groups);
79        self.sticky_notes.extend(other.sticky_notes);
80        self.bindings.extend(other.bindings);
81    }
82
83    /// Marks the whole graph document as touched.
84    pub fn touch_graph(&mut self) {
85        self.graph = true;
86    }
87
88    /// Marks a node id as touched.
89    pub fn touch_node(&mut self, id: NodeId) {
90        self.nodes.insert(id);
91    }
92
93    /// Marks a port id as touched.
94    pub fn touch_port(&mut self, id: PortId) {
95        self.ports.insert(id);
96    }
97
98    /// Marks an edge id as touched.
99    pub fn touch_edge(&mut self, id: EdgeId) {
100        self.edges.insert(id);
101    }
102
103    /// Marks a group id as touched.
104    pub fn touch_group(&mut self, id: GroupId) {
105        self.groups.insert(id);
106    }
107
108    /// Marks a sticky note id as touched.
109    pub fn touch_sticky_note(&mut self, id: StickyNoteId) {
110        self.sticky_notes.insert(id);
111    }
112
113    /// Marks a binding id as touched.
114    pub fn touch_binding(&mut self, id: BindingId) {
115        self.bindings.insert(id);
116    }
117
118    /// Marks an import id as touched.
119    pub fn touch_import(&mut self, id: GraphId) {
120        self.imports.insert(id);
121    }
122
123    /// Marks a symbol id as touched.
124    pub fn touch_symbol(&mut self, id: SymbolId) {
125        self.symbols.insert(id);
126    }
127
128    /// Marks ids referenced by a node snapshot.
129    pub fn touch_node_snapshot(&mut self, id: NodeId, node: &Node) {
130        self.touch_node(id);
131        self.ports.extend(node.ports.iter().copied());
132        if let Some(parent) = node.parent {
133            self.touch_group(parent);
134        }
135    }
136
137    /// Marks ids referenced by a port snapshot.
138    pub fn touch_port_snapshot(&mut self, id: PortId, port: &Port) {
139        self.touch_port(id);
140        self.touch_node(port.node);
141    }
142
143    /// Marks ids referenced by an edge snapshot.
144    pub fn touch_edge_snapshot(&mut self, id: EdgeId, edge: &Edge) {
145        self.touch_edge(id);
146        self.touch_edge_endpoints(EdgeEndpoints::from_edge(edge));
147    }
148
149    /// Marks ids referenced by edge endpoint ports.
150    pub fn touch_edge_endpoints(&mut self, endpoints: EdgeEndpoints) {
151        self.touch_port(endpoints.from);
152        self.touch_port(endpoints.to);
153    }
154
155    /// Marks ids referenced by a binding snapshot.
156    pub fn touch_binding_snapshot(&mut self, id: BindingId, binding: &Binding) {
157        self.touch_binding(id);
158        self.touch_binding_endpoint(&binding.subject);
159        self.touch_binding_endpoint(&binding.target);
160    }
161
162    /// Marks ids referenced by one binding endpoint.
163    pub fn touch_binding_endpoint(&mut self, endpoint: &BindingEndpoint) {
164        let Some(target) = endpoint.graph_local_target() else {
165            return;
166        };
167
168        match target {
169            GraphLocalBindingTarget::Graph => self.touch_graph(),
170            GraphLocalBindingTarget::Node { id } => self.touch_node(id),
171            GraphLocalBindingTarget::Port { id } => self.touch_port(id),
172            GraphLocalBindingTarget::Edge { id } => self.touch_edge(id),
173            GraphLocalBindingTarget::Group { id } => self.touch_group(id),
174            GraphLocalBindingTarget::StickyNote { id } => self.touch_sticky_note(id),
175        }
176    }
177}
178
179impl GraphOp {
180    /// Returns the graph-local ids touched by this operation.
181    pub fn footprint(&self) -> GraphMutationFootprint {
182        let mut footprint = GraphMutationFootprint::new();
183        self.append_footprint(&mut footprint);
184        footprint
185    }
186
187    /// Appends this operation's touched ids to an existing footprint.
188    pub fn append_footprint(&self, footprint: &mut GraphMutationFootprint) {
189        match self {
190            Self::AddNode { id, node } | Self::RemoveNode { id, node, .. } => {
191                footprint.touch_node_snapshot(*id, node);
192            }
193            Self::SetNodePos { id, .. }
194            | Self::SetNodeOrigin { id, .. }
195            | Self::SetNodeKind { id, .. }
196            | Self::SetNodeKindVersion { id, .. }
197            | Self::SetNodeSelectable { id, .. }
198            | Self::SetNodeFocusable { id, .. }
199            | Self::SetNodeDraggable { id, .. }
200            | Self::SetNodeConnectable { id, .. }
201            | Self::SetNodeDeletable { id, .. }
202            | Self::SetNodeExtent { id, .. }
203            | Self::SetNodeExpandParent { id, .. }
204            | Self::SetNodeSize { id, .. }
205            | Self::SetNodeHidden { id, .. }
206            | Self::SetNodeCollapsed { id, .. }
207            | Self::SetNodeData { id, .. } => {
208                footprint.touch_node(*id);
209            }
210            Self::SetNodeParent { id, from, to } => {
211                footprint.touch_node(*id);
212                if let Some(parent) = from {
213                    footprint.touch_group(*parent);
214                }
215                if let Some(parent) = to {
216                    footprint.touch_group(*parent);
217                }
218            }
219            Self::SetNodePorts { id, from, to } => {
220                footprint.touch_node(*id);
221                footprint.ports.extend(from.iter().copied());
222                footprint.ports.extend(to.iter().copied());
223            }
224
225            Self::AddPort { id, port } | Self::RemovePort { id, port, .. } => {
226                footprint.touch_port_snapshot(*id, port);
227            }
228            Self::SetPortConnectable { id, .. }
229            | Self::SetPortConnectableStart { id, .. }
230            | Self::SetPortConnectableEnd { id, .. }
231            | Self::SetPortType { id, .. }
232            | Self::SetPortData { id, .. } => {
233                footprint.touch_port(*id);
234            }
235
236            Self::AddEdge { id, edge } | Self::RemoveEdge { id, edge, .. } => {
237                footprint.touch_edge_snapshot(*id, edge);
238            }
239            Self::SetEdgeKind { id, .. }
240            | Self::SetEdgeSelectable { id, .. }
241            | Self::SetEdgeFocusable { id, .. }
242            | Self::SetEdgeHidden { id, .. }
243            | Self::SetEdgeInteractionWidth { id, .. }
244            | Self::SetEdgeDeletable { id, .. }
245            | Self::SetEdgeReconnectable { id, .. }
246            | Self::SetEdgeData { id, .. }
247            | Self::SetEdgeView { id, .. } => {
248                footprint.touch_edge(*id);
249            }
250            Self::SetEdgeEndpoints { id, from, to } => {
251                footprint.touch_edge(*id);
252                footprint.touch_edge_endpoints(*from);
253                footprint.touch_edge_endpoints(*to);
254            }
255
256            Self::AddImport { id, .. }
257            | Self::RemoveImport { id, .. }
258            | Self::SetImportAlias { id, .. } => {
259                footprint.touch_import(*id);
260            }
261
262            Self::AddSymbol { id, .. }
263            | Self::RemoveSymbol { id, .. }
264            | Self::SetSymbolName { id, .. }
265            | Self::SetSymbolType { id, .. }
266            | Self::SetSymbolDefaultValue { id, .. }
267            | Self::SetSymbolMeta { id, .. } => {
268                footprint.touch_symbol(*id);
269            }
270
271            Self::AddGroup { id, .. } | Self::SetGroupRect { id, .. } => {
272                footprint.touch_group(*id);
273            }
274            Self::RemoveGroup {
275                id,
276                detached,
277                bindings,
278                ..
279            } => {
280                footprint.touch_group(*id);
281                for (node, previous_parent) in detached {
282                    footprint.touch_node(*node);
283                    if let Some(previous_parent) = previous_parent {
284                        footprint.touch_group(*previous_parent);
285                    }
286                }
287                for (binding_id, binding) in bindings {
288                    footprint.touch_binding_snapshot(*binding_id, binding);
289                }
290            }
291            Self::SetGroupTitle { id, .. } | Self::SetGroupColor { id, .. } => {
292                footprint.touch_group(*id);
293            }
294
295            Self::AddStickyNote { id, .. }
296            | Self::SetStickyNoteText { id, .. }
297            | Self::SetStickyNoteRect { id, .. }
298            | Self::SetStickyNoteColor { id, .. } => {
299                footprint.touch_sticky_note(*id);
300            }
301            Self::RemoveStickyNote { id, bindings, .. } => {
302                footprint.touch_sticky_note(*id);
303                for (binding_id, binding) in bindings {
304                    footprint.touch_binding_snapshot(*binding_id, binding);
305                }
306            }
307
308            Self::AddBinding { id, binding } | Self::RemoveBinding { id, binding } => {
309                footprint.touch_binding_snapshot(*id, binding);
310            }
311            Self::SetBindingSubject { id, from, to } | Self::SetBindingTarget { id, from, to } => {
312                footprint.touch_binding(*id);
313                footprint.touch_binding_endpoint(from);
314                footprint.touch_binding_endpoint(to);
315            }
316            Self::SetBindingKind { id, .. } | Self::SetBindingMeta { id, .. } => {
317                footprint.touch_binding(*id);
318            }
319        }
320
321        self.append_cascaded_footprint(footprint);
322    }
323
324    fn append_cascaded_footprint(&self, footprint: &mut GraphMutationFootprint) {
325        match self {
326            Self::RemoveNode {
327                ports,
328                edges,
329                bindings,
330                ..
331            } => {
332                for (port_id, port) in ports {
333                    footprint.touch_port_snapshot(*port_id, port);
334                }
335                for (edge_id, edge) in edges {
336                    footprint.touch_edge_snapshot(*edge_id, edge);
337                }
338                for (binding_id, binding) in bindings {
339                    footprint.touch_binding_snapshot(*binding_id, binding);
340                }
341            }
342            Self::RemovePort {
343                edges, bindings, ..
344            } => {
345                for (edge_id, edge) in edges {
346                    footprint.touch_edge_snapshot(*edge_id, edge);
347                }
348                for (binding_id, binding) in bindings {
349                    footprint.touch_binding_snapshot(*binding_id, binding);
350                }
351            }
352            Self::RemoveEdge { bindings, .. } => {
353                for (binding_id, binding) in bindings {
354                    footprint.touch_binding_snapshot(*binding_id, binding);
355                }
356            }
357            _ => {}
358        }
359    }
360}
361
362impl GraphTransaction {
363    /// Returns the graph-local ids touched by all operations in this transaction.
364    pub fn footprint(&self) -> GraphMutationFootprint {
365        let mut footprint = GraphMutationFootprint::new();
366        self.append_footprint(&mut footprint);
367        footprint
368    }
369
370    /// Appends this transaction's touched ids to an existing footprint.
371    pub fn append_footprint(&self, footprint: &mut GraphMutationFootprint) {
372        for op in self.ops() {
373            op.append_footprint(footprint);
374        }
375    }
376}