Skip to main content

hen/request/
session.rs

1use std::{
2    collections::HashMap,
3    sync::{Arc, Mutex},
4    time::Instant,
5};
6
7use reqwest::cookie::Jar;
8use tokio::{
9    sync::{mpsc, oneshot, Mutex as AsyncMutex},
10    task::AbortHandle,
11};
12
13use super::RequestProtocol;
14
15#[derive(Debug, Clone, Default)]
16pub struct SessionRegistry {
17    inner: Arc<Mutex<HashMap<String, SessionHandle>>>,
18    oauth: Arc<Mutex<HashMap<String, Arc<AsyncMutex<OAuthSessionRuntime>>>>>,
19}
20
21#[derive(Debug, Clone)]
22pub struct SessionHandle {
23    pub name: String,
24    pub protocol: RequestProtocol,
25    pub metadata: HashMap<String, String>,
26    runtime: Option<SessionRuntime>,
27}
28
29#[derive(Debug, Clone)]
30enum SessionRuntime {
31    Http(Arc<HttpSessionRuntime>),
32    Sse(Arc<SseSessionRuntime>),
33    Ws(Arc<WsSessionRuntime>),
34}
35
36#[derive(Debug, Default)]
37pub(crate) struct HttpSessionRuntime {
38    cookie_jar: Arc<Jar>,
39}
40
41#[derive(Debug, Default)]
42pub(crate) struct OAuthSessionRuntime {
43    pub cache_key: Option<String>,
44    pub token_endpoint: Option<String>,
45    pub token_fields: HashMap<String, String>,
46    pub refresh_token: Option<String>,
47    pub expires_at: Option<Instant>,
48}
49
50#[derive(Debug)]
51pub(crate) struct SseSessionRuntime {
52    receiver: AsyncMutex<mpsc::UnboundedReceiver<SseMessage>>,
53    abort_handle: AbortHandle,
54}
55
56#[derive(Debug)]
57pub(crate) struct WsSessionRuntime {
58    receiver: AsyncMutex<mpsc::UnboundedReceiver<WsMessage>>,
59    sender: mpsc::UnboundedSender<WsCommand>,
60    reader_abort_handle: AbortHandle,
61    writer_abort_handle: AbortHandle,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) struct SseMessage {
66    pub event: Option<String>,
67    pub id: Option<String>,
68    pub data: String,
69    pub raw_event: String,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub(crate) enum WsMessageKind {
74    Text,
75    Binary,
76    Close,
77}
78
79impl WsMessageKind {
80    pub(crate) fn as_str(self) -> &'static str {
81        match self {
82            Self::Text => "text",
83            Self::Binary => "binary",
84            Self::Close => "close",
85        }
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub(crate) struct WsMessage {
91    pub kind: WsMessageKind,
92    pub data: String,
93    pub raw_message: String,
94}
95
96#[derive(Debug)]
97pub(crate) struct WsCommand {
98    pub payload: String,
99    pub completion: oneshot::Sender<Result<(), String>>,
100}
101
102impl SseSessionRuntime {
103    pub(crate) fn new(
104        receiver: mpsc::UnboundedReceiver<SseMessage>,
105        abort_handle: AbortHandle,
106    ) -> Self {
107        Self {
108            receiver: AsyncMutex::new(receiver),
109            abort_handle,
110        }
111    }
112
113    pub(crate) async fn recv(&self) -> Option<SseMessage> {
114        self.receiver.lock().await.recv().await
115    }
116
117    fn abort(&self) {
118        self.abort_handle.abort();
119    }
120}
121
122impl HttpSessionRuntime {
123    pub(crate) fn cookie_jar(&self) -> Arc<Jar> {
124        self.cookie_jar.clone()
125    }
126}
127
128impl WsSessionRuntime {
129    pub(crate) fn new(
130        receiver: mpsc::UnboundedReceiver<WsMessage>,
131        sender: mpsc::UnboundedSender<WsCommand>,
132        reader_abort_handle: AbortHandle,
133        writer_abort_handle: AbortHandle,
134    ) -> Self {
135        Self {
136            receiver: AsyncMutex::new(receiver),
137            sender,
138            reader_abort_handle,
139            writer_abort_handle,
140        }
141    }
142
143    pub(crate) async fn recv(&self) -> Option<WsMessage> {
144        self.receiver.lock().await.recv().await
145    }
146
147    pub(crate) async fn send(&self, payload: String) -> Result<(), String> {
148        let (completion_tx, completion_rx) = oneshot::channel();
149        self.sender
150            .send(WsCommand {
151                payload,
152                completion: completion_tx,
153            })
154            .map_err(|_| "WebSocket session writer is closed".to_string())?;
155
156        completion_rx
157            .await
158            .map_err(|_| "WebSocket session writer dropped before acknowledging the send".to_string())?
159    }
160
161    fn abort(&self) {
162        self.reader_abort_handle.abort();
163        self.writer_abort_handle.abort();
164    }
165}
166
167impl SessionHandle {
168    pub(crate) fn http_runtime(&self) -> Option<Arc<HttpSessionRuntime>> {
169        match &self.runtime {
170            Some(SessionRuntime::Http(runtime)) => Some(runtime.clone()),
171            Some(SessionRuntime::Sse(_)) | Some(SessionRuntime::Ws(_)) | None => None,
172        }
173    }
174
175    pub(crate) fn sse_runtime(&self) -> Option<Arc<SseSessionRuntime>> {
176        match &self.runtime {
177            Some(SessionRuntime::Http(_)) => None,
178            Some(SessionRuntime::Sse(runtime)) => Some(runtime.clone()),
179            Some(SessionRuntime::Ws(_)) | None => None,
180        }
181    }
182
183    pub(crate) fn ws_runtime(&self) -> Option<Arc<WsSessionRuntime>> {
184        match &self.runtime {
185            Some(SessionRuntime::Ws(runtime)) => Some(runtime.clone()),
186            None => None,
187            Some(SessionRuntime::Http(_)) | Some(SessionRuntime::Sse(_)) => None,
188        }
189    }
190}
191
192impl SessionRegistry {
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    pub fn upsert(
198        &self,
199        name: impl Into<String>,
200        protocol: RequestProtocol,
201        metadata: HashMap<String, String>,
202    ) -> SessionHandle {
203        self.insert_handle(SessionHandle {
204            name: name.into(),
205            protocol,
206            metadata,
207            runtime: None,
208        })
209    }
210
211    pub(crate) fn get_or_create_http(
212        &self,
213        name: impl Into<String>,
214        metadata: HashMap<String, String>,
215    ) -> SessionHandle {
216        let name = name.into();
217        let mut sessions = self
218            .inner
219            .lock()
220            .expect("session registry mutex poisoned");
221
222        if let Some(existing) = sessions.get(&name) {
223            return existing.clone();
224        }
225
226        let handle = SessionHandle {
227            name: name.clone(),
228            protocol: RequestProtocol::Http,
229            metadata,
230            runtime: Some(SessionRuntime::Http(Arc::new(HttpSessionRuntime::default()))),
231        };
232        sessions.insert(name, handle.clone());
233        handle
234    }
235
236    pub(crate) fn upsert_sse(
237        &self,
238        name: impl Into<String>,
239        metadata: HashMap<String, String>,
240        receiver: mpsc::UnboundedReceiver<SseMessage>,
241        abort_handle: AbortHandle,
242    ) -> SessionHandle {
243        self.insert_handle(SessionHandle {
244            name: name.into(),
245            protocol: RequestProtocol::Sse,
246            metadata,
247            runtime: Some(SessionRuntime::Sse(Arc::new(SseSessionRuntime::new(
248                receiver,
249                abort_handle,
250            )))),
251        })
252    }
253
254    pub(crate) fn upsert_ws(
255        &self,
256        name: impl Into<String>,
257        metadata: HashMap<String, String>,
258        receiver: mpsc::UnboundedReceiver<WsMessage>,
259        sender: mpsc::UnboundedSender<WsCommand>,
260        reader_abort_handle: AbortHandle,
261        writer_abort_handle: AbortHandle,
262    ) -> SessionHandle {
263        self.insert_handle(SessionHandle {
264            name: name.into(),
265            protocol: RequestProtocol::Ws,
266            metadata,
267            runtime: Some(SessionRuntime::Ws(Arc::new(WsSessionRuntime::new(
268                receiver,
269                sender,
270                reader_abort_handle,
271                writer_abort_handle,
272            )))),
273        })
274    }
275
276    pub(crate) fn get_or_create_oauth(
277        &self,
278        name: impl Into<String>,
279    ) -> Arc<AsyncMutex<OAuthSessionRuntime>> {
280        let name = name.into();
281        let mut runtimes = self
282            .oauth
283            .lock()
284            .expect("oauth registry mutex poisoned");
285
286        runtimes
287            .entry(name)
288            .or_insert_with(|| Arc::new(AsyncMutex::new(OAuthSessionRuntime::default())))
289            .clone()
290    }
291
292    fn insert_handle(&self, handle: SessionHandle) -> SessionHandle {
293        let mut sessions = self
294            .inner
295            .lock()
296            .expect("session registry mutex poisoned");
297
298        if let Some(previous) = sessions.insert(handle.name.clone(), handle.clone()) {
299            abort_runtime(previous.runtime.as_ref());
300        }
301
302        handle
303    }
304
305    pub fn get(&self, name: &str) -> Option<SessionHandle> {
306        self.inner
307            .lock()
308            .expect("session registry mutex poisoned")
309            .get(name)
310            .cloned()
311    }
312
313    pub fn remove(&self, name: &str) -> Option<SessionHandle> {
314        let removed = self
315            .inner
316            .lock()
317            .expect("session registry mutex poisoned")
318            .remove(name);
319
320        if let Some(handle) = removed.as_ref() {
321            abort_runtime(handle.runtime.as_ref());
322        }
323
324        removed
325    }
326
327    pub fn clear(&self) {
328        let mut sessions = self
329            .inner
330            .lock()
331            .expect("session registry mutex poisoned");
332        let mut oauth = self
333            .oauth
334            .lock()
335            .expect("oauth registry mutex poisoned");
336
337        for handle in sessions.values() {
338            abort_runtime(handle.runtime.as_ref());
339        }
340
341        sessions.clear();
342        oauth.clear();
343    }
344}
345
346fn abort_runtime(runtime: Option<&SessionRuntime>) {
347    match runtime {
348        Some(SessionRuntime::Http(_)) => {}
349        Some(SessionRuntime::Sse(runtime)) => runtime.abort(),
350        Some(SessionRuntime::Ws(runtime)) => runtime.abort(),
351        None => {}
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn registry_round_trips_handles() {
361        let registry = SessionRegistry::new();
362        let handle = registry.upsert("alpha", RequestProtocol::Http, HashMap::new());
363
364        let loaded = registry.get("alpha").expect("session should exist");
365        assert_eq!(loaded.name, handle.name);
366        assert_eq!(loaded.protocol, RequestProtocol::Http);
367
368        let removed = registry.remove("alpha").expect("session should be removed");
369        assert_eq!(removed.name, "alpha");
370        assert!(registry.get("alpha").is_none());
371    }
372}