Skip to main content

jellyflow_runtime/runtime/store/
mod.rs

1//! Headless runtime store (B-layer) for node graphs.
2//!
3//! This is the ergonomic "single entry point" that B-layer consumers want:
4//! - authoritative `Graph` (serializable document),
5//! - per-user/per-project `NodeGraphViewState` (pan/zoom/selection),
6//! - undo/redo history (`GraphHistory`),
7//! - dispatch methods that return a full-fidelity `NodeGraphPatch`.
8
9mod dispatch;
10mod dispatch_profile;
11mod events;
12mod history;
13mod snapshot;
14mod subscriptions;
15mod view;
16
17use std::cell::RefCell;
18
19use crate::io::{
20    NodeGraphEditorConfig, NodeGraphInteractionConfig, NodeGraphRuntimeTuning, NodeGraphViewState,
21};
22use crate::profile::{ApplyPipelineError, GraphProfile};
23use crate::runtime::commit::NodeGraphPatch;
24use crate::runtime::lookups::NodeGraphLookups;
25use crate::runtime::middleware::NodeGraphStoreMiddleware;
26use crate::runtime::query::spatial::SpatialQueryCache;
27use jellyflow_core::core::Graph;
28use jellyflow_core::ops::{GraphHistory, GraphMutationFootprint, GraphTransaction};
29
30/// Dispatch outcome for store actions.
31#[derive(Debug, Clone)]
32pub struct DispatchOutcome {
33    /// Full-fidelity patch that was committed.
34    patch: NodeGraphPatch,
35}
36
37impl DispatchOutcome {
38    pub fn new(patch: NodeGraphPatch) -> Self {
39        Self { patch }
40    }
41
42    pub fn from_committed(committed: GraphTransaction) -> Self {
43        Self::new(NodeGraphPatch::new(committed))
44    }
45
46    pub fn patch(&self) -> &NodeGraphPatch {
47        &self.patch
48    }
49
50    pub fn committed(&self) -> &GraphTransaction {
51        self.patch.transaction()
52    }
53
54    pub fn footprint(&self) -> &GraphMutationFootprint {
55        self.patch.footprint()
56    }
57
58    pub fn into_patch(self) -> NodeGraphPatch {
59        self.patch
60    }
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum DispatchError {
65    #[error(transparent)]
66    Apply(#[from] ApplyPipelineError),
67}
68
69/// Minimal B-layer store.
70///
71/// This is intentionally headless-safe and does not depend on `fret-ui`.
72pub struct NodeGraphStore {
73    graph: Graph,
74    graph_revision: u64,
75    layout_facts_revision: u64,
76    view_state: NodeGraphViewState,
77    interaction: NodeGraphInteractionConfig,
78    runtime_tuning: NodeGraphRuntimeTuning,
79    history: GraphHistory,
80    profile: Option<Box<dyn GraphProfile>>,
81    middleware: Option<Box<dyn NodeGraphStoreMiddleware>>,
82    lookups: NodeGraphLookups,
83    spatial_query_cache: RefCell<SpatialQueryCache>,
84    subscriptions: subscriptions::StoreSubscriptions,
85}
86
87impl std::fmt::Debug for NodeGraphStore {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("NodeGraphStore")
90            .field("graph_id", &self.graph.graph_id())
91            .field("graph_revision", &self.graph_revision)
92            .field("layout_facts_revision", &self.layout_facts_revision)
93            .field("node_count", &self.graph.nodes().len())
94            .field("edge_count", &self.graph.edges().len())
95            .field("lookup_node_count", &self.lookups.node_count())
96            .field("lookup_edge_count", &self.lookups.edge_count())
97            .field("undo_len", &self.history.undo_len())
98            .field("redo_len", &self.history.redo_len())
99            .field("has_profile", &self.profile.is_some())
100            .field(
101                "event_subscription_count",
102                &self.subscriptions.event_subscription_count(),
103            )
104            .field(
105                "gesture_subscription_count",
106                &self.subscriptions.gesture_subscription_count(),
107            )
108            .field(
109                "selector_subscription_count",
110                &self.subscriptions.selector_subscription_count(),
111            )
112            .finish()
113    }
114}
115
116impl NodeGraphStore {
117    /// Creates a store with an explicit editor configuration payload.
118    pub fn new(
119        graph: Graph,
120        view_state: NodeGraphViewState,
121        editor_config: NodeGraphEditorConfig,
122    ) -> Self {
123        Self::new_with_optional_profile(graph, view_state, editor_config, None)
124    }
125
126    /// Creates a store with a profile pipeline (apply -> concretize -> validate).
127    pub fn with_profile(
128        graph: Graph,
129        view_state: NodeGraphViewState,
130        editor_config: NodeGraphEditorConfig,
131        profile: Box<dyn GraphProfile>,
132    ) -> Self {
133        Self::new_with_optional_profile(graph, view_state, editor_config, Some(profile))
134    }
135
136    fn new_with_optional_profile(
137        graph: Graph,
138        mut view_state: NodeGraphViewState,
139        editor_config: NodeGraphEditorConfig,
140        profile: Option<Box<dyn GraphProfile>>,
141    ) -> Self {
142        view_state.sanitize_for_graph(&graph);
143        let mut lookups = NodeGraphLookups::default();
144        lookups.rebuild_from(&graph);
145        let (interaction, runtime_tuning) = editor_config.into_parts();
146        Self {
147            graph,
148            graph_revision: 0,
149            layout_facts_revision: 0,
150            view_state,
151            interaction,
152            runtime_tuning,
153            history: GraphHistory::default(),
154            profile,
155            middleware: None,
156            lookups,
157            spatial_query_cache: RefCell::new(SpatialQueryCache::default()),
158            subscriptions: subscriptions::StoreSubscriptions::default(),
159        }
160    }
161
162    pub(crate) fn spatial_query_cache(&self) -> &RefCell<SpatialQueryCache> {
163        &self.spatial_query_cache
164    }
165
166    pub fn with_middleware(mut self, middleware: impl NodeGraphStoreMiddleware) -> Self {
167        self.middleware = Some(Box::new(middleware));
168        self
169    }
170}