use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
tokio::task_local! {
static PUSHES: RefCell<HashMap<String, Vec<String>>>;
}
pub async fn with_scope<F: Future>(fut: F) -> F::Output {
PUSHES.scope(RefCell::new(HashMap::new()), fut).await
}
pub fn record(name: &str, html: String) {
let _ = PUSHES.try_with(|pushes| {
pushes
.borrow_mut()
.entry(name.to_string())
.or_default()
.push(html);
});
}
pub fn mark(name: &str) -> usize {
PUSHES
.try_with(|pushes| pushes.borrow().get(name).map_or(0, Vec::len))
.unwrap_or(0)
}
pub fn drain_since(name: &str, mark: usize) -> String {
PUSHES
.try_with(|pushes| {
let mut pushes = pushes.borrow_mut();
let Some(entries) = pushes.get_mut(name) else {
return String::new();
};
let mark = mark.min(entries.len());
entries.split_off(mark).concat()
})
.unwrap_or_default()
}
pub fn drain(name: &str) -> String {
drain_since(name, 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_and_drain_are_inert_no_ops_outside_a_scope() {
record("head", "<title>x</title>".to_string());
assert_eq!(mark("head"), 0);
assert_eq!(drain("head"), "");
}
#[tokio::test]
async fn drain_returns_and_removes_everything_recorded_under_that_name() {
with_scope(async {
record("head", "<title>a</title>".to_string());
record("head", "<meta>b</meta>".to_string());
record("scripts", "<script>c</script>".to_string());
assert_eq!(drain("head"), "<title>a</title><meta>b</meta>");
assert_eq!(drain("head"), "");
assert_eq!(drain("scripts"), "<script>c</script>");
})
.await;
}
#[tokio::test]
async fn drain_since_only_takes_entries_recorded_after_the_snapshot() {
with_scope(async {
record("head", "<title>before</title>".to_string());
let mark_point = mark("head");
record("head", "<meta>after</meta>".to_string());
assert_eq!(drain_since("head", mark_point), "<meta>after</meta>");
assert_eq!(drain("head"), "<title>before</title>");
})
.await;
}
#[tokio::test]
async fn each_scope_starts_fresh_and_is_isolated_from_other_tasks() {
with_scope(async {
record("head", "<title>first</title>".to_string());
assert_eq!(drain("head"), "<title>first</title>");
})
.await;
with_scope(async {
assert_eq!(drain("head"), "");
})
.await;
}
}