huskies: merge 843
This commit is contained in:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user