Skip to main content

jellyflow_runtime/io/files/
editor_state.rs

1use std::path::Path;
2
3use jellyflow_core::core::GraphId;
4use serde::{Deserialize, Serialize};
5
6use crate::io::config::NodeGraphEditorConfig;
7use crate::io::view_state::{NodeGraphPureViewState, NodeGraphViewState};
8
9/// Editor-state file format version.
10pub const EDITOR_STATE_FILE_VERSION: u32 = 1;
11
12/// Errors for reading/writing editor-state files.
13#[derive(Debug, thiserror::Error)]
14pub enum NodeGraphEditorStateFileError {
15    /// Read failure.
16    #[error("failed to read node graph editor-state file: {path}")]
17    Read {
18        path: String,
19        source: std::io::Error,
20    },
21    /// JSON parse failure.
22    #[error("failed to parse node graph editor-state file JSON: {path}")]
23    Parse {
24        path: String,
25        source: serde_json::Error,
26    },
27    /// Write failure.
28    #[error("failed to write node graph editor-state file: {path}")]
29    Write {
30        path: String,
31        source: std::io::Error,
32    },
33    /// JSON serialization failure.
34    #[error("failed to serialize node graph editor-state JSON: {path}")]
35    Serialize {
36        path: String,
37        source: serde_json::Error,
38    },
39    /// Wrapper id mismatch.
40    #[error("editor-state file wrapper graph_id does not match requested graph_id")]
41    InconsistentGraphId,
42    /// Unsupported wrapper version.
43    #[error("unsupported node graph editor-state version {version}; expected {expected}")]
44    UnsupportedVersion { version: u32, expected: u32 },
45}
46
47/// Project-scoped editor-state persistence file.
48///
49/// The graph document is saved separately by `GraphFileV1`; this file owns only user/editor state:
50/// pure canvas view state plus persisted editor policy and runtime tuning.
51#[derive(Debug, Clone, PartialEq)]
52pub struct NodeGraphEditorStateFile {
53    /// Graph id.
54    pub graph_id: GraphId,
55    /// Editor-state schema version.
56    pub editor_state_version: u32,
57    /// Pure view-state payload.
58    pub view_state: NodeGraphViewState,
59    /// Persisted editor policy and runtime tuning.
60    pub editor_config: NodeGraphEditorConfig,
61}
62
63impl NodeGraphEditorStateFile {
64    /// Wraps editor state for a graph.
65    pub fn new(
66        graph_id: GraphId,
67        view_state: NodeGraphViewState,
68        editor_config: NodeGraphEditorConfig,
69    ) -> Self {
70        Self {
71            graph_id,
72            editor_state_version: EDITOR_STATE_FILE_VERSION,
73            view_state,
74            editor_config,
75        }
76    }
77
78    /// Loads a JSON file.
79    pub fn load_json(
80        path: impl AsRef<Path>,
81        graph_id: GraphId,
82    ) -> Result<Self, NodeGraphEditorStateFileError> {
83        let path = path.as_ref();
84        let bytes = std::fs::read(path).map_err(|source| NodeGraphEditorStateFileError::Read {
85            path: path.display().to_string(),
86            source,
87        })?;
88
89        let persisted: PersistedNodeGraphEditorStateFile =
90            serde_json::from_slice(&bytes).map_err(|source| {
91                NodeGraphEditorStateFileError::Parse {
92                    path: path.display().to_string(),
93                    source,
94                }
95            })?;
96        persisted.validate_for_graph(graph_id)?;
97        Ok(persisted.into_editor_state_file())
98    }
99
100    /// Loads the JSON file if it exists.
101    pub fn load_json_if_exists(
102        path: impl AsRef<Path>,
103        graph_id: GraphId,
104    ) -> Result<Option<Self>, NodeGraphEditorStateFileError> {
105        let path = path.as_ref();
106        if !path.exists() {
107            return Ok(None);
108        }
109        Self::load_json(path, graph_id).map(Some)
110    }
111
112    /// Saves the JSON file (pretty-printed).
113    pub fn save_json(&self, path: impl AsRef<Path>) -> Result<(), NodeGraphEditorStateFileError> {
114        let path = path.as_ref();
115        if let Some(parent) = path.parent() {
116            std::fs::create_dir_all(parent).map_err(|source| {
117                NodeGraphEditorStateFileError::Write {
118                    path: path.display().to_string(),
119                    source,
120                }
121            })?;
122        }
123        let persisted = PersistedNodeGraphEditorStateFile::from_editor_state_file(self);
124        let bytes = serde_json::to_vec_pretty(&persisted).map_err(|source| {
125            NodeGraphEditorStateFileError::Serialize {
126                path: path.display().to_string(),
127                source,
128            }
129        })?;
130        std::fs::write(path, bytes).map_err(|source| NodeGraphEditorStateFileError::Write {
131            path: path.display().to_string(),
132            source,
133        })
134    }
135}
136
137#[derive(Serialize, Deserialize)]
138struct PersistedNodeGraphEditorStateFile {
139    graph_id: GraphId,
140    editor_state_version: u32,
141    view_state: NodeGraphPureViewState,
142    #[serde(default, skip_serializing_if = "NodeGraphEditorConfig::is_default")]
143    editor_config: NodeGraphEditorConfig,
144}
145
146impl PersistedNodeGraphEditorStateFile {
147    fn from_editor_state_file(file: &NodeGraphEditorStateFile) -> Self {
148        Self {
149            graph_id: file.graph_id,
150            editor_state_version: EDITOR_STATE_FILE_VERSION,
151            view_state: NodeGraphPureViewState::from(&file.view_state),
152            editor_config: file.editor_config.clone(),
153        }
154    }
155
156    fn validate_for_graph(&self, graph_id: GraphId) -> Result<(), NodeGraphEditorStateFileError> {
157        if self.graph_id != graph_id {
158            return Err(NodeGraphEditorStateFileError::InconsistentGraphId);
159        }
160        if self.editor_state_version != EDITOR_STATE_FILE_VERSION {
161            return Err(NodeGraphEditorStateFileError::UnsupportedVersion {
162                version: self.editor_state_version,
163                expected: EDITOR_STATE_FILE_VERSION,
164            });
165        }
166        Ok(())
167    }
168
169    fn into_editor_state_file(self) -> NodeGraphEditorStateFile {
170        let mut view_state = NodeGraphViewState::from(self.view_state);
171        view_state.sanitize_viewport();
172        NodeGraphEditorStateFile {
173            graph_id: self.graph_id,
174            editor_state_version: self.editor_state_version,
175            view_state,
176            editor_config: self.editor_config,
177        }
178    }
179}