1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use adler_core::{Client, Registry, Site};
8use tokio::sync::RwLock;
9
10use crate::scan::{ScanHandle, ScanId};
11
12#[derive(Clone)]
17pub struct AppState {
18 pub sites: Arc<[Site]>,
22 pub client: Arc<Client>,
24 pub scans: Arc<RwLock<HashMap<ScanId, ScanHandle>>>,
26 pub scan_capacity: usize,
31 pub scans_dir: Option<Arc<PathBuf>>,
34}
35
36impl AppState {
37 #[must_use]
44 pub fn new(sites: Vec<Site>, client: Client, scan_capacity: usize) -> Self {
45 Self {
46 sites: Arc::from(sites.into_boxed_slice()),
47 client: Arc::new(client),
48 scans: Arc::new(RwLock::new(HashMap::new())),
49 scan_capacity: scan_capacity.max(1),
50 scans_dir: None,
51 }
52 }
53
54 #[must_use]
59 pub fn from_registry(registry: &Registry, client: Client, scan_capacity: usize) -> Self {
60 let sites = registry.filter(&[], &[], &[], &[], false);
61 Self::new(sites, client, scan_capacity)
62 }
63
64 #[must_use]
68 pub fn with_scans_dir(mut self, dir: PathBuf) -> Self {
69 self.scans_dir = Some(Arc::new(dir));
70 self
71 }
72
73 pub async fn insert_scan(&self, id: ScanId, handle: ScanHandle) {
77 let mut scans = self.scans.write().await;
78 if scans.len() >= self.scan_capacity {
79 let mut finished_candidate: Option<(ScanId, std::time::Duration)> = None;
80 let mut any_candidate: Option<(ScanId, std::time::Duration)> = None;
81 for (k, v) in scans.iter() {
82 let age = v.elapsed();
83 if v.is_finished_now()
84 && finished_candidate
85 .as_ref()
86 .is_none_or(|(_, prev)| age > *prev)
87 {
88 finished_candidate = Some((k.clone(), age));
89 }
90 if any_candidate.as_ref().is_none_or(|(_, prev)| age > *prev) {
91 any_candidate = Some((k.clone(), age));
92 }
93 }
94 if let Some((victim, _)) = finished_candidate.or(any_candidate) {
95 scans.remove(&victim);
96 }
97 }
98 scans.insert(id, handle);
99 }
100
101 pub async fn get_scan(&self, id: &ScanId) -> Option<ScanHandle> {
103 self.scans.read().await.get(id).cloned()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::scan::{FinishedScan, Summary};
111
112 fn client() -> Client {
113 Client::builder().build().expect("default client")
114 }
115
116 #[tokio::test]
117 async fn evicts_oldest_finished_when_over_capacity() {
118 let state = AppState::new(Vec::new(), client(), 2);
119
120 let id_a = ScanId::from("aaaaaaaaaaaa".to_owned());
121 let handle_a = ScanHandle::new("a", 0, 4);
122 handle_a
123 .publish(FinishedScan {
124 summary: Summary::default(),
125 outcomes: Vec::new(),
126 elapsed_ms: 0,
127 })
128 .await;
129 state.insert_scan(id_a.clone(), handle_a).await;
130
131 let id_b = ScanId::from("bbbbbbbbbbbb".to_owned());
132 state
133 .insert_scan(id_b.clone(), ScanHandle::new("b", 0, 4))
134 .await;
135
136 assert!(state.get_scan(&id_a).await.is_some());
138 assert!(state.get_scan(&id_b).await.is_some());
139
140 let id_c = ScanId::from("cccccccccccc".to_owned());
143 state
144 .insert_scan(id_c.clone(), ScanHandle::new("c", 0, 4))
145 .await;
146
147 assert!(
148 state.get_scan(&id_a).await.is_none(),
149 "finished scan should be evicted first"
150 );
151 assert!(state.get_scan(&id_b).await.is_some());
152 assert!(state.get_scan(&id_c).await.is_some());
153 }
154}