Skip to main content

sova_devtools/
hub.rs

1//! Process-wide ring buffer + SSE fan-out.
2
3use crate::collector::{LogLine, RequestMeta, RequestSnapshot};
4use serde_json::json;
5use sova_sse::{SseChannel, SseEvent};
6use std::collections::{HashMap, VecDeque};
7use std::sync::{
8    atomic::{AtomicU64, Ordering},
9    Arc, Mutex,
10};
11
12static SEQ: AtomicU64 = AtomicU64::new(1);
13
14pub fn next_id() -> String {
15    format!("dt-{}", SEQ.fetch_add(1, Ordering::Relaxed))
16}
17
18struct HubInner {
19    requests: VecDeque<RequestSnapshot>,
20    by_id: HashMap<String, RequestSnapshot>,
21    logs: VecDeque<LogLine>,
22    plugins: Vec<String>,
23    profile: String,
24    event_seq: u64,
25}
26
27/// Shared DevTools state installed on the app.
28#[derive(Clone)]
29pub struct DevToolsHub {
30    inner: Arc<Mutex<HubInner>>,
31    pub channel: SseChannel,
32    request_cap: usize,
33    log_cap: usize,
34}
35
36impl DevToolsHub {
37    pub fn new(request_cap: usize, log_cap: usize) -> Self {
38        let channel = SseChannel::new(256).history_cap(100);
39        Self {
40            inner: Arc::new(Mutex::new(HubInner {
41                requests: VecDeque::new(),
42                by_id: HashMap::new(),
43                logs: VecDeque::new(),
44                plugins: Vec::new(),
45                profile: String::new(),
46                event_seq: 0,
47            })),
48            channel,
49            request_cap: request_cap.max(10),
50            log_cap: log_cap.max(50),
51        }
52    }
53
54    pub fn set_config_info(&self, plugins: Vec<String>, profile: String) {
55        let mut g = self.inner.lock().unwrap();
56        g.plugins = plugins;
57        g.profile = profile;
58    }
59
60    pub fn push_snapshot(&self, snap: RequestSnapshot) {
61        let meta = RequestMeta::from(&snap);
62        let mut g = self.inner.lock().unwrap();
63        g.event_seq += 1;
64        let eid = g.event_seq.to_string();
65        g.by_id.insert(snap.id.clone(), snap.clone());
66        g.requests.push_back(snap);
67        while g.requests.len() > self.request_cap {
68            if let Some(old) = g.requests.pop_front() {
69                g.by_id.remove(&old.id);
70            }
71        }
72        drop(g);
73        let data = serde_json::to_string(&json!({
74            "type": "request.finished",
75            "meta": meta,
76        }))
77        .unwrap_or_else(|_| "{}".into());
78        self.channel.publish(
79            SseEvent::data(data)
80                .id(eid)
81                .event("request.finished"),
82        );
83    }
84
85    pub fn push_log(&self, line: LogLine) {
86        let mut g = self.inner.lock().unwrap();
87        g.event_seq += 1;
88        let eid = g.event_seq.to_string();
89        // Attach to open bag via request_id if middleware left one — also keep site feed.
90        g.logs.push_back(line.clone());
91        while g.logs.len() > self.log_cap {
92            g.logs.pop_front();
93        }
94        drop(g);
95        let data = serde_json::to_string(&json!({
96            "type": "log.line",
97            "line": line,
98        }))
99        .unwrap_or_else(|_| "{}".into());
100        self.channel
101            .publish(SseEvent::data(data).id(eid).event("log.line"));
102    }
103
104    pub fn get(&self, id: &str) -> Option<RequestSnapshot> {
105        self.inner.lock().unwrap().by_id.get(id).cloned()
106    }
107
108    pub fn list_meta(&self, limit: usize) -> Vec<RequestMeta> {
109        let g = self.inner.lock().unwrap();
110        g.requests
111            .iter()
112            .rev()
113            .take(limit)
114            .map(RequestMeta::from)
115            .collect()
116    }
117
118    pub fn recent_logs(&self, limit: usize) -> Vec<LogLine> {
119        let g = self.inner.lock().unwrap();
120        g.logs.iter().rev().take(limit).cloned().collect()
121    }
122
123    pub fn config_json(&self) -> serde_json::Value {
124        let g = self.inner.lock().unwrap();
125        json!({
126            "profile": g.profile,
127            "plugins": g.plugins,
128            "features": compile_features(),
129        })
130    }
131}
132
133fn compile_features() -> Vec<&'static str> {
134    #[allow(unused_mut)]
135    let mut v = Vec::new();
136    #[cfg(feature = "session")]
137    v.push("session");
138    #[cfg(feature = "mail")]
139    v.push("mail");
140    #[cfg(feature = "http")]
141    v.push("http");
142    #[cfg(feature = "db")]
143    v.push("db");
144    #[cfg(feature = "tasks")]
145    v.push("tasks");
146    #[cfg(feature = "auth")]
147    v.push("auth");
148    #[cfg(feature = "i18n")]
149    v.push("i18n");
150    #[cfg(feature = "csrf")]
151    v.push("csrf");
152    #[cfg(feature = "passport")]
153    v.push("passport");
154    #[cfg(feature = "store")]
155    v.push("store");
156    #[cfg(feature = "redis")]
157    v.push("redis");
158    #[cfg(feature = "rate-limit")]
159    v.push("rate-limit");
160    v
161}