bob_adapters/
store_memory.rs1use bob_core::{
51 error::StoreError,
52 ports::SessionStore,
53 types::{SessionId, SessionState},
54};
55
56#[derive(Debug)]
61pub struct InMemorySessionStore {
62 inner: scc::HashMap<SessionId, SessionState>,
63}
64
65impl Default for InMemorySessionStore {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl InMemorySessionStore {
72 #[must_use]
74 pub fn new() -> Self {
75 Self { inner: scc::HashMap::new() }
76 }
77}
78
79#[async_trait::async_trait]
80impl SessionStore for InMemorySessionStore {
81 async fn load(&self, id: &SessionId) -> Result<Option<SessionState>, StoreError> {
82 let state = self.inner.read_async(id, |_k, v| v.clone()).await;
83 Ok(state)
84 }
85
86 async fn save(&self, id: &SessionId, state: &SessionState) -> Result<(), StoreError> {
87 let entry = self.inner.entry_async(id.clone()).await;
89 match entry {
90 scc::hash_map::Entry::Occupied(mut occ) => {
91 occ.get_mut().clone_from(state);
92 }
93 scc::hash_map::Entry::Vacant(vac) => {
94 let _ = vac.insert_entry(state.clone());
95 }
96 }
97 Ok(())
98 }
99}
100
101#[cfg(test)]
104mod tests {
105 use std::sync::Arc;
106
107 use bob_core::types::Message;
108
109 use super::*;
110
111 #[tokio::test]
112 async fn load_missing_returns_none() {
113 let store = InMemorySessionStore::new();
114 let result = store.load(&"nonexistent".to_string()).await;
115 assert!(result.is_ok());
116 assert!(result.ok().flatten().is_none());
117 }
118
119 #[tokio::test]
120 async fn roundtrip_save_load() {
121 let store = InMemorySessionStore::new();
122 let id = "sess-1".to_string();
123 let state = SessionState {
124 messages: vec![Message { role: bob_core::types::Role::User, content: "hello".into() }],
125 ..SessionState::default()
126 };
127
128 store.save(&id, &state).await.ok();
129 let loaded = store.load(&id).await.ok().flatten();
130 assert!(loaded.is_some());
131 assert_eq!(loaded.as_ref().map(|s| s.messages.len()), Some(1));
132 }
133
134 #[tokio::test]
135 async fn overwrite_existing_session() {
136 let store = InMemorySessionStore::new();
137 let id = "sess-2".to_string();
138
139 let state1 = SessionState {
140 messages: vec![Message { role: bob_core::types::Role::User, content: "first".into() }],
141 ..SessionState::default()
142 };
143 store.save(&id, &state1).await.ok();
144
145 let state2 = SessionState {
146 messages: vec![
147 Message { role: bob_core::types::Role::User, content: "first".into() },
148 Message { role: bob_core::types::Role::Assistant, content: "second".into() },
149 ],
150 ..SessionState::default()
151 };
152 store.save(&id, &state2).await.ok();
153
154 let loaded = store.load(&id).await.ok().flatten();
155 assert_eq!(loaded.as_ref().map(|s| s.messages.len()), Some(2));
156 }
157
158 #[tokio::test]
159 async fn arc_dyn_session_store_works() {
160 let store: Arc<dyn SessionStore> = Arc::new(InMemorySessionStore::new());
161 let id = "sess-arc".to_string();
162 let state = SessionState::default();
163 store.save(&id, &state).await.ok();
164 let loaded = store.load(&id).await.ok().flatten();
165 assert!(loaded.is_some());
166 }
167}