1use async_trait::async_trait;
2use dashmap::DashMap;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6use crate::{Context, error::{GraphError, Result}, graph::Graph};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Session {
16 pub id: String,
17 pub graph_id: String,
18 pub current_task_id: String,
19 pub status_message: Option<String>,
21 pub context: crate::context::Context,
22 #[serde(default)]
25 pub version: u64,
26}
27
28impl Session {
29 pub fn new_from_task(sid: String, task_name: &str) -> Self {
34 Self {
35 id: sid,
36 graph_id: "default".to_string(),
37 current_task_id: task_name.to_string(),
38 status_message: None,
39 context: Context::new(),
40 version: 0,
41 }
42 }
43
44 pub fn with_graph_id(mut self, graph_id: impl Into<String>) -> Self {
46 self.graph_id = graph_id.into();
47 self
48 }
49}
50
51#[async_trait]
53pub trait GraphStorage: Send + Sync {
54 async fn save(&self, id: String, graph: Arc<Graph>) -> Result<()>;
55 async fn get(&self, id: &str) -> Result<Option<Arc<Graph>>>;
56 async fn delete(&self, id: &str) -> Result<()>;
57}
58
59#[async_trait]
66pub trait SessionStorage: Send + Sync {
67 async fn save(&self, session: Session) -> Result<()>;
68 async fn get(&self, id: &str) -> Result<Option<Session>>;
69 async fn delete(&self, id: &str) -> Result<()>;
70}
71
72pub struct InMemoryGraphStorage {
74 graphs: DashMap<String, Arc<Graph>>,
75}
76
77impl Default for InMemoryGraphStorage {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl InMemoryGraphStorage {
84 pub fn new() -> Self {
85 Self {
86 graphs: DashMap::new(),
87 }
88 }
89}
90
91#[async_trait]
92impl GraphStorage for InMemoryGraphStorage {
93 async fn save(&self, id: String, graph: Arc<Graph>) -> Result<()> {
94 self.graphs.insert(id, graph);
95 Ok(())
96 }
97
98 async fn get(&self, id: &str) -> Result<Option<Arc<Graph>>> {
99 Ok(self.graphs.get(id).map(|entry| entry.clone()))
100 }
101
102 async fn delete(&self, id: &str) -> Result<()> {
103 self.graphs.remove(id);
104 Ok(())
105 }
106}
107
108pub struct InMemorySessionStorage {
110 sessions: DashMap<String, Session>,
111}
112
113impl Default for InMemorySessionStorage {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl InMemorySessionStorage {
120 pub fn new() -> Self {
121 Self {
122 sessions: DashMap::new(),
123 }
124 }
125}
126
127#[async_trait]
128impl SessionStorage for InMemorySessionStorage {
129 async fn save(&self, mut session: Session) -> Result<()> {
130 match self.sessions.entry(session.id.clone()) {
132 dashmap::Entry::Occupied(mut occupied) => {
133 let stored_version = occupied.get().version;
134 if stored_version != session.version {
135 return Err(GraphError::SessionConflict(format!(
136 "Session '{}' was modified concurrently (stored version {}, \
137 attempted save from version {})",
138 session.id, stored_version, session.version
139 )));
140 }
141 session.version += 1;
142 occupied.insert(session);
143 }
144 dashmap::Entry::Vacant(vacant) => {
145 session.version += 1;
146 vacant.insert(session);
147 }
148 }
149 Ok(())
150 }
151
152 async fn get(&self, id: &str) -> Result<Option<Session>> {
153 Ok(self.sessions.get(id).map(|entry| entry.clone()))
154 }
155
156 async fn delete(&self, id: &str) -> Result<()> {
157 self.sessions.remove(id);
158 Ok(())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[tokio::test]
167 async fn test_session_version_conflict() {
168 let storage = InMemorySessionStorage::new();
169
170 let session = Session::new_from_task("s1".to_string(), "task");
171 storage.save(session).await.unwrap(); let a = storage.get("s1").await.unwrap().unwrap();
175 let b = storage.get("s1").await.unwrap().unwrap();
176 assert_eq!(a.version, 1);
177
178 storage.save(a).await.unwrap(); let err = storage.save(b).await.unwrap_err();
183 assert!(matches!(err, GraphError::SessionConflict(_)), "got: {err:?}");
184 }
185
186 #[tokio::test]
187 async fn test_session_save_reload_cycle() {
188 let storage = InMemorySessionStorage::new();
189
190 let session = Session::new_from_task("s1".to_string(), "task");
191 storage.save(session).await.unwrap();
192
193 for expected_version in 1..4 {
195 let s = storage.get("s1").await.unwrap().unwrap();
196 assert_eq!(s.version, expected_version);
197 storage.save(s).await.unwrap();
198 }
199 }
200}