Skip to main content

domi_server/http/
state.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use tokio::sync::broadcast;
5use ulid::Ulid;
6
7use crate::events::EventWriter;
8
9pub struct AppState {
10    pub root: PathBuf,
11    pub state_dir: PathBuf,
12    pub writer: Arc<EventWriter>,
13    pub broadcaster: broadcast::Sender<crate::events::Event>,
14    pub server_id: Ulid,
15    pub library_root: Option<PathBuf>,
16}
17
18impl AppState {
19    pub fn new(
20        root: PathBuf,
21        state_dir: PathBuf,
22        writer: Arc<EventWriter>,
23        capacity: usize,
24        library_root: Option<PathBuf>,
25    ) -> Self {
26        let resolved_library_root = library_root.map(|p| match std::fs::canonicalize(&p) {
27            Ok(canon) => canon,
28            Err(e) => {
29                tracing::warn!(path = %p.display(), error = %e,
30                    "library_root could not be canonicalized; library routes will 404");
31                p
32            }
33        });
34        let (broadcaster, _) = broadcast::channel(capacity);
35        Self {
36            root,
37            state_dir,
38            writer,
39            broadcaster,
40            server_id: Ulid::new(),
41            library_root: resolved_library_root,
42        }
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::events::Event;
50    use tempfile::tempdir;
51
52    fn sample_event() -> Event {
53        use crate::events::{EventData, Kind, Rect, Source, Target};
54        Event {
55            v: 2,
56            id: Ulid::from_string("01H8XZQ5K2J9Z9Q4X5Y6Z7XYZ0").unwrap(),
57            ts: chrono::DateTime::parse_from_rfc3339("2026-07-05T18:21:00Z")
58                .unwrap()
59                .with_timezone(&chrono::Utc),
60            src: Source::DomiJs,
61            doc: "x".into(),
62            kind: Kind::Click,
63            target: Target {
64                id: None,
65                selector: None,
66                rect: Rect {
67                    x: 0.0,
68                    y: 0.0,
69                    w: 1.0,
70                    h: 1.0,
71                },
72            },
73            data: EventData::Click {
74                value: Some("hi".into()),
75            },
76        }
77    }
78
79    #[test]
80    fn new_assigns_unique_server_id() {
81        let dir = tempdir().unwrap();
82        let path = dir.path().join("events.jsonl");
83        let w = Arc::new(EventWriter::new(&path));
84        let s1 = AppState::new(
85            dir.path().to_path_buf(),
86            dir.path().to_path_buf(),
87            w.clone(),
88            16,
89            None,
90        );
91        let s2 = AppState::new(dir.path().to_path_buf(), dir.path().to_path_buf(), w, 16, None);
92        assert_ne!(s1.server_id, s2.server_id);
93    }
94
95    #[test]
96    fn broadcaster_receives_sent_event() {
97        let dir = tempdir().unwrap();
98        let path = dir.path().join("events.jsonl");
99        let w = Arc::new(EventWriter::new(&path));
100        let state = AppState::new(dir.path().to_path_buf(), dir.path().to_path_buf(), w, 16, None);
101        let mut rx = state.broadcaster.subscribe();
102        let ev = sample_event();
103        let _ = state.broadcaster.send(ev.clone());
104        let received = rx.try_recv().expect("event delivered");
105        assert_eq!(received.id, ev.id);
106    }
107
108    #[test]
109    fn broadcaster_capacity_is_respected() {
110        let dir = tempdir().unwrap();
111        let path = dir.path().join("events.jsonl");
112        let w = Arc::new(EventWriter::new(&path));
113        let state = AppState::new(dir.path().to_path_buf(), dir.path().to_path_buf(), w, 4, None);
114        assert_eq!(state.broadcaster.receiver_count(), 0, "no subscribers yet");
115    }
116}