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}