Skip to main content

entrenar/dashboard/wasm/
storage.rs

1//! IndexedDB-backed storage implementation.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use crate::storage::{ExperimentStorage, MetricPoint, Result, RunStatus, StorageError};
8
9/// Internal experiment data structure.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub(crate) struct ExperimentData {
12    pub id: String,
13    pub name: String,
14    pub config: Option<serde_json::Value>,
15    pub created_at: DateTime<Utc>,
16}
17
18/// Internal run data structure.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub(crate) struct RunData {
21    pub id: String,
22    pub experiment_id: String,
23    pub status: RunStatus,
24    pub span_id: Option<String>,
25    pub started_at: Option<DateTime<Utc>>,
26    pub completed_at: Option<DateTime<Utc>>,
27}
28
29/// IndexedDB-backed storage for browser environments.
30///
31/// This is a simplified in-memory implementation that mimics
32/// IndexedDB behavior. A full implementation would use web-sys
33/// to interact with the actual IndexedDB API.
34#[derive(Debug, Default)]
35pub struct IndexedDbStorage {
36    /// Experiments by ID
37    experiments: HashMap<String, ExperimentData>,
38    /// Runs by ID
39    runs: HashMap<String, RunData>,
40    /// Metrics by run_id -> key -> points
41    metrics: HashMap<String, HashMap<String, Vec<MetricPoint>>>,
42    /// Artifacts by hash
43    artifacts: HashMap<String, Vec<u8>>,
44    /// Next experiment ID counter
45    next_exp_id: u64,
46    /// Next run ID counter
47    next_run_id: u64,
48}
49
50impl IndexedDbStorage {
51    /// Create a new IndexedDB storage instance.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Get all experiments.
57    pub fn list_experiments(&self) -> Vec<String> {
58        self.experiments.keys().cloned().collect()
59    }
60
61    /// Get all runs for an experiment.
62    pub fn list_runs(&self, experiment_id: &str) -> Vec<String> {
63        self.runs
64            .values()
65            .filter(|r| r.experiment_id == experiment_id)
66            .map(|r| r.id.clone())
67            .collect()
68    }
69
70    /// Get all metric keys for a run.
71    pub fn list_metric_keys(&self, run_id: &str) -> Vec<String> {
72        self.metrics.get(run_id).map(|m| m.keys().cloned().collect()).unwrap_or_default()
73    }
74}
75
76impl ExperimentStorage for IndexedDbStorage {
77    fn create_experiment(
78        &mut self,
79        name: &str,
80        config: Option<serde_json::Value>,
81    ) -> Result<String> {
82        let id = format!("exp-{}", self.next_exp_id);
83        self.next_exp_id += 1;
84
85        let experiment = ExperimentData {
86            id: id.clone(),
87            name: name.to_string(),
88            config,
89            created_at: Utc::now(),
90        };
91
92        self.experiments.insert(id.clone(), experiment);
93        Ok(id)
94    }
95
96    fn create_run(&mut self, experiment_id: &str) -> Result<String> {
97        if !self.experiments.contains_key(experiment_id) {
98            return Err(StorageError::ExperimentNotFound(experiment_id.to_string()));
99        }
100
101        let id = format!("run-{}", self.next_run_id);
102        self.next_run_id += 1;
103
104        let run = RunData {
105            id: id.clone(),
106            experiment_id: experiment_id.to_string(),
107            status: RunStatus::Pending,
108            span_id: None,
109            started_at: None,
110            completed_at: None,
111        };
112
113        self.runs.insert(id.clone(), run);
114        self.metrics.insert(id.clone(), HashMap::new());
115        Ok(id)
116    }
117
118    fn start_run(&mut self, run_id: &str) -> Result<()> {
119        let run = self
120            .runs
121            .get_mut(run_id)
122            .ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
123
124        if run.status != RunStatus::Pending {
125            return Err(StorageError::InvalidState(format!(
126                "Run {run_id} is not in Pending state"
127            )));
128        }
129
130        run.status = RunStatus::Running;
131        run.started_at = Some(Utc::now());
132        Ok(())
133    }
134
135    fn complete_run(&mut self, run_id: &str, status: RunStatus) -> Result<()> {
136        let run = self
137            .runs
138            .get_mut(run_id)
139            .ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
140
141        if run.status != RunStatus::Running {
142            return Err(StorageError::InvalidState(format!(
143                "Run {run_id} is not in Running state"
144            )));
145        }
146
147        run.status = status;
148        run.completed_at = Some(Utc::now());
149        Ok(())
150    }
151
152    fn log_metric(&mut self, run_id: &str, key: &str, step: u64, value: f64) -> Result<()> {
153        let metrics = self
154            .metrics
155            .get_mut(run_id)
156            .ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
157
158        let points = metrics.entry(key.to_string()).or_default();
159        points.push(MetricPoint::new(step, value));
160        Ok(())
161    }
162
163    fn log_artifact(&mut self, run_id: &str, key: &str, data: &[u8]) -> Result<String> {
164        if !self.runs.contains_key(run_id) {
165            return Err(StorageError::RunNotFound(run_id.to_string()));
166        }
167
168        // Compute SHA-256 hash
169        use sha2::{Digest, Sha256};
170        let mut hasher = Sha256::new();
171        hasher.update(data);
172        let hash = hex::encode(hasher.finalize());
173
174        // Store artifact
175        let artifact_key = format!("{run_id}/{key}/{hash}");
176        self.artifacts.insert(artifact_key, data.to_vec());
177
178        Ok(hash)
179    }
180
181    fn get_metrics(&self, run_id: &str, key: &str) -> Result<Vec<MetricPoint>> {
182        let metrics = self
183            .metrics
184            .get(run_id)
185            .ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
186
187        Ok(metrics.get(key).cloned().unwrap_or_default())
188    }
189
190    fn get_run_status(&self, run_id: &str) -> Result<RunStatus> {
191        let run =
192            self.runs.get(run_id).ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
193
194        Ok(run.status)
195    }
196
197    fn set_span_id(&mut self, run_id: &str, span_id: &str) -> Result<()> {
198        let run = self
199            .runs
200            .get_mut(run_id)
201            .ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
202
203        run.span_id = Some(span_id.to_string());
204        Ok(())
205    }
206
207    fn get_span_id(&self, run_id: &str) -> Result<Option<String>> {
208        let run =
209            self.runs.get(run_id).ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
210
211        Ok(run.span_id.clone())
212    }
213}