use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use kindling_service::KindlingService;
use kindling_store::{project_db_path, project_id};
use crate::error::ApiError;
type Registry = Arc<Mutex<HashMap<String, Arc<Mutex<KindlingService>>>>>;
#[derive(Clone)]
pub struct AppState {
kindling_home: PathBuf,
services: Registry,
activity: Arc<Activity>,
}
impl AppState {
pub fn new(kindling_home: PathBuf) -> Self {
Self {
kindling_home,
services: Arc::new(Mutex::new(HashMap::new())),
activity: Arc::new(Activity::new()),
}
}
pub fn kindling_home(&self) -> &PathBuf {
&self.kindling_home
}
pub fn activity(&self) -> &Arc<Activity> {
&self.activity
}
pub fn known_project_ids(&self) -> Vec<String> {
let map = self.services.lock().expect("registry mutex poisoned");
let mut ids: Vec<String> = map.keys().cloned().collect();
ids.sort();
ids
}
pub fn service_for(&self, project_root: &str) -> Result<Arc<Mutex<KindlingService>>, ApiError> {
let key = project_id(project_root);
{
let map = self.services.lock().expect("registry mutex poisoned");
if let Some(svc) = map.get(&key) {
return Ok(Arc::clone(svc));
}
}
let mut map = self.services.lock().expect("registry mutex poisoned");
if let Some(svc) = map.get(&key) {
return Ok(Arc::clone(svc));
}
let db_path = project_db_path(&self.kindling_home, project_root);
let service = KindlingService::open(&db_path)
.map_err(|e| ApiError::Internal(format!("opening project db: {e}")))?;
let entry = Arc::new(Mutex::new(service));
map.insert(key, Arc::clone(&entry));
Ok(entry)
}
}
#[derive(Debug)]
pub struct Activity {
in_flight: AtomicU64,
last_active_ms: AtomicU64,
start: Instant,
}
impl Activity {
fn new() -> Self {
Self {
in_flight: AtomicU64::new(0),
last_active_ms: AtomicU64::new(0),
start: Instant::now(),
}
}
fn now_ms(&self) -> u64 {
self.start.elapsed().as_millis() as u64
}
pub fn enter(&self) {
self.in_flight.fetch_add(1, Ordering::SeqCst);
self.last_active_ms.store(self.now_ms(), Ordering::SeqCst);
}
pub fn leave(&self) {
self.in_flight.fetch_sub(1, Ordering::SeqCst);
self.last_active_ms.store(self.now_ms(), Ordering::SeqCst);
}
pub fn is_idle_for(&self, timeout: Duration) -> bool {
if self.in_flight.load(Ordering::SeqCst) > 0 {
return false;
}
let idle_ms = self
.now_ms()
.saturating_sub(self.last_active_ms.load(Ordering::SeqCst));
idle_ms >= timeout.as_millis() as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idle_after_timeout_when_no_activity() {
let a = Activity::new();
assert!(a.is_idle_for(Duration::from_millis(0)));
}
#[test]
fn not_idle_while_in_flight() {
let a = Activity::new();
a.enter();
assert!(!a.is_idle_for(Duration::from_millis(0)));
a.leave();
assert!(a.is_idle_for(Duration::from_millis(0)));
}
}