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