1use std::fs;
4use std::path::{Path, PathBuf};
5
6use parking_lot::RwLock;
7use serde::{de::DeserializeOwned, Serialize};
8use serde_json::Value;
9
10use crate::core::{Result, ResumaError};
11
12use super::types::{GraphId, GraphSnapshot, WorkerEvent};
13
14static ROOT: RwLock<Option<PathBuf>> = RwLock::new(None);
15
16pub fn configure(root: impl AsRef<Path>) {
18 let p = root.as_ref().to_path_buf();
19 let _ = fs::create_dir_all(&p);
20 *ROOT.write() = Some(p);
21}
22
23fn root_dir() -> PathBuf {
24 ROOT.read()
25 .clone()
26 .unwrap_or_else(|| PathBuf::from(".resuma/durable"))
27}
28
29fn key_path(namespace: &str, key: &str) -> PathBuf {
30 let safe_ns = sanitize(namespace);
31 let safe_key = sanitize(key);
32 root_dir().join(safe_ns).join(format!("{safe_key}.json"))
33}
34
35fn sanitize(s: &str) -> String {
36 s.chars()
37 .map(|c| {
38 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
39 c
40 } else {
41 '_'
42 }
43 })
44 .collect()
45}
46
47pub fn get(namespace: &str, key: &str) -> Option<Value> {
49 let path = key_path(namespace, key);
50 let data = fs::read_to_string(path).ok()?;
51 serde_json::from_str(&data).ok()
52}
53
54pub fn set(namespace: &str, key: &str, value: &Value) -> Result<()> {
56 use std::io::Write;
57 let path = key_path(namespace, key);
58 if let Some(parent) = path.parent() {
59 fs::create_dir_all(parent).map_err(ResumaError::Io)?;
60 }
61 let data = serde_json::to_string_pretty(value)?;
62 let tmp = path.with_extension("json.tmp");
63 {
64 let mut f = fs::File::create(&tmp).map_err(ResumaError::Io)?;
65 f.write_all(data.as_bytes()).map_err(ResumaError::Io)?;
66 f.sync_all().map_err(ResumaError::Io)?;
67 }
68 fs::rename(&tmp, &path).map_err(ResumaError::Io)
69}
70
71pub fn get_typed<T: DeserializeOwned>(namespace: &str, key: &str) -> Result<Option<T>> {
73 match get(namespace, key) {
74 Some(v) => Ok(Some(serde_json::from_value(v)?)),
75 None => Ok(None),
76 }
77}
78
79pub fn set_typed<T: Serialize>(namespace: &str, key: &str, value: &T) -> Result<()> {
80 set(namespace, key, &serde_json::to_value(value)?)
81}
82
83const GRAPHS_NS: &str = "graphs";
84const EVENTS_NS: &str = "events";
85const CHECKPOINTS_NS: &str = "checkpoints";
86
87pub fn persist_graph(snapshot: &GraphSnapshot) -> Result<()> {
89 set(GRAPHS_NS, &snapshot.id.0, &serde_json::to_value(snapshot)?)
90}
91
92pub fn load_graph(id: &GraphId) -> Option<GraphSnapshot> {
93 get(GRAPHS_NS, &id.0).and_then(|v| serde_json::from_value(v).ok())
94}
95
96pub fn persist_events(id: &GraphId, events: &[WorkerEvent]) -> Result<()> {
98 set(EVENTS_NS, &id.0, &serde_json::to_value(events)?)
99}
100
101pub fn load_events(id: &GraphId) -> Option<Vec<WorkerEvent>> {
102 get(EVENTS_NS, &id.0).and_then(|v| serde_json::from_value(v).ok())
103}
104
105pub fn save_checkpoint(id: &GraphId, state: &super::state::StateStore) -> Result<()> {
107 set(
108 CHECKPOINTS_NS,
109 &id.0,
110 &serde_json::to_value(state.snapshot())?,
111 )
112}
113
114pub fn load_checkpoint(id: &GraphId) -> Option<super::state::StateStore> {
115 let map = get(CHECKPOINTS_NS, &id.0)?;
116 let store = super::state::StateStore::default();
117 if let Some(obj) = map.as_object() {
118 for (k, v) in obj {
119 store.set(k.clone(), v.clone());
120 }
121 }
122 Some(store)
123}
124
125const EXECUTIONS_NS: &str = "executions";
126
127#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
129pub struct ExecutionRecord {
130 pub graph_id: GraphId,
131 pub worker: String,
132 pub input: Value,
133 pub plan: super::types::ExecutionPlan,
134 pub profile: super::resources::ResourceProfile,
135 pub paused: bool,
136 #[serde(default)]
138 pub cancelled: bool,
139}
140
141pub fn save_execution_record(record: &ExecutionRecord) -> Result<()> {
142 set(
143 EXECUTIONS_NS,
144 &record.graph_id.0,
145 &serde_json::to_value(record)?,
146 )
147}
148
149pub fn load_execution_record(id: &GraphId) -> Option<ExecutionRecord> {
150 get(EXECUTIONS_NS, &id.0).and_then(|v| serde_json::from_value(v).ok())
151}
152
153const TOKENS_NS: &str = "tokens";
154
155pub fn save_graph_token(id: &GraphId, token: &str) -> Result<()> {
157 set(TOKENS_NS, &id.0, &serde_json::json!({ "token": token }))
158}
159
160pub fn load_graph_token(id: &GraphId) -> Option<String> {
161 get(TOKENS_NS, &id.0).and_then(|v| v.get("token").and_then(|t| t.as_str()).map(str::to_string))
162}