use crate::core::task::use_visible_task;
use crate::core::view::{Child, View};
use super::nav::build_query_href;
pub fn invalidate_href(path: &str, query: &[(&str, &str)]) -> String {
let mut pairs: Vec<(&str, &str)> = query.to_vec();
pairs.push(("_r", "1"));
build_query_href(path, &pairs)
}
pub fn invalidate_href_now(path: &str, query: &[(&str, &str)]) -> String {
let bust = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis().to_string())
.unwrap_or_else(|_| "0".into());
let mut owned: Vec<(String, String)> = query
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
owned.push(("_r".into(), bust));
let refs: Vec<(&str, &str)> = owned
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
build_query_href(path, &refs)
}
pub fn invalidate_link(path: &str, query: &[(&str, &str)], label: impl Into<String>) -> View {
let href = invalidate_href_now(path, query);
View::Element(crate::core::view::Element {
tag: "a".into(),
attrs: vec![
crate::core::view::Attr {
name: "href".into(),
value: crate::core::view::AttrValue::Static(href),
},
crate::core::view::Attr {
name: "data-r-nav".into(),
value: crate::core::view::AttrValue::Static("true".into()),
},
],
children: vec![Child::Text(label.into())],
dom_id: None,
})
}
pub fn loader_poll(path: &str, interval_ms: u64) -> View {
let path_js = serde_json::to_string(path).unwrap_or_else(|_| "\"/\"".into());
let ms = interval_ms.max(250);
let body = format!(
r#"(async (state, __resuma) => {{
const path = {path_js};
const ms = {ms};
let busy = false;
let idle = false;
let idleT = 0;
const bump = () => {{ idle = false; clearTimeout(idleT); idleT = setTimeout(() => {{ idle = true; }}, 60000); }};
["pointerdown", "keydown"].forEach((ev) => document.addEventListener(ev, bump, {{ passive: true }}));
bump();
const tick = async () => {{
if (busy || document.visibilityState === "hidden" || idle) return;
if (__resuma.presence && __resuma.presence().idle) return;
busy = true;
try {{ await __resuma.invalidate(path); }} catch (_) {{}}
finally {{ busy = false; }}
}};
const id = setInterval(tick, ms);
return () => {{
clearInterval(id);
clearTimeout(idleT);
["pointerdown", "keydown"].forEach((ev) => document.removeEventListener(ev, bump));
}};
}})"#,
path_js = path_js,
ms = ms,
);
let _ = use_visible_task(body);
View::empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalidate_href_adds_cache_bust_param() {
let href = invalidate_href("/users", &[("q", "a")]);
assert!(href.starts_with("/users?"));
assert!(href.contains("q=a"));
assert!(href.contains("_r=1"));
}
#[test]
fn loader_poll_cleanup_removes_idle_listeners() {
use crate::core::context::{with_context, RenderContext, RenderMode};
let ctx = RenderContext::new(RenderMode::Ssr);
let payload = with_context(ctx.clone(), || {
let _ = loader_poll("/", 1000);
ctx.snapshot()
});
let js: String = payload
.visible_tasks
.values()
.map(|t| t.body.as_str())
.collect();
assert!(
js.contains("removeEventListener"),
"poll cleanup must unbind idle listeners: {js}"
);
assert!(js.contains("clearTimeout"), "{js}");
}
}