1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Session access trait for decoupling use cases from server infrastructure.
use async_trait::async_trait;
use bamboo_domain::Session;
use super::errors::{RespondError, SessionLoadError, SessionSaveError};
use crate::SessionRepository;
/// Trait for loading and persisting sessions.
///
/// The canonical implementation is [`SessionRepository`] (the framework-owned
/// coordinator). The server's `AppState` also implements it by delegating to
/// its `session_repo`. Use cases depend on this trait rather than concrete
/// server types.
#[async_trait]
pub trait SessionAccess: Send + Sync {
/// Load a session by ID (from cache or storage).
async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError>;
/// Load an existing session or create a new one with the given model.
async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError>;
/// Load a session, merging memory and storage using a preference heuristic.
///
/// Prefers storage when it has a pending question or newer `updated_at`.
async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError>;
/// Save a session to persistent storage only.
///
/// Implementations may merge concurrent UI edits to
/// title/title_generated/pinned/title_version
/// from disk back into `session` (which is why this takes `&mut`).
async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError>;
/// Save a session to persistent storage and update the in-memory cache.
///
/// Implementations may merge concurrent UI edits to
/// title/title_generated/pinned/title_version
/// from disk back into `session` (which is why this takes `&mut`).
async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError>;
/// Inspect the authoritative snapshot used by a pending-response
/// transaction, without mutating it. Canonical repositories override this
/// so durable consumed-response state wins over a stale cache candidate.
async fn inspect_for_response(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
self.load_merged(id).await
}
/// Compare/mutate/persist the latest session for a pending response.
/// Implementations backed by a canonical per-session lock override this so
/// the whole operation is atomic. The default preserves compatibility for
/// lightweight/in-memory adapters; the respond use case also serializes
/// its entrypoints so those adapters cannot double-consume concurrently.
async fn mutate_for_response(
&self,
id: &str,
mutate: Box<
dyn for<'session> FnOnce(&'session mut Session) -> Result<(), RespondError>
+ Send
+ 'static,
>,
) -> Result<Option<Session>, RespondError> {
let Some(mut session) = self.load_merged(id).await? else {
return Ok(None);
};
mutate(&mut session)?;
self.save_and_cache(&mut session).await?;
Ok(Some(session))
}
}
/// The framework-owned [`SessionRepository`] is the canonical `SessionAccess`.
/// Server `AppState` delegates to its `session_repo`; SDK / in-process callers
/// can use a `SessionRepository` directly as a `SessionAccess`.
#[async_trait]
impl SessionAccess for SessionRepository {
async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
// Historical contract: absence is an error, not Ok(None).
match SessionRepository::load(self, id).await {
Some(session) => Ok(Some(session)),
None => Err(SessionLoadError::NotFound(id.to_string())),
}
}
async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError> {
Ok(SessionRepository::load_or_create(self, id, model).await)
}
async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
SessionRepository::load_merged_checked(self, id)
.await
.map_err(|error| SessionLoadError::StorageError(error.to_string()))
}
async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError> {
// Storage-only persist (no cache write), matching the trait contract.
self.persistence()
.merge_save_runtime(session)
.await
.map_err(|e| SessionSaveError::StorageError(e.to_string()))
}
async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError> {
SessionRepository::save_and_cache(self, session).await;
Ok(())
}
async fn inspect_for_response(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
let cache = self.cache().clone();
let session_id = id.to_string();
self.persistence()
.inspect_runtime_session_for_response(id, move || {
crate::read_cached_session(&cache, &session_id)
})
.await
.map_err(|error| SessionLoadError::StorageError(error.to_string()))
}
async fn mutate_for_response(
&self,
id: &str,
mutate: Box<
dyn for<'session> FnOnce(&'session mut Session) -> Result<(), RespondError>
+ Send
+ 'static,
>,
) -> Result<Option<Session>, RespondError> {
let cache_for_load = self.cache().clone();
let publish_cache = self.cache().clone();
let session_id = id.to_string();
match self
.persistence()
.mutate_runtime_session_and_publish(
id,
move || crate::read_cached_session(&cache_for_load, &session_id),
move |session| mutate(session),
move |saved| {
publish_cache.insert(
saved.id.clone(),
std::sync::Arc::new(parking_lot::RwLock::new(saved.clone())),
);
},
)
.await
{
Ok(Ok(session)) => Ok(session),
Ok(Err(error)) => Err(error),
Err(error) => Err(RespondError::SaveFailed(SessionSaveError::StorageError(
error.to_string(),
))),
}
}
}