//! In-memory content store — fast synchronous reads for story markdown. //! //! Backed by a `HashMap` wrapped in a `Mutex`. In //! non-test builds the store lives in a process-global `OnceLock`; in tests //! each thread gets its own isolated copy via a `thread_local!` to avoid //! cross-test pollution. use std::collections::HashMap; use std::sync::{Mutex, OnceLock}; static CONTENT_STORE: OnceLock>> = OnceLock::new(); #[cfg(test)] thread_local! { /// Per-thread isolated content store used in tests to prevent cross-test pollution. pub(super) static CONTENT_STORE_TL: OnceLock>> = const { OnceLock::new() }; } #[cfg(not(test))] /// Return a reference to the process-global content store, or `None` if not yet initialised. pub(super) fn get_content_store() -> Option<&'static Mutex>> { CONTENT_STORE.get() } #[cfg(test)] /// Return the thread-local content store for tests, falling back to the global store. pub(super) fn get_content_store() -> Option<&'static Mutex>> { let tl = CONTENT_STORE_TL.with(|lock| { if lock.get().is_some() { Some(lock as *const OnceLock>>) } else { None } }); if let Some(ptr) = tl { // SAFETY: The thread-local lives as long as the thread, which outlives // any test using it. We only need 'static for the return type. let lock = unsafe { &*ptr }; lock.get() } else { CONTENT_STORE.get() } } /// Read the full markdown content of a story from the in-memory store. pub fn read_content(story_id: &str) -> Option { let store = get_content_store()?; let map = store.lock().ok()?; map.get(story_id).cloned() } /// Write (or overwrite) the full markdown content of a story. /// /// Updates the in-memory store immediately. pub fn write_content(story_id: &str, content: &str) { if let Some(store) = get_content_store() && let Ok(mut map) = store.lock() { map.insert(story_id.to_string(), content.to_string()); } } /// Remove a story's content from the in-memory store. pub fn delete_content(story_id: &str) { if let Some(store) = get_content_store() && let Ok(mut map) = store.lock() { map.remove(story_id); } } /// Ensure the in-memory content store is initialised. /// /// Safe to call multiple times — the `OnceLock` is set at most once. pub fn ensure_content_store() { #[cfg(not(test))] { let _ = CONTENT_STORE.set(Mutex::new(HashMap::new())); } #[cfg(test)] { CONTENT_STORE_TL.with(|lock| { if lock.get().is_none() { let _ = lock.set(Mutex::new(HashMap::new())); } }); crate::crdt_state::init_for_test(); } } /// Return all story IDs present in the content store. pub fn all_content_ids() -> Vec { match get_content_store() { Some(store) => match store.lock() { Ok(map) => map.keys().cloned().collect(), Err(_) => Vec::new(), }, None => Vec::new(), } } /// Initialise the content store from a pre-loaded map (used during DB startup). pub(super) fn init_content_store(map: HashMap) { let _ = CONTENT_STORE.set(Mutex::new(map)); }