use anyhow::{anyhow, Result};
use chromiumoxide::Page;
use rand::{Rng, SeedableRng};
use std::time::{Duration, Instant};
pub const DEFAULT_WARMUP_MS: u64 = 4_000;
pub async fn warm_session(page: &Page, budget: Duration) -> Result<()> {
let start = Instant::now();
let deadline = start + budget;
let viewport = page
.evaluate("({w: window.innerWidth || 1280, h: window.innerHeight || 800})")
.await
.map_err(|e| anyhow!("warmup viewport probe failed: {e}"))?
.into_value::<serde_json::Value>()
.unwrap_or(serde_json::Value::Null);
let vw = viewport["w"].as_f64().unwrap_or(1280.0);
let vh = viewport["h"].as_f64().unwrap_or(800.0);
let mut rng = rand::rngs::StdRng::from_entropy();
let mut cx = vw * 0.5 + rng.gen_range(-50.0..50.0);
let mut cy = vh * 0.5 + rng.gen_range(-50.0..50.0);
while Instant::now() < deadline {
let action = rng.gen_range(0..10);
let result: Result<()> = match action {
0..=4 => {
let tx = rng.gen_range(50.0..(vw - 50.0).max(50.0));
let ty = rng.gen_range(50.0..(vh - 50.0).max(50.0));
let r = mouse_move_smooth(page, cx, cy, tx, ty).await;
cx = tx;
cy = ty;
r
}
5..=6 => {
let down = rng.gen_range(80..240);
let _ = page.evaluate(format!("window.scrollBy(0, {})", down)).await;
tokio::time::sleep(Duration::from_millis(rng.gen_range(180..500))).await;
let _ = page
.evaluate(format!("window.scrollBy(0, -{})", down))
.await;
Ok(())
}
7 => {
let hover_js = r#"
(() => {
const els = document.querySelectorAll('a, button, h1, h2, h3, p');
if (els.length === 0) return null;
const el = els[Math.floor(Math.random() * els.length)];
const r = el.getBoundingClientRect();
return [r.left + r.width / 2, r.top + r.height / 2];
})()
"#;
if let Ok(raw) = page.evaluate(hover_js).await {
if let Ok(Some([tx, ty])) = raw.into_value::<Option<[f64; 2]>>() {
if tx.is_finite() && ty.is_finite() && tx > 0.0 && ty > 0.0 {
let r = mouse_move_smooth(page, cx, cy, tx, ty).await;
cx = tx;
cy = ty;
let _ = r;
}
}
}
Ok(())
}
_ => {
tokio::time::sleep(Duration::from_millis(rng.gen_range(120..400))).await;
Ok(())
}
};
if let Err(e) = result {
tracing::debug!("warmup action failed (continuing): {e}");
}
tokio::time::sleep(Duration::from_millis(rng.gen_range(80..250))).await;
}
Ok(())
}
async fn mouse_move_smooth(page: &Page, x0: f64, y0: f64, x1: f64, y1: f64) -> Result<()> {
let mut rng = rand::rngs::StdRng::from_entropy();
let steps = rng.gen_range(8..18);
let (cx1, cy1) = (
x0 + (x1 - x0) / 3.0 + rng.gen_range(-40.0..40.0),
y0 + (y1 - y0) / 3.0 + rng.gen_range(-40.0..40.0),
);
let (cx2, cy2) = (
x0 + 2.0 * (x1 - x0) / 3.0 + rng.gen_range(-40.0..40.0),
y0 + 2.0 * (y1 - y0) / 3.0 + rng.gen_range(-40.0..40.0),
);
for i in 0..=steps {
let t = i as f64 / steps as f64;
let omt = 1.0 - t;
let bx = omt.powi(3) * x0
+ 3.0 * omt.powi(2) * t * cx1
+ 3.0 * omt * t.powi(2) * cx2
+ t.powi(3) * x1;
let by = omt.powi(3) * y0
+ 3.0 * omt.powi(2) * t * cy1
+ 3.0 * omt * t.powi(2) * cy2
+ t.powi(3) * y1;
let jx = rng.gen_range(-1.5..1.5);
let jy = rng.gen_range(-1.5..1.5);
let _ = page
.evaluate(format!(
"(() => {{ const e = new MouseEvent('mousemove', {{ \
bubbles: true, clientX: {x}, clientY: {y} \
}}); document.elementFromPoint({x}, {y})?.dispatchEvent(e); }})()",
x = bx + jx,
y = by + jy
))
.await;
tokio::time::sleep(Duration::from_millis(rng.gen_range(8..22))).await;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_warmup_ms_is_4_seconds() {
assert_eq!(DEFAULT_WARMUP_MS, 4_000);
}
#[test]
fn module_exports_warm_session() {
let _: &str = std::any::type_name_of_val(&warm_session);
}
}