use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::types::DbError;
use crate::Graph;
pub type GraphState = (Graph, u64);
pub struct GraphRegistry {
graphs_dir: PathBuf,
open: Mutex<HashMap<String, Arc<Mutex<GraphState>>>>,
}
impl GraphRegistry {
pub fn new(graphs_dir: PathBuf) -> Arc<Self> {
Arc::new(Self {
graphs_dir,
open: Mutex::new(HashMap::new()),
})
}
pub async fn get_or_open(
&self,
name: &str,
) -> Result<Arc<Mutex<GraphState>>, DbError> {
let mut map = self.open.lock().await;
if let Some(g) = map.get(name) {
return Ok(Arc::clone(g));
}
if !name.starts_with('_') {
validate_graph_name(name)?;
}
let path = self.graphs_dir.join(name);
std::fs::create_dir_all(&path).map_err(DbError::Storage)?;
let graph = Graph::open(&path).map_err(|e| {
let msg = e.to_string();
if msg.contains("LOCK") || msg.contains("lock file") {
crate::types::DbError::Query(format!(
"graph '{}' is locked by another process — stop the REPL or any \
other minigdb instance that has this graph open before using the server",
name
))
} else {
e
}
})?;
let state = Arc::new(Mutex::new((graph, 0u64)));
map.insert(name.to_string(), Arc::clone(&state));
Ok(state)
}
pub async fn create(&self, name: &str) -> Result<(), DbError> {
self.get_or_open(name).await?;
Ok(())
}
pub async fn drop_graph(&self, name: &str) -> Result<(), DbError> {
let mut map = self.open.lock().await;
map.remove(name);
let path = self.graphs_dir.join(name);
if path.exists() {
std::fs::remove_dir_all(&path).map_err(DbError::Storage)?;
}
Ok(())
}
pub async fn list(&self) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(&self.graphs_dir) else {
return Vec::new();
};
let mut names: Vec<String> = entries
.filter_map(|e| {
let e = e.ok()?;
if e.file_type().ok()?.is_dir() {
let name = e.file_name().into_string().ok()?;
if name.starts_with('_') { None } else { Some(name) }
} else {
None
}
})
.collect();
names.sort();
names
}
pub async fn checkpoint_all(&self) {
let map = self.open.lock().await;
for (name, state) in map.iter() {
let guard = state.lock().await;
let (graph, _) = &*guard;
if let Err(e) = graph.store.flush() {
eprintln!("Warning: checkpoint of graph '{name}' failed: {e}");
}
}
}
}
pub const META_GRAPH: &str = "_meta";
fn validate_graph_name(name: &str) -> Result<(), DbError> {
if name.is_empty()
|| name.len() > 64
|| name.starts_with('_')
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
Err(DbError::Query(format!("invalid graph name '{name}'")))
} else {
Ok(())
}
}