jellyflow-runtime 0.2.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use crate::runtime::commit::NodeGraphPatch;
use crate::runtime::xyflow::callbacks::{ConnectionChange, DeleteChange};
use crate::runtime::xyflow::changes::NodeGraphChanges;
use jellyflow_core::ops::GraphTransaction;

use super::connections::ConnectionChangeAccumulator;
use super::deletes::DeleteChangeAccumulator;
use super::node_graph::NodeGraphChangeAccumulator;

#[derive(Debug)]
pub(in crate::runtime::xyflow) struct XyFlowCommitProjection {
    node_edge_changes: NodeGraphChanges,
    connection_changes: Vec<ConnectionChange>,
    delete_change: DeleteChange,
}

impl XyFlowCommitProjection {
    pub(in crate::runtime::xyflow) fn from_patch(patch: &NodeGraphPatch) -> Self {
        Self::from_transaction(patch.transaction())
    }

    pub(in crate::runtime::xyflow) fn from_transaction(tx: &GraphTransaction) -> Self {
        let mut node_graph = NodeGraphChangeAccumulator::new();
        let mut connections = ConnectionChangeAccumulator::new(tx.len());
        let mut deletes = DeleteChangeAccumulator::default();

        for op in tx.ops() {
            node_graph.push_op(op);
            connections.push_op(op);
            deletes.push_op(op);
        }

        Self {
            node_edge_changes: node_graph.finish(),
            connection_changes: connections.finish(),
            delete_change: deletes.finish(),
        }
    }

    pub(in crate::runtime::xyflow) fn node_edge_changes(&self) -> &NodeGraphChanges {
        &self.node_edge_changes
    }

    pub(in crate::runtime::xyflow) fn connection_changes(&self) -> &[ConnectionChange] {
        &self.connection_changes
    }

    pub(in crate::runtime::xyflow) fn delete_change(&self) -> &DeleteChange {
        &self.delete_change
    }

