Skip to main content

voidcrawl_mcp/
sessions.rs

1//! Stateful session registry.
2//!
3//! Each `session_open` spawns a dedicated `BrowserSession` with its own
4//! temporary user-data-dir, so cookies and storage never leak between
5//! subagents. Pooled tabs are only used for stateless `fetch*` calls.
6
7use std::{collections::HashMap, path::PathBuf, sync::Arc};
8
9use tempfile::TempDir;
10use tokio::sync::{Mutex, RwLock};
11use void_crawl_core::{
12    AntibotVerdict, BrowserSession, ChallengeSnapshot, DownloadCapture, ManagedProfileLease, Page,
13    ResolutionOutcome,
14};
15
16pub type SessionId = String;
17
18/// A download armed on a session by `download_arm`, awaiting `download_wait`.
19/// Holds the quarantine `TempDir` alive between the two tool calls.
20#[derive(Debug)]
21pub struct PendingDownload {
22    pub capture:    DownloadCapture,
23    pub quarantine: TempDir,
24    pub output_dir: PathBuf,
25    pub max_bytes:  u64,
26}
27
28/// Last document navigation metadata kept so a later `capture_challenge` call
29/// can combine response-side anti-bot evidence with DOM-side captcha evidence.
30#[derive(Debug, Clone)]
31pub struct LastNavigation {
32    pub url:         String,
33    pub status_code: Option<u16>,
34    pub antibot:     Option<AntibotVerdict>,
35}
36
37/// Challenge event currently owned by a session.
38#[derive(Debug, Clone)]
39pub struct PendingChallenge {
40    pub snapshot: ChallengeSnapshot,
41    pub outcome:  Option<ResolutionOutcome>,
42}
43
44/// Owned state for one stateful MCP session.
45#[derive(Debug)]
46pub struct DedicatedSession {
47    pub session:          Arc<BrowserSession>,
48    pub page:             Mutex<Page>,
49    pub profile_lease:    Option<ManagedProfileLease>,
50    pub last_navigation:  Mutex<Option<LastNavigation>>,
51    pub challenge:        Mutex<Option<PendingChallenge>>,
52    /// A download armed via `download_arm`, pending its `download_wait`.
53    pub pending_download: Mutex<Option<PendingDownload>>,
54}
55
56/// Thread-safe map of live sessions.
57#[derive(Debug, Default)]
58pub struct SessionRegistry {
59    inner: RwLock<HashMap<SessionId, Arc<DedicatedSession>>>,
60}
61
62impl SessionRegistry {
63    pub async fn insert(&self, id: SessionId, session: Arc<DedicatedSession>) {
64        self.inner.write().await.insert(id, session);
65    }
66
67    pub async fn get(&self, id: &str) -> Option<Arc<DedicatedSession>> {
68        self.inner.read().await.get(id).cloned()
69    }
70
71    pub async fn remove(&self, id: &str) -> Option<Arc<DedicatedSession>> {
72        self.inner.write().await.remove(id)
73    }
74
75    pub async fn len(&self) -> usize {
76        self.inner.read().await.len()
77    }
78
79    pub async fn is_empty(&self) -> bool {
80        self.inner.read().await.is_empty()
81    }
82
83    /// Drain every session. Used on shutdown.
84    pub async fn drain(&self) -> Vec<Arc<DedicatedSession>> {
85        self.inner.write().await.drain().map(|(_, v)| v).collect()
86    }
87}