use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use super::{ContentStore, Journal, JournalEntry};
use crate::error::{Result, ShoreError};
use crate::session::store::event_store::event_filename_stem;
use crate::storage::{CreateOutcome, RemoveOutcome};
#[derive(Debug, Default)]
pub(crate) struct InMemoryStore {
events: Mutex<HashMap<String, Vec<u8>>>,
blobs: Mutex<HashMap<String, Vec<u8>>>,
list_event_entries_calls: AtomicUsize,
read_event_bytes_calls: AtomicUsize,
}
impl InMemoryStore {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub(crate) fn list_event_entries_call_count(&self) -> usize {
self.list_event_entries_calls.load(Ordering::Relaxed)
}
pub(crate) fn read_event_bytes_call_count(&self) -> usize {
self.read_event_bytes_calls.load(Ordering::Relaxed)
}
pub(crate) fn journal(self: &Arc<Self>) -> InMemoryJournal {
InMemoryJournal {
store: Arc::clone(self),
}
}
pub(crate) fn content_store(self: &Arc<Self>) -> InMemoryContentStore {
InMemoryContentStore {
store: Arc::clone(self),
}
}
}
#[derive(Debug)]
pub(crate) struct InMemoryJournal {
store: Arc<InMemoryStore>,
}
impl Journal for InMemoryJournal {
fn create_event_once(&self, idempotency_key: &str, bytes: &[u8]) -> Result<CreateOutcome> {
match lock(&self.store.events).entry(idempotency_key.to_owned()) {
Entry::Vacant(slot) => {
slot.insert(bytes.to_vec());
Ok(CreateOutcome::Created)
}
Entry::Occupied(_) => Ok(CreateOutcome::AlreadyExists),
}
}
fn read_event_bytes(&self, idempotency_key: &str) -> Result<Option<Vec<u8>>> {
self.store
.read_event_bytes_calls
.fetch_add(1, Ordering::Relaxed);
Ok(lock(&self.store.events).get(idempotency_key).cloned())
}
fn event_exists(&self, idempotency_key: &str) -> Result<bool> {
Ok(lock(&self.store.events).contains_key(idempotency_key))
}
fn list_event_entries(&self) -> Result<Vec<JournalEntry>> {
self.store
.list_event_entries_calls
.fetch_add(1, Ordering::Relaxed);
let mut entries: Vec<JournalEntry> = lock(&self.store.events)
.iter()
.map(|(key, bytes)| JournalEntry {
key_digest: event_filename_stem(key),
bytes: bytes.clone(),
})
.collect();
entries.sort_by(|a, b| a.key_digest.cmp(&b.key_digest));
Ok(entries)
}
fn head_marker(&self) -> Result<u64> {
Ok(lock(&self.store.events).len() as u64)
}
#[cfg(test)]
fn insert_raw(&self, idempotency_key: &str, bytes: &[u8]) -> Result<()> {
lock(&self.store.events).insert(idempotency_key.to_owned(), bytes.to_vec());
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct InMemoryContentStore {
store: Arc<InMemoryStore>,
}
impl ContentStore for InMemoryContentStore {
fn put_once(&self, content_ref: &str, bytes: &[u8]) -> Result<CreateOutcome> {
match lock(&self.store.blobs).entry(content_ref.to_owned()) {
Entry::Vacant(slot) => {
slot.insert(bytes.to_vec());
Ok(CreateOutcome::Created)
}
Entry::Occupied(_) => Ok(CreateOutcome::AlreadyExists),
}
}
fn get(&self, content_ref: &str) -> Result<Vec<u8>> {
lock(&self.store.blobs)
.get(content_ref)
.cloned()
.ok_or_else(|| ShoreError::Message(format!("missing in-memory blob {content_ref}")))
}
fn get_if_exists(&self, content_ref: &str) -> Result<Option<Vec<u8>>> {
Ok(lock(&self.store.blobs).get(content_ref).cloned())
}
fn remove(&self, content_ref: &str) -> Result<RemoveOutcome> {
Ok(match lock(&self.store.blobs).remove(content_ref) {
Some(_) => RemoveOutcome::Removed,
None => RemoveOutcome::Missing,
})
}
fn list(&self, prefix: &str) -> Result<Vec<String>> {
let mut refs: Vec<String> = lock(&self.store.blobs)
.keys()
.filter(|key| is_immediate_child(key, prefix))
.cloned()
.collect();
refs.sort();
Ok(refs)
}
#[cfg(test)]
fn put_raw(&self, content_ref: &str, bytes: &[u8]) -> Result<()> {
lock(&self.store.blobs).insert(content_ref.to_owned(), bytes.to_vec());
Ok(())
}
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn is_immediate_child(key: &str, prefix: &str) -> bool {
key.strip_prefix(prefix)
.and_then(|rest| rest.strip_prefix('/'))
.is_some_and(|name| !name.is_empty() && !name.contains('/'))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn head_marker_reads_no_event_bytes() {
let store = InMemoryStore::new();
let journal = store.journal();
for key in ["k:a", "k:b", "k:c"] {
journal.create_event_once(key, key.as_bytes()).unwrap();
}
let before = store.read_event_bytes_call_count();
assert_eq!(journal.head_marker().unwrap(), 3);
assert_eq!(
store.read_event_bytes_call_count(),
before,
"the marker is a count, not a read — it touches no event bytes"
);
}
}