forge_browser/idle.rs
1//! The idle release: a profile nobody has touched for a while closes.
2//!
3//! **The clock is the server's**, because the browser and the profiles are:
4//! every call and every attach touches the profile, and a sweep reads the
5//! clocks. A close ends the host and its tabs, not the profile: the next call
6//! starts a fresh browser with the logins intact.
7//!
8//! **A held tab holds the profile open.** A parked hand-off is a person being
9//! asked to act in the page, so the sweep skips a profile with any held tab
10//! rather than closing it under them; the hold's resolution is what lets the
11//! clock close it later.
12
13use std::time::{Duration, Instant};
14
15/// How often the sweep looks. The clock is coarse on purpose: a browser
16/// sitting a few seconds past its bound is not a fault.
17pub const SWEEP: Duration = Duration::from_secs(30);
18
19/// One profile's clock.
20#[derive(Clone, Copy, Debug)]
21pub struct Touch {
22 /// When a seat last used the profile: a call and an attach.
23 pub at: Instant,
24}
25
26impl Touch {
27 pub fn now() -> Self {
28 Self { at: Instant::now() }
29 }
30
31 pub fn touched(&mut self) {
32 *self = Self::now();
33 }
34}
35
36/// What one candidate's clock says this pass.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Step {
39 Nothing,
40 Close,
41}
42
43/// The clock itself, pure: what the sweep should do for a profile touched at
44/// `last`, now.
45pub fn step(last: Instant, now: Instant, idle: Duration) -> Step {
46 if now.duration_since(last) < idle { Step::Nothing } else { Step::Close }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 const IDLE: Duration = Duration::from_secs(30 * 60);
54
55 /// The clock, one case per arm: a fresh touch sits quiet, and a use
56 /// restarts the bound.
57 #[test]
58 fn the_clock_closes_at_the_bound_and_a_touch_resets_it() {
59 let base = Instant::now();
60 assert_eq!(
61 step(base, base + Duration::from_secs(60), IDLE),
62 Step::Nothing,
63 "a fresh touch is quiet",
64 );
65 let inside = (base + IDLE).checked_sub(Duration::from_secs(1)).expect("the test's clock");
66 assert_eq!(
67 step(base, inside, IDLE),
68 Step::Nothing,
69 "just inside the bound it is still quiet"
70 );
71 assert_eq!(step(base, base + IDLE, IDLE), Step::Close, "at the bound it closes");
72 assert_eq!(
73 step(base + Duration::from_secs(60), base + IDLE, IDLE),
74 Step::Nothing,
75 "a touch inside the old bound makes the browser fresh again",
76 );
77 }
78}