1use std::cell::RefCell;
18use std::collections::BTreeMap;
19use std::path::{Path, PathBuf};
20
21use crate::stdlib::macros::{harn_builtin, VmBuiltinDef};
22use crate::value::{VmError, VmValue};
23use crate::vm::Vm;
24
25struct CheckpointState {
26 data: BTreeMap<String, serde_json::Value>,
27 path: PathBuf,
28 loaded: bool,
29}
30
31impl CheckpointState {
32 fn new(base_dir: &Path, pipeline_name: &str) -> Self {
33 Self {
34 data: BTreeMap::new(),
35 path: crate::runtime_paths::checkpoint_dir(base_dir)
36 .join(format!("{pipeline_name}.json")),
37 loaded: false,
38 }
39 }
40
41 fn ensure_loaded(&mut self) {
42 if self.loaded {
43 return;
44 }
45 self.loaded = true;
46 if let Ok(contents) = std::fs::read_to_string(&self.path) {
47 if let Ok(serde_json::Value::Object(map)) =
48 serde_json::from_str::<serde_json::Value>(&contents)
49 {
50 for (k, v) in map {
51 self.data.insert(k, v);
52 }
53 }
54 }
55 }
56
57 fn save(&self) -> Result<(), String> {
58 let obj: serde_json::Map<String, serde_json::Value> = self
59 .data
60 .iter()
61 .map(|(k, v)| (k.clone(), v.clone()))
62 .collect();
63 let json = serde_json::to_string_pretty(&serde_json::Value::Object(obj))
64 .map_err(|e| format!("checkpoint save error: {e}"))?;
65 crate::atomic_io::atomic_write(&self.path, json.as_bytes())
66 .map_err(|e| format!("checkpoint write error: {e}"))?;
67 Ok(())
68 }
69
70 fn get(&mut self, key: &str) -> VmValue {
71 self.ensure_loaded();
72 match self.data.get(key) {
73 Some(v) => json_to_vm(v),
74 None => VmValue::Nil,
75 }
76 }
77
78 fn set(&mut self, key: String, value: serde_json::Value) -> Result<(), String> {
79 self.ensure_loaded();
80 self.data.insert(key, value);
81 self.save()
82 }
83
84 fn clear(&mut self) -> Result<(), String> {
85 self.data.clear();
86 if self.path.exists() {
87 std::fs::remove_file(&self.path).map_err(|e| format!("checkpoint clear error: {e}"))?;
88 }
89 Ok(())
90 }
91
92 fn list(&mut self) -> Vec<String> {
93 self.ensure_loaded();
94 self.data.keys().cloned().collect()
95 }
96
97 fn exists(&mut self, key: &str) -> bool {
98 self.ensure_loaded();
99 self.data.contains_key(key)
100 }
101
102 fn delete(&mut self, key: &str) -> Result<(), String> {
103 self.ensure_loaded();
104 self.data.remove(key);
105 self.save()
106 }
107}
108
109thread_local! {
110 static CHECKPOINT_STATE: RefCell<Option<CheckpointState>> = const { RefCell::new(None) };
117}
118
119fn with_state<R>(
120 fn_name: &'static str,
121 f: impl FnOnce(&mut CheckpointState) -> Result<R, VmError>,
122) -> Result<R, VmError> {
123 CHECKPOINT_STATE.with(|cell| {
124 let mut guard = cell.borrow_mut();
125 let state = guard.as_mut().ok_or_else(|| {
126 VmError::Runtime(format!(
127 "{fn_name}: checkpoint builtins not registered for this VM"
128 ))
129 })?;
130 f(state)
131 })
132}
133
134use crate::value::vm_to_storage_json as vm_to_json;
135
136fn json_to_vm(jv: &serde_json::Value) -> VmValue {
137 match jv {
138 serde_json::Value::Null => VmValue::Nil,
139 serde_json::Value::Bool(b) => VmValue::Bool(*b),
140 serde_json::Value::Number(n) => {
141 if let Some(i) = n.as_i64() {
142 VmValue::Int(i)
143 } else {
144 VmValue::Float(n.as_f64().unwrap_or(0.0))
145 }
146 }
147 serde_json::Value::String(s) => VmValue::String(arcstr::ArcStr::from(s.as_str())),
148 serde_json::Value::Array(arr) => {
149 VmValue::List(std::sync::Arc::new(arr.iter().map(json_to_vm).collect()))
150 }
151 serde_json::Value::Object(map) => {
152 let mut m = BTreeMap::new();
153 for (k, v) in map {
154 m.insert(k.clone(), json_to_vm(v));
155 }
156 VmValue::dict(m)
157 }
158 }
159}
160
161fn sanitize_pipeline_name(name: &str) -> String {
164 let base = std::path::Path::new(name)
165 .file_name()
166 .and_then(|f| f.to_str())
167 .unwrap_or("default");
168 if base.is_empty() || base == "." || base == ".." {
169 return "default".to_string();
170 }
171 base.to_string()
172}
173
174pub fn register_checkpoint_builtins(vm: &mut Vm, base_dir: &Path, pipeline_name: &str) {
182 let safe_name = sanitize_pipeline_name(pipeline_name);
183 CHECKPOINT_STATE.with(|cell| {
184 *cell.borrow_mut() = Some(CheckpointState::new(base_dir, &safe_name));
185 });
186 for def in MODULE_BUILTINS {
187 vm.register_builtin_def(def);
188 }
189}
190
191pub(crate) const MODULE_BUILTINS: &[&VmBuiltinDef] = &[
192 &CHECKPOINT_IMPL_DEF,
193 &CHECKPOINT_GET_IMPL_DEF,
194 &CHECKPOINT_CLEAR_IMPL_DEF,
195 &CHECKPOINT_LIST_IMPL_DEF,
196 &CHECKPOINT_EXISTS_IMPL_DEF,
197 &CHECKPOINT_DELETE_IMPL_DEF,
198];
199
200#[harn_builtin(
201 sig = "checkpoint(key: string, value: any) -> nil",
202 category = "checkpoint",
203 doc = "Persist a checkpoint key/value pair to durable storage immediately."
204)]
205fn checkpoint_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
206 let key = args.first().map(|a| a.display()).unwrap_or_default();
207 let value = args.get(1).unwrap_or(&VmValue::Nil);
208 let json_val = vm_to_json(value);
209 with_state("checkpoint", |state| {
210 state.set(key, json_val).map_err(VmError::Runtime)
211 })?;
212 Ok(VmValue::Nil)
213}
214
215#[harn_builtin(
216 sig = "checkpoint_get(key: string) -> any",
217 category = "checkpoint",
218 doc = "Read a persisted checkpoint value, or nil if the key is absent."
219)]
220fn checkpoint_get_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
221 let key = args.first().map(|a| a.display()).unwrap_or_default();
222 with_state("checkpoint_get", |state| Ok(state.get(&key)))
223}
224
225#[harn_builtin(
226 sig = "checkpoint_clear() -> nil",
227 category = "checkpoint",
228 doc = "Clear every checkpoint for the active pipeline."
229)]
230fn checkpoint_clear_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
231 with_state("checkpoint_clear", |state| {
232 state.clear().map_err(VmError::Runtime)
233 })?;
234 Ok(VmValue::Nil)
235}
236
237#[harn_builtin(
238 sig = "checkpoint_list() -> list",
239 category = "checkpoint",
240 doc = "Return every checkpoint key for the active pipeline."
241)]
242fn checkpoint_list_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
243 with_state("checkpoint_list", |state| {
244 let keys = state.list();
245 Ok(VmValue::List(std::sync::Arc::new(
246 keys.into_iter()
247 .map(|k| VmValue::String(arcstr::ArcStr::from(k)))
248 .collect(),
249 )))
250 })
251}
252
253#[harn_builtin(
254 sig = "checkpoint_exists(key: string) -> bool",
255 category = "checkpoint",
256 doc = "Return true when the checkpoint key is present (even when its value is nil)."
257)]
258fn checkpoint_exists_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
259 let key = args.first().map(|a| a.display()).unwrap_or_default();
260 with_state("checkpoint_exists", |state| {
261 Ok(VmValue::Bool(state.exists(&key)))
262 })
263}
264
265#[harn_builtin(
266 sig = "checkpoint_delete(key: string) -> nil",
267 category = "checkpoint",
268 doc = "Remove a single key from the checkpoint store."
269)]
270fn checkpoint_delete_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
271 let key = args.first().map(|a| a.display()).unwrap_or_default();
272 with_state("checkpoint_delete", |state| {
273 state.delete(&key).map_err(VmError::Runtime)
274 })?;
275 Ok(VmValue::Nil)
276}