use async_trait::async_trait;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::{Context, error::{GraphError, Result}, graph::Graph};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub graph_id: String,
pub current_task_id: String,
pub status_message: Option<String>,
pub context: crate::context::Context,
#[serde(default)]
pub version: u64,
}
impl Session {
pub fn new_from_task(sid: String, task_name: &str) -> Self {
Self {
id: sid,
graph_id: "default".to_string(),
current_task_id: task_name.to_string(),
status_message: None,
context: Context::new(),
version: 0,
}
}
pub fn with_graph_id(mut self, graph_id: impl Into<String>) -> Self {
self.graph_id = graph_id.into();
self
}
}
#[async_trait]
pub trait GraphStorage: Send + Sync {
async fn save(&self, id: String, graph: Arc<Graph>) -> Result<()>;
async fn get(&self, id: &str) -> Result<Option<Arc<Graph>>>;
async fn delete(&self, id: &str) -> Result<()>;
}
#[async_trait]
pub trait SessionStorage: Send + Sync {
async fn save(&self, session: Session) -> Result<()>;
async fn get(&self, id: &str) -> Result<Option<Session>>;
async fn delete(&self, id: &str) -> Result<()>;
}
pub struct InMemoryGraphStorage {
graphs: DashMap<String, Arc<Graph>>,
}
impl Default for InMemoryGraphStorage {
fn default() -> Self {
Self::new()
}
}
impl InMemoryGraphStorage {
pub fn new() -> Self {
Self {
graphs: DashMap::new(),
}
}
}
#[async_trait]
impl GraphStorage for InMemoryGraphStorage {
async fn save(&self, id: String, graph: Arc<Graph>) -> Result<()> {
self.graphs.insert(id, graph);
Ok(())
}
async fn get(&self, id: &str) -> Result<Option<Arc<Graph>>> {
Ok(self.graphs.get(id).map(|entry| entry.clone()))
}
async fn delete(&self, id: &str) -> Result<()> {
self.graphs.remove(id);
Ok(())
}
}
pub struct InMemorySessionStorage {
sessions: DashMap<String, Session>,
}
impl Default for InMemorySessionStorage {
fn default() -> Self {
Self::new()
}
}
impl InMemorySessionStorage {
pub fn new() -> Self {
Self {
sessions: DashMap::new(),
}
}
}
#[async_trait]
impl SessionStorage for InMemorySessionStorage {
async fn save(&self, mut session: Session) -> Result<()> {
match self.sessions.entry(session.id.clone()) {
dashmap::Entry::Occupied(mut occupied) => {
let stored_version = occupied.get().version;
if stored_version != session.version {
return Err(GraphError::SessionConflict(format!(
"Session '{}' was modified concurrently (stored version {}, \
attempted save from version {})",
session.id, stored_version, session.version
)));
}
session.version += 1;
occupied.insert(session);
}
dashmap::Entry::Vacant(vacant) => {
session.version += 1;
vacant.insert(session);
}
}
Ok(())
}
async fn get(&self, id: &str) -> Result<Option<Session>> {
Ok(self.sessions.get(id).map(|entry| entry.clone()))
}
async fn delete(&self, id: &str) -> Result<()> {
self.sessions.remove(id);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_session_version_conflict() {
let storage = InMemorySessionStorage::new();
let session = Session::new_from_task("s1".to_string(), "task");
storage.save(session).await.unwrap();
let a = storage.get("s1").await.unwrap().unwrap();
let b = storage.get("s1").await.unwrap().unwrap();
assert_eq!(a.version, 1);
storage.save(a).await.unwrap();
let err = storage.save(b).await.unwrap_err();
assert!(matches!(err, GraphError::SessionConflict(_)), "got: {err:?}");
}
#[tokio::test]
async fn test_session_save_reload_cycle() {
let storage = InMemorySessionStorage::new();
let session = Session::new_from_task("s1".to_string(), "task");
storage.save(session).await.unwrap();
for expected_version in 1..4 {
let s = storage.get("s1").await.unwrap().unwrap();
assert_eq!(s.version, expected_version);
storage.save(s).await.unwrap();
}
}
}