use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::storage::{ExperimentStorage, MetricPoint, Result, RunStatus, StorageError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExperimentData {
pub id: String,
pub name: String,
pub config: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct RunData {
pub id: String,
pub experiment_id: String,
pub status: RunStatus,
pub span_id: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Default)]
pub struct IndexedDbStorage {
experiments: HashMap<String, ExperimentData>,
runs: HashMap<String, RunData>,
metrics: HashMap<String, HashMap<String, Vec<MetricPoint>>>,
artifacts: HashMap<String, Vec<u8>>,
next_exp_id: u64,
next_run_id: u64,
}
impl IndexedDbStorage {
pub fn new() -> Self {
Self::default()
}
pub fn list_experiments(&self) -> Vec<String> {
self.experiments.keys().cloned().collect()
}
pub fn list_runs(&self, experiment_id: &str) -> Vec<String> {
self.runs
.values()
.filter(|r| r.experiment_id == experiment_id)
.map(|r| r.id.clone())
.collect()
}
pub fn list_metric_keys(&self, run_id: &str) -> Vec<String> {
self.metrics.get(run_id).map(|m| m.keys().cloned().collect()).unwrap_or_default()
}
}
impl ExperimentStorage for IndexedDbStorage {
fn create_experiment(
&mut self,
name: &str,
config: Option<serde_json::Value>,
) -> Result<String> {
let id = format!("exp-{}", self.next_exp_id);
self.next_exp_id += 1;
let experiment = ExperimentData {
id: id.clone(),
name: name.to_string(),
config,
created_at: Utc::now(),
};
self.experiments.insert(id.clone(), experiment);
Ok(id)
}
fn create_run(&mut self, experiment_id: &str) -> Result<String> {
if !self.experiments.contains_key(experiment_id) {
return Err(StorageError::ExperimentNotFound(experiment_id.to_string()));
}
let id = format!("run-{}", self.next_run_id);
self.next_run_id += 1;
let run = RunData {
id: id.clone(),
experiment_id: experiment_id.to_string(),
status: RunStatus::Pending,
span_id: None,
started_at: None,
completed_at: None,
};
self.runs.insert(id.clone(), run);
self.metrics.insert(id.clone(), HashMap::new());
Ok(id)
}
fn start_run(&mut self, run_id: &str) -> Result<()> {
let run = self
.runs
.get_mut(run_id)
.ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
if run.status != RunStatus::Pending {
return Err(StorageError::InvalidState(format!(
"Run {run_id} is not in Pending state"
)));
}
run.status = RunStatus::Running;
run.started_at = Some(Utc::now());
Ok(())
}
fn complete_run(&mut self, run_id: &str, status: RunStatus) -> Result<()> {
let run = self
.runs
.get_mut(run_id)
.ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
if run.status != RunStatus::Running {
return Err(StorageError::InvalidState(format!(
"Run {run_id} is not in Running state"
)));
}
run.status = status;
run.completed_at = Some(Utc::now());
Ok(())
}
fn log_metric(&mut self, run_id: &str, key: &str, step: u64, value: f64) -> Result<()> {
let metrics = self
.metrics
.get_mut(run_id)
.ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
let points = metrics.entry(key.to_string()).or_default();
points.push(MetricPoint::new(step, value));
Ok(())
}
fn log_artifact(&mut self, run_id: &str, key: &str, data: &[u8]) -> Result<String> {
if !self.runs.contains_key(run_id) {
return Err(StorageError::RunNotFound(run_id.to_string()));
}
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
let hash = hex::encode(hasher.finalize());
let artifact_key = format!("{run_id}/{key}/{hash}");
self.artifacts.insert(artifact_key, data.to_vec());
Ok(hash)
}
fn get_metrics(&self, run_id: &str, key: &str) -> Result<Vec<MetricPoint>> {
let metrics = self
.metrics
.get(run_id)
.ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
Ok(metrics.get(key).cloned().unwrap_or_default())
}
fn get_run_status(&self, run_id: &str) -> Result<RunStatus> {
let run =
self.runs.get(run_id).ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
Ok(run.status)
}
fn set_span_id(&mut self, run_id: &str, span_id: &str) -> Result<()> {
let run = self
.runs
.get_mut(run_id)
.ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
run.span_id = Some(span_id.to_string());
Ok(())
}
fn get_span_id(&self, run_id: &str) -> Result<Option<String>> {
let run =
self.runs.get(run_id).ok_or_else(|| StorageError::RunNotFound(run_id.to_string()))?;
Ok(run.span_id.clone())
}
}