huskies: merge 843

This commit is contained in:
dave
2026-04-29 15:49:50 +00:00
parent 7f8467b068
commit 7505f7fdeb
4 changed files with 450 additions and 412 deletions
+105
View File
@@ -0,0 +1,105 @@
//! In-memory content store — fast synchronous reads for story markdown.
//!
//! Backed by a `HashMap<story_id, markdown>` 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<Mutex<HashMap<String, String>>> = 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<Mutex<HashMap<String, String>>> = 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<HashMap<String, String>>> {
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<HashMap<String, String>>> {
let tl = CONTENT_STORE_TL.with(|lock| {
if lock.get().is_some() {
Some(lock as *const OnceLock<Mutex<HashMap<String, String>>>)
} 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<String> {
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<String> {
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<String, String>) {
let _ = CONTENT_STORE.set(Mutex::new(map));
}