    pub(in crate::runtime::xyflow) fn into_node_edge_changes(self) -> NodeGraphChanges {
        self.node_edge_changes
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::runtime::xyflow::callbacks::ConnectionChange;
    use crate::runtime::xyflow::changes::{EdgeChange, NodeChange};
    use jellyflow_core::core::{
        CanvasPoint, CanvasRect, CanvasSize, Edge, EdgeId, EdgeKind, Group, GroupId, Node, NodeId,
        NodeKindKey, Port, PortCapacity, PortDirection, PortId, PortKey, PortKind, StickyNote,
        StickyNoteId,
    };
    use jellyflow_core::ops::{EdgeEndpoints, GraphOp};

    #[test]
    fn commit_projection_maps_edge_connection_ops() {
        let edge = TestEdgeFixture::new(PortId::new(), PortId::new());
        let unchanged = edge.endpoints();

        let tx = GraphTransaction::from_ops([
            GraphOp::AddEdge {
                id: edge.id,
                edge: edge.edge(),
            },
            GraphOp::SetEdgeEndpoints {
                id: edge.id,
                from: unchanged,
                to: unchanged,
            },
            GraphOp::RemoveEdge {
                id: edge.id,
                edge: edge.edge(),
                bindings: Vec::new(),
            },
        ]);

        let projection = XyFlowCommitProjection::from_transaction(&tx);

        assert_eq!(projection.connection_changes().len(), 3);
        assert!(matches!(
            projection.connection_changes()[0],
            ConnectionChange::Connected(_)
        ));
        assert!(matches!(
            projection.connection_changes()[1],
            ConnectionChange::Reconnected { .. }
        ));
        assert!(matches!(
            projection.connection_changes()[2],
            ConnectionChange::Disconnected(_)
        ));
    }

    #[test]
    fn commit_projection_resets_connection_remove_dedup_after_edge_add() {
        let edge = TestEdgeFixture::new(PortId::new(), PortId::new());

        let tx = GraphTransaction::from_ops([
            GraphOp::RemoveEdge {
                id: edge.id,
                edge: edge.edge(),
                bindings: Vec::new(),
            },
            GraphOp::AddEdge {
                id: edge.id,
                edge: edge.edge(),
            },
            GraphOp::RemoveEdge {
                id: edge.id,
                edge: edge.edge(),
                bindings: Vec::new(),
            },
        ]);

        let projection = XyFlowCommitProjection::from_transaction(&tx);

        assert!(matches!(
            projection.connection_changes(),
            [
                ConnectionChange::Disconnected(first),
                ConnectionChange::Connected(connected),
                ConnectionChange::Disconnected(second),
            ] if first.edge == edge.id && connected.edge == edge.id && second.edge == edge.id
        ));
    }

    #[test]
    fn commit_projection_maps_resource_deletions_and_cascaded_edges() {
        let node = TestNodeFixture::new();
        let edge = TestEdgeFixture::new(node.port, PortId::new());
        let group = TestGroupFixture::new("group");
        let sticky_note = TestStickyNoteFixture::new("note");

        let tx = GraphTransaction::from_ops([
            GraphOp::RemoveNode {
                id: node.id,
                node: node.node(),
                ports: vec![(node.port, node.port())],
                edges: vec![(edge.id, edge.edge())],
                bindings: Vec::new(),
            },
            GraphOp::RemovePort {
                id: node.port,
                port: node.port(),
                edges: vec![(edge.id, edge.edge())],
                bindings: Vec::new(),
            },
            GraphOp::RemoveGroup {
                id: group.id,
                group: group.group(),
                detached: vec![(node.id, None)],
                bindings: Vec::new(),
            },
            GraphOp::RemoveStickyNote {
                id: sticky_note.id,
                note: sticky_note.note(),
                bindings: Vec::new(),
            },
        ]);

        let projection = XyFlowCommitProjection::from_transaction(&tx);
        let delete_change = projection.delete_change();

        assert!(
            projection
                .node_edge_changes()
                .nodes()
                .iter()
                .any(|change| matches!(change, NodeChange::Remove { id } if *id == node.id))
        );
        assert!(
            matches!(projection.node_edge_changes().edges(), [EdgeChange::Remove { id }] if *id == edge.id)
        );
        assert!(
            matches!(projection.connection_changes(), [ConnectionChange::Disconnected(conn)] if conn.edge == edge.id)
        );
        assert_eq!(delete_change.nodes(), &[node.id]);
        assert_eq!(delete_change.edges(), &[edge.id]);
        assert_eq!(delete_change.groups(), &[group.id]);
        assert_eq!(delete_change.sticky_notes(), &[sticky_note.id]);
    }

    struct TestNodeFixture {
        id: NodeId,
        port: PortId,
    }

    impl TestNodeFixture {
        fn new() -> Self {
            Self {
                id: NodeId::new(),
                port: PortId::new(),
            }
        }

        fn node(&self) -> Node {
            Node {
                kind: NodeKindKey::new("test.node"),
                kind_version: 1,
                pos: CanvasPoint { x: 0.0, y: 0.0 },
                origin: None,
                selectable: None,
                focusable: None,
                draggable: None,
                connectable: None,
                deletable: None,
                parent: None,
                extent: None,
                expand_parent: None,
                size: None,
                hidden: false,
                collapsed: false,
                ports: vec![self.port],
                data: serde_json::Value::Null,
            }
        }

        fn port(&self) -> Port {
            Port {
                node: self.id,
                key: PortKey::new("out"),
                dir: PortDirection::Out,
                kind: PortKind::Data,
                capacity: PortCapacity::Multi,
                connectable: None,
                connectable_start: None,
                connectable_end: None,
                ty: None,
                data: serde_json::Value::Null,
            }
        }
    }

    struct TestEdgeFixture {
        id: EdgeId,
        from: PortId,
        to: PortId,
    }

    impl TestEdgeFixture {
        fn new(from: PortId, to: PortId) -> Self {
            Self {
                id: EdgeId::new(),
                from,
                to,
            }
        }

        fn edge(&self) -> Edge {
            Edge {
                kind: EdgeKind::Data,
                from: self.from,
                to: self.to,
                hidden: false,
                selectable: None,
                focusable: None,
                interaction_width: None,
                deletable: None,
                reconnectable: None,
            }
        }

        fn endpoints(&self) -> EdgeEndpoints {
            EdgeEndpoints {
                from: self.from,
                to: self.to,
            }
        }
    }

    struct TestGroupFixture {
        id: GroupId,
        title: &'static str,
    }

    impl TestGroupFixture {
        fn new(title: &'static str) -> Self {
            Self {
                id: GroupId::new(),
                title,
            }
        }

        fn group(&self) -> Group {
            Group {
                title: self.title.to_owned(),
                rect: test_rect(),
                color: None,
            }
        }
    }

    struct TestStickyNoteFixture {
        id: StickyNoteId,
        text: &'static str,
    }

    impl TestStickyNoteFixture {
        fn new(text: &'static str) -> Self {
            Self {
                id: StickyNoteId::new(),
                text,
            }
        }

        fn note(&self) -> StickyNote {
            StickyNote {
                text: self.text.to_owned(),
                rect: test_rect(),
                color: None,
            }
        }
    }

    fn test_rect() -> CanvasRect {
        CanvasRect {
            origin: CanvasPoint { x: 0.0, y: 0.0 },
            size: CanvasSize {
                width: 10.0,
                height: 10.0,
            },
        }
    }
}