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::{RespondError, 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    /// Use the exact successor's Root capability for a response CAS. External
18    /// adapters without the named writer reject a Root handoff before consume.
19    fn bind_response_writer(
20        &self,
21        owner: Option<bamboo_domain::RootActorRuntimeWrite>,
22    ) -> Result<Option<std::sync::Arc<dyn SessionAccess>>, SessionSaveError> {
23        if owner.is_some() {
24            return Err(SessionSaveError::StorageError(
25                "Root response writer is unsupported".into(),
26            ));
27        }
28        Ok(None)
29    }
30
31    /// Bind a response writer while retaining the successor's captured wait.
32    /// Implementations that cannot retain this provenance must fail closed.
33    fn bind_response_writer_with_inherited(
34        &self,
35        owner: Option<bamboo_domain::RootActorRuntimeWrite>,
36        inherited: Option<bamboo_domain::InheritedChildWait>,
37    ) -> Result<Option<std::sync::Arc<dyn SessionAccess>>, SessionSaveError> {
38        if inherited.is_some() {
39            return Err(SessionSaveError::StorageError(
40                "inherited response writer is unsupported".into(),
41            ));
42        }
43        self.bind_response_writer(owner)
44    }
45
46    /// Load a session by ID (from cache or storage).
47    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError>;
48
49    /// Load an existing session or create a new one with the given model.
50    async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError>;
51
52    /// Load a session, merging memory and storage using a preference heuristic.
53    ///
54    /// Prefers storage when it has a pending question or newer `updated_at`.
55    async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError>;
56
57    /// Save a session to persistent storage only.
58    ///
59    /// Implementations may merge concurrent UI edits to
60    /// title/title_generated/pinned/title_version
61    /// from disk back into `session` (which is why this takes `&mut`).
62    async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError>;
63
64    /// Save a session to persistent storage and update the in-memory cache.
65    ///
66    /// Implementations may merge concurrent UI edits to
67    /// title/title_generated/pinned/title_version
68    /// from disk back into `session` (which is why this takes `&mut`).
69    async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError>;
70
71    /// Inspect the authoritative snapshot used by a pending-response
72    /// transaction, without mutating it. Canonical repositories override this
73    /// so durable consumed-response state wins over a stale cache candidate.
74    async fn inspect_for_response(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
75        self.load_merged(id).await
76    }
77
78    /// Compare/mutate/persist the latest session for a pending response.
79    /// Implementations backed by a canonical per-session lock override this so
80    /// the whole operation is atomic. The default preserves compatibility for
81    /// lightweight/in-memory adapters; the respond use case also serializes
82    /// its entrypoints so those adapters cannot double-consume concurrently.
83    async fn mutate_for_response(
84        &self,
85        id: &str,
86        mutate: Box<
87            dyn for<'session> FnOnce(&'session mut Session) -> Result<(), RespondError>
88                + Send
89                + 'static,
90        >,
91    ) -> Result<Option<Session>, RespondError> {
92        let Some(mut session) = self.load_merged(id).await? else {
93            return Ok(None);
94        };
95        mutate(&mut session)?;
96        self.save_and_cache(&mut session).await?;
97        Ok(Some(session))
98    }
99}
100
101/// The framework-owned [`SessionRepository`] is the canonical `SessionAccess`.
102/// Server `AppState` delegates to its `session_repo`; SDK / in-process callers
103/// can use a `SessionRepository` directly as a `SessionAccess`.
104#[async_trait]
105impl SessionAccess for SessionRepository {
106    fn bind_response_writer(
107        &self,
108        owner: Option<bamboo_domain::RootActorRuntimeWrite>,
109    ) -> Result<Option<std::sync::Arc<dyn SessionAccess>>, SessionSaveError> {
110        owner
111            .map(|owner| {
112                self.bind_root_response_writer(owner)
113                    .map(|bound| std::sync::Arc::new(bound) as std::sync::Arc<dyn SessionAccess>)
114                    .map_err(|error| SessionSaveError::StorageError(error.to_string()))
115            })
116            .transpose()
117    }
118
119    fn bind_response_writer_with_inherited(
120        &self,
121        owner: Option<bamboo_domain::RootActorRuntimeWrite>,
122        inherited: Option<bamboo_domain::InheritedChildWait>,
123    ) -> Result<Option<std::sync::Arc<dyn SessionAccess>>, SessionSaveError> {
124        match owner {
125            Some(owner) => self
126                .bind_root_response_writer_with_inherited(owner, inherited)
127                .map(|bound| Some(std::sync::Arc::new(bound) as std::sync::Arc<dyn SessionAccess>))
128                .map_err(|error| SessionSaveError::StorageError(error.to_string())),
129            None if inherited.is_some() => Err(SessionSaveError::StorageError(
130                "inherited response writer requires its Root owner".into(),
131            )),
132            None => Ok(None),
133        }
134    }
135
136    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
137        // Historical contract: absence is an error, not Ok(None).
138        match SessionRepository::load(self, id).await {
139            Some(session) => Ok(Some(session)),
140            None => Err(SessionLoadError::NotFound(id.to_string())),
141        }
142    }
143
144    async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError> {
145        Ok(SessionRepository::load_or_create(self, id, model).await)
146    }
147
148    async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
149        SessionRepository::load_merged_checked(self, id)
150            .await
151            .map_err(|error| SessionLoadError::StorageError(error.to_string()))
152    }
153
154    async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError> {
155        // Storage-only persist (no cache write), matching the trait contract.
156        self.persistence()
157            .merge_save_runtime(session)
158            .await
159            .map_err(|e| SessionSaveError::StorageError(e.to_string()))
160    }
161
162    async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError> {
163        SessionRepository::save_and_cache(self, session).await;
164        Ok(())
165    }
166
167    async fn inspect_for_response(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
168        let cache = self.cache().clone();
169        let session_id = id.to_string();
170        self.persistence()
171            .inspect_runtime_session_for_response(id, move || {
172                crate::read_cached_session(&cache, &session_id)
173            })
174            .await
175            .map_err(|error| SessionLoadError::StorageError(error.to_string()))
176    }
177
178    async fn mutate_for_response(
179        &self,
180        id: &str,
181        mutate: Box<
182            dyn for<'session> FnOnce(&'session mut Session) -> Result<(), RespondError>
183                + Send
184                + 'static,
185        >,
186    ) -> Result<Option<Session>, RespondError> {
187        let cache_for_load = self.cache().clone();
188        let publish_cache = self.cache().clone();
189        let session_id = id.to_string();
190        match self
191            .persistence()
192            .mutate_runtime_session_and_publish(
193                id,
194                move || crate::read_cached_session(&cache_for_load, &session_id),
195                move |session| mutate(session),
196                move |saved| {
197                    publish_cache.insert(
198                        saved.id.clone(),
199                        std::sync::Arc::new(crate::SessionSnapshot::new(saved.clone())),
200                    );
201                },
202            )
203            .await
204        {
205            Ok(Ok(session)) => Ok(session),
206            Ok(Err(error)) => Err(error),
207            Err(error) => Err(RespondError::SaveFailed(SessionSaveError::StorageError(
208                error.to_string(),
209            ))),
210        }
211    }
212}