use parking_lot::RwLock; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{broadcast, mpsc}; use solverforge::{ HardSoftScore, SolverEvent, SolverManager, SolverManagerError, SolverSnapshot, SolverSnapshotAnalysis, SolverStatus, }; use super::event_payload::{ bootstrap_event_type, bootstrap_snapshot_event_type, event_payload, snapshot_status_event_payload, status_event_payload, }; use crate::domain::FieldServicePlan; // Static manager — must be 'static for retained job execution. static MANAGER: SolverManager = SolverManager::new(); struct JobState { sse_tx: broadcast::Sender, } /// Manages retained solving jobs and broadcasts lifecycle-complete SSE payloads. pub struct SolverService { jobs: Arc>>, } impl SolverService { pub fn new() -> Self { Self { jobs: Arc::new(RwLock::new(HashMap::new())), } } pub fn start_job(&self, plan: FieldServicePlan) -> Result { let (job_id, receiver) = MANAGER.solve(plan)?; let (sse_tx, _) = broadcast::channel(64); self.jobs.write().insert( job_id, JobState { sse_tx: sse_tx.clone(), }, ); let jobs = Arc::clone(&self.jobs); tokio::spawn(async move { drain_receiver(jobs, job_id, sse_tx, receiver).await; }); Ok(job_id.to_string()) } pub fn subscribe(&self, id: &str) -> Option> { let job_id = parse_job_id(id).ok()?; self.jobs .read() .get(&job_id) .map(|state| state.sse_tx.subscribe()) } pub fn bootstrap_event(&self, id: &str) -> Result { let job_id = parse_job_id(id)?; let status = MANAGER.get_status(job_id)?; if let Some(revision) = status.latest_snapshot_revision { let snapshot = MANAGER.get_snapshot(job_id, Some(revision))?; return Ok(snapshot_status_event_payload( job_id, bootstrap_snapshot_event_type(status.lifecycle_state), &status, &snapshot, )); } Ok(status_event_payload( job_id, bootstrap_event_type(status.lifecycle_state), &status, )) } pub fn get_status(&self, id: &str) -> Result, SolverManagerError> { let job_id = parse_job_id(id)?; MANAGER.get_status(job_id) } pub fn pause(&self, id: &str) -> Result<(), SolverManagerError> { MANAGER.pause(parse_job_id(id)?) } pub fn resume(&self, id: &str) -> Result<(), SolverManagerError> { MANAGER.resume(parse_job_id(id)?) } pub fn cancel(&self, id: &str) -> Result<(), SolverManagerError> { MANAGER.cancel(parse_job_id(id)?) } pub fn delete(&self, id: &str) -> Result<(), SolverManagerError> { let job_id = parse_job_id(id)?; MANAGER.delete(job_id)?; self.jobs.write().remove(&job_id); Ok(()) } pub fn get_snapshot( &self, id: &str, snapshot_revision: Option, ) -> Result, SolverManagerError> { MANAGER.get_snapshot(parse_job_id(id)?, snapshot_revision) } pub fn analyze_snapshot( &self, id: &str, snapshot_revision: Option, ) -> Result, SolverManagerError> { MANAGER.analyze_snapshot(parse_job_id(id)?, snapshot_revision) } } async fn drain_receiver( jobs: Arc>>, job_id: usize, sse_tx: broadcast::Sender, mut receiver: mpsc::UnboundedReceiver>, ) { while let Some(event) = receiver.recv().await { let payload = match &event { SolverEvent::Progress { metadata } => { event_payload(job_id, "progress", metadata, None, None) } SolverEvent::BestSolution { metadata, solution } => { event_payload(job_id, "best_solution", metadata, Some(solution), None) } SolverEvent::PauseRequested { metadata } => { event_payload(job_id, "pause_requested", metadata, None, None) } SolverEvent::Paused { metadata } => { event_payload(job_id, "paused", metadata, None, None) } SolverEvent::Resumed { metadata } => { event_payload(job_id, "resumed", metadata, None, None) } SolverEvent::Completed { metadata, solution } => { event_payload(job_id, "completed", metadata, Some(solution), None) } SolverEvent::Cancelled { metadata } => { event_payload(job_id, "cancelled", metadata, None, None) } SolverEvent::Failed { metadata, error } => { event_payload(job_id, "failed", metadata, None, Some(error.as_str())) } }; if !jobs.read().contains_key(&job_id) { return; } let _ = sse_tx.send(payload); } } fn parse_job_id(id: &str) -> Result { id.parse::() .map_err(|_| SolverManagerError::JobNotFound { job_id: usize::MAX }) } impl Default for SolverService { fn default() -> Self { Self::new() } }