1use std::collections::BTreeMap;
7use std::sync::{Arc, Mutex};
8
9use async_trait::async_trait;
10use uuid::Uuid;
11
12use super::event::{now_ms_and_rfc3339, AppendEvent, EventId, SessionEventKind, StoredEvent};
13use super::memory_helpers::{meta_for_create, validate_open};
14use super::signing::{
15 chain_root_fold, chain_root_hash, chain_root_init, compute_record_hash, re_anchor_events,
16 verify_event_chain,
17};
18use super::store::{
19 CreateSession, EventPage, ForkResult, ListFilter, ReadRange, SessionId, SessionMeta,
20 SessionStatus, SessionStore, Snapshot, SnapshotId, StoreError, StoreHooks, StoreResult,
21 TruncateResult, VerifyFailure, VerifyReport, MAX_READ_BATCH,
22};
23
24struct SessionRecord {
25 meta: SessionMeta,
26 events: Vec<StoredEvent>,
27 next_event_id: EventId,
28}
29
30impl SessionRecord {
31 fn fresh(meta: SessionMeta) -> Self {
32 Self {
33 meta,
34 events: Vec::new(),
35 next_event_id: 1,
36 }
37 }
38}
39
40#[derive(Default)]
41struct Inner {
42 sessions: BTreeMap<SessionId, SessionRecord>,
43 snapshots: BTreeMap<String, Snapshot>,
44}
45
46#[derive(Clone)]
47pub struct MemorySessionStore {
48 inner: Arc<Mutex<Inner>>,
49 hooks: Arc<StoreHooks>,
50}
51
52impl MemorySessionStore {
53 pub fn new() -> Self {
54 Self::with_hooks(StoreHooks::default())
55 }
56
57 pub fn with_hooks(hooks: StoreHooks) -> Self {
58 Self {
59 inner: Arc::new(Mutex::new(Inner::default())),
60 hooks: Arc::new(hooks),
61 }
62 }
63}
64
65impl Default for MemorySessionStore {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71fn lock(inner: &Arc<Mutex<Inner>>) -> std::sync::MutexGuard<'_, Inner> {
72 inner.lock().unwrap_or_else(|e| e.into_inner())
73}
74
75fn apply_redaction(hooks: &StoreHooks, event: &mut AppendEvent) {
76 let Some(policy) = hooks.redaction.as_ref() else {
77 return;
78 };
79 policy.redact_json_in_place(&mut event.payload);
80 let redacted_headers = policy.redact_headers(&event.headers);
81 event.headers = redacted_headers;
82}
83
84fn append_locked(
90 record: &mut SessionRecord,
91 hooks: &StoreHooks,
92 mut event: AppendEvent,
93) -> StoreResult<StoredEvent> {
94 apply_redaction(hooks, &mut event);
95 validate_open(&record.meta)?;
96 validate_parent(record, &event)?;
97 let (ts_ms, ts) = now_ms_and_rfc3339();
98 let event_id = record.next_event_id;
99 record.next_event_id = record.next_event_id.saturating_add(1);
100 let prev_hash = record.events.last().map(|tail| tail.record_hash.clone());
101 let mut stored = StoredEvent {
102 event_id,
103 session_id: record.meta.id.clone(),
104 tenant_id: record.meta.tenant_id.clone(),
105 parent_event_id: event.parent_event_id,
106 actor: event.actor,
107 kind: event.kind,
108 payload: event.payload,
109 tags: event.tags,
110 headers: event.headers,
111 ts_ms,
112 ts,
113 record_hash: String::new(),
114 prev_hash,
115 signed_by: None,
116 };
117 stored.record_hash = compute_record_hash(&stored);
118 if let Some(signer) = hooks.event_signer.as_ref() {
119 stored.signed_by = Some(signer.sign_event(&stored));
120 }
121 let prev_root = record
122 .meta
123 .chain_root_hash
124 .clone()
125 .unwrap_or_else(chain_root_init);
126 record.events.push(stored.clone());
127 record.meta.event_count = record.events.len();
128 record.meta.last_event_id = Some(event_id);
129 record.meta.updated_at_ms = ts_ms;
130 record.meta.updated_at = stored.ts.clone();
131 record.meta.chain_root_hash = Some(chain_root_fold(&prev_root, &stored.record_hash));
132 Ok(stored)
133}
134
135#[async_trait]
136impl SessionStore for MemorySessionStore {
137 fn hooks(&self) -> &StoreHooks {
138 &self.hooks
139 }
140
141 async fn create(&self, request: CreateSession) -> StoreResult<SessionMeta> {
142 let meta = meta_for_create(request);
143 let mut guard = lock(&self.inner);
144 if guard.sessions.contains_key(&meta.id) {
145 return Err(StoreError::AlreadyExists(meta.id));
146 }
147 guard
148 .sessions
149 .insert(meta.id.clone(), SessionRecord::fresh(meta.clone()));
150 Ok(meta)
151 }
152
153 async fn describe(&self, session_id: &str) -> StoreResult<SessionMeta> {
154 let guard = lock(&self.inner);
155 guard
156 .sessions
157 .get(session_id)
158 .map(|record| record.meta.clone())
159 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))
160 }
161
162 async fn list(&self, filter: ListFilter) -> StoreResult<Vec<SessionMeta>> {
163 let guard = lock(&self.inner);
164 let limit = filter.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH);
165 let mut out: Vec<SessionMeta> = guard
166 .sessions
167 .values()
168 .map(|record| record.meta.clone())
169 .filter(|meta| match_filter(meta, &filter))
170 .collect();
171 out.sort_by_key(|meta| meta.created_at_ms);
172 if let Some(cursor) = filter.cursor.as_ref() {
173 let position = out.iter().position(|meta| meta.id == *cursor);
174 if let Some(start) = position {
175 out = out.into_iter().skip(start + 1).collect();
176 }
177 }
178 out.truncate(limit);
179 Ok(out)
180 }
181
182 async fn append(&self, session_id: &str, event: AppendEvent) -> StoreResult<StoredEvent> {
183 let mut guard = lock(&self.inner);
184 let record = guard
185 .sessions
186 .get_mut(session_id)
187 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
188 append_locked(record, &self.hooks, event)
189 }
190
191 async fn read(&self, session_id: &str, range: ReadRange) -> StoreResult<EventPage> {
192 let guard = lock(&self.inner);
193 let record = guard
194 .sessions
195 .get(session_id)
196 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
197 let from = range.from_event_id.unwrap_or(1);
198 let to = range.to_event_id.unwrap_or(EventId::MAX);
199 let limit = range.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH);
200 let mut events: Vec<StoredEvent> = record
201 .events
202 .iter()
203 .filter(|event| event.event_id >= from && event.event_id <= to)
204 .take(limit)
205 .cloned()
206 .collect();
207 let next_cursor = if events.len() == limit {
208 events.last().map(|tail| tail.event_id + 1)
209 } else {
210 None
211 };
212 if let Some(policy) = self.hooks.redaction.as_ref() {
214 for event in events.iter_mut() {
215 policy.redact_json_in_place(&mut event.payload);
216 }
217 }
218 Ok(EventPage {
219 events,
220 next_cursor,
221 })
222 }
223
224 async fn fork(
225 &self,
226 session_id: &str,
227 at_event_id: EventId,
228 child_id: Option<SessionId>,
229 ) -> StoreResult<ForkResult> {
230 let mut guard = lock(&self.inner);
231 let parent = guard
232 .sessions
233 .get(session_id)
234 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
235 if !parent
236 .events
237 .iter()
238 .any(|event| event.event_id == at_event_id)
239 {
240 return Err(StoreError::InvalidInput(format!(
241 "event {at_event_id} not found in session '{session_id}'"
242 )));
243 }
244 let new_id = child_id.unwrap_or_else(|| Uuid::now_v7().to_string());
245 if guard.sessions.contains_key(&new_id) {
246 return Err(StoreError::AlreadyExists(new_id));
247 }
248 let parent = guard.sessions.get(session_id).unwrap();
249 let (ms, text) = now_ms_and_rfc3339();
250 let mut child_meta = parent.meta.clone();
251 child_meta.id = new_id.clone();
252 child_meta.parent_session_id = Some(parent.meta.id.clone());
253 child_meta.created_at_ms = ms;
254 child_meta.created_at = text.clone();
255 child_meta.updated_at_ms = ms;
256 child_meta.updated_at = text;
257 child_meta.status = SessionStatus::Open;
258 child_meta.closed_at = None;
259 child_meta.closed_at_ms = None;
260 child_meta.soft_deleted_at_ms = None;
261 let parent_events: Vec<StoredEvent> = parent
262 .events
263 .iter()
264 .filter(|event| event.event_id <= at_event_id)
265 .cloned()
266 .collect();
267 let copied_events = re_anchor_events(&parent_events, &new_id);
268 let copied_event_count = copied_events.len();
269 child_meta.event_count = copied_event_count;
270 child_meta.last_event_id = copied_events.last().map(|tail| tail.event_id);
271 child_meta.chain_root_hash = Some(chain_root_hash(&copied_events));
272 let next_event_id = copied_events
273 .last()
274 .map(|tail| tail.event_id + 1)
275 .unwrap_or(1);
276 let child_record = SessionRecord {
277 meta: child_meta,
278 events: copied_events,
279 next_event_id,
280 };
281 guard.sessions.insert(new_id.clone(), child_record);
282 Ok(ForkResult {
283 child_session_id: new_id,
284 forked_from_event_id: at_event_id,
285 copied_event_count,
286 })
287 }
288
289 async fn truncate(
290 &self,
291 session_id: &str,
292 at_event_id: EventId,
293 ) -> StoreResult<TruncateResult> {
294 let mut guard = lock(&self.inner);
295 let record = guard
296 .sessions
297 .get_mut(session_id)
298 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
299 if !record
300 .events
301 .iter()
302 .any(|event| event.event_id == at_event_id)
303 {
304 return Err(StoreError::InvalidInput(format!(
305 "event {at_event_id} not found in session '{session_id}'"
306 )));
307 }
308 let removed = record
309 .events
310 .iter()
311 .filter(|event| event.event_id > at_event_id)
312 .count();
313 record.events.retain(|event| event.event_id <= at_event_id);
314 record.next_event_id = at_event_id + 1;
315 record.meta.event_count = record.events.len();
316 record.meta.last_event_id = record.events.last().map(|tail| tail.event_id);
317 record.meta.chain_root_hash = Some(chain_root_hash(&record.events));
318 let (ms, text) = now_ms_and_rfc3339();
319 record.meta.updated_at_ms = ms;
320 record.meta.updated_at = text;
321 Ok(TruncateResult {
322 kept_event_count: record.events.len(),
323 removed_event_count: removed,
324 new_tip_event_id: record.events.last().map(|tail| tail.event_id),
325 })
326 }
327
328 async fn snapshot(&self, session_id: &str) -> StoreResult<Snapshot> {
329 let mut guard = lock(&self.inner);
330 let record = guard
331 .sessions
332 .get(session_id)
333 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
334 let (ms, text) = now_ms_and_rfc3339();
335 let snapshot = Snapshot {
336 id: SnapshotId(format!("snap-{}", Uuid::now_v7())),
337 session: record.meta.clone(),
338 events: record.events.clone(),
339 captured_at_ms: ms,
340 captured_at: text,
341 };
342 guard
343 .snapshots
344 .insert(snapshot.id.0.clone(), snapshot.clone());
345 Ok(snapshot)
346 }
347
348 async fn replay(&self, snapshot_id: &SnapshotId) -> StoreResult<Snapshot> {
349 let guard = lock(&self.inner);
350 guard
351 .snapshots
352 .get(&snapshot_id.0)
353 .cloned()
354 .ok_or_else(|| StoreError::NotFound(snapshot_id.0.clone()))
355 }
356
357 async fn close(&self, session_id: &str) -> StoreResult<StoredEvent> {
358 let mut guard = lock(&self.inner);
362 let record = guard
363 .sessions
364 .get_mut(session_id)
365 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
366 validate_open(&record.meta)?;
367 let record_root = record
368 .meta
369 .chain_root_hash
370 .clone()
371 .unwrap_or_else(|| chain_root_hash(&record.events));
372 let last_event_id = record.meta.last_event_id.unwrap_or(0);
373 let payload =
374 super::signing::canonical_receipt_payload(session_id, last_event_id, &record_root);
375 let mut append = AppendEvent::new(SessionEventKind::Receipt, payload);
376 append.actor = Some("session_store".into());
377 let mut stored = append_locked(record, &self.hooks, append)?;
378 if let Some(signer) = self
383 .hooks
384 .receipt_signer
385 .as_ref()
386 .or(self.hooks.event_signer.as_ref())
387 {
388 let signature = signer.sign_receipt(&record_root);
389 if let Some(receipt) = record
392 .events
393 .iter_mut()
394 .find(|event| event.event_id == stored.event_id)
395 {
396 receipt.signed_by = Some(signature.clone());
397 }
398 stored.signed_by = Some(signature);
399 }
400 let (ms, text) = now_ms_and_rfc3339();
401 record.meta.status = SessionStatus::Closed;
402 record.meta.closed_at_ms = Some(ms);
403 record.meta.closed_at = Some(text);
404 Ok(stored)
405 }
406
407 async fn soft_delete(&self, session_id: &str) -> StoreResult<SessionMeta> {
408 let mut guard = lock(&self.inner);
409 let record = guard
410 .sessions
411 .get_mut(session_id)
412 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
413 match record.meta.status {
414 SessionStatus::HardDeleted => return Err(StoreError::NotFound(session_id.to_string())),
415 SessionStatus::SoftDeleted => return Ok(record.meta.clone()),
416 _ => {}
417 }
418 let (ms, text) = now_ms_and_rfc3339();
419 record.meta.status = SessionStatus::SoftDeleted;
420 record.meta.soft_deleted_at_ms = Some(ms);
421 record.meta.updated_at_ms = ms;
422 record.meta.updated_at = text;
423 Ok(record.meta.clone())
424 }
425
426 async fn hard_delete(&self, session_id: &str) -> StoreResult<()> {
427 let mut guard = lock(&self.inner);
428 guard
429 .sessions
430 .remove(session_id)
431 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
432 Ok(())
433 }
434
435 async fn verify(&self, session_id: &str) -> StoreResult<VerifyReport> {
436 let guard = lock(&self.inner);
437 let record = guard
438 .sessions
439 .get(session_id)
440 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
441 let chain_root = chain_root_hash(&record.events);
442 let event_verifier = self
443 .hooks
444 .event_signer
445 .as_ref()
446 .map(|signer| signer.verifying_key());
447 let receipt_verifier = self
448 .hooks
449 .receipt_signer
450 .as_ref()
451 .or(self.hooks.event_signer.as_ref())
452 .map(|signer| signer.verifying_key());
453 let (signed, failures) = verify_event_chain(
454 &record.events,
455 event_verifier.as_ref(),
456 receipt_verifier.as_ref(),
457 );
458 Ok(VerifyReport {
459 session_id: session_id.to_string(),
460 chain_root_hash: chain_root,
461 event_count: record.events.len(),
462 signed_event_count: signed,
463 failures: failures
464 .into_iter()
465 .map(|(event_id, reason)| VerifyFailure { event_id, reason })
466 .collect(),
467 })
468 }
469}
470
471fn validate_parent(record: &SessionRecord, event: &AppendEvent) -> StoreResult<()> {
472 let Some(parent_event_id) = event.parent_event_id else {
473 return Ok(());
474 };
475 if record
476 .events
477 .iter()
478 .any(|stored| stored.event_id == parent_event_id)
479 {
480 Ok(())
481 } else {
482 Err(StoreError::InvalidInput(format!(
483 "parent_event_id {parent_event_id} not present in session"
484 )))
485 }
486}
487
488fn match_filter(meta: &SessionMeta, filter: &ListFilter) -> bool {
489 if let Some(tenant) = filter.tenant_id.as_ref() {
490 if meta.tenant_id.as_deref() != Some(tenant.as_str()) {
491 return false;
492 }
493 }
494 if let Some(persona) = filter.persona.as_ref() {
495 if meta.persona.as_deref() != Some(persona.as_str()) {
496 return false;
497 }
498 }
499 if let Some(status) = filter.status {
500 if meta.status != status {
501 return false;
502 }
503 }
504 if let Some(tag) = filter.tag.as_ref() {
505 if !meta.tags.iter().any(|value| value == tag) {
506 return false;
507 }
508 }
509 if let Some(after) = filter.created_after_ms {
510 if meta.created_at_ms < after {
511 return false;
512 }
513 }
514 if let Some(before) = filter.created_before_ms {
515 if meta.created_at_ms > before {
516 return false;
517 }
518 }
519 true
520}