Skip to main content

bamboo_engine/session_app/
repository.rs

1//! Session access trait for decoupling use cases from server infrastructure.
2
3use async_trait::async_trait;
4use bamboo_domain::Session;
5
6use super::errors::{SessionLoadError, SessionSaveError};
7use crate::SessionRepository;
8
9/// Trait for loading and persisting sessions.
10///
11/// The canonical implementation is [`SessionRepository`] (the framework-owned
12/// coordinator). The server's `AppState` also implements it by delegating to
13/// its `session_repo`. Use cases depend on this trait rather than concrete
14/// server types.
15#[async_trait]
16pub trait SessionAccess: Send + Sync {
17    /// Load a session by ID (from cache or storage).
18    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError>;
19
20    /// Load an existing session or create a new one with the given model.
21    async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError>;
22
23    /// Load a session, merging memory and storage using a preference heuristic.
24    ///
25    /// Prefers storage when it has a pending question or newer `updated_at`.
26    async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError>;
27
28    /// Save a session to persistent storage only.
29    ///
30    /// Implementations may merge concurrent UI edits to title/pinned/title_version
31    /// from disk back into `session` (which is why this takes `&mut`).
32    async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError>;
33
34    /// Save a session to persistent storage and update the in-memory cache.
35    ///
36    /// Implementations may merge concurrent UI edits to title/pinned/title_version
37    /// from disk back into `session` (which is why this takes `&mut`).
38    async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError>;
39}
40
41/// The framework-owned [`SessionRepository`] is the canonical `SessionAccess`.
42/// Server `AppState` delegates to its `session_repo`; SDK / in-process callers
43/// can use a `SessionRepository` directly as a `SessionAccess`.
44#[async_trait]
45impl SessionAccess for SessionRepository {
46    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
47        // Historical contract: absence is an error, not Ok(None).
48        match SessionRepository::load(self, id).await {
49            Some(session) => Ok(Some(session)),
50            None => Err(SessionLoadError::NotFound(id.to_string())),
51        }
52    }
53
54    async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError> {
55        Ok(SessionRepository::load_or_create(self, id, model).await)
56    }
57
58    async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
59        Ok(SessionRepository::load_merged(self, id).await)
60    }
61
62    async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError> {
63        // Storage-only persist (no cache write), matching the trait contract.
64        self.persistence()
65            .merge_save_runtime(session)
66            .await
67            .map_err(|e| SessionSaveError::StorageError(e.to_string()))
68    }
69
70    async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError> {
71        SessionRepository::save_and_cache(self, session).await;
72        Ok(())
73    }
74}