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}