leyline-http 0.1.5

HTTP client that impersonates Chrome, Firefox, and Safari on the wire: TLS (JA3/JA4), HTTP/2, and HTTP/3 fingerprints
Documentation
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use crate::core::error::Result;
use crate::core::{Session, SessionState, Tab};
use crate::util::autosave::{Autosave, SaveJob, SaveTarget, Schedule};
use crate::util::lock;

use super::Device;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct DeviceAutosaveOptions {
    pub interval: Duration,
    pub state_interval: Duration,
}

impl DeviceAutosaveOptions {
    #[must_use]
    pub const fn new(interval: Duration) -> Self {
        Self {
            interval,
            state_interval: Duration::from_secs(300),
        }
    }

    #[must_use]
    pub const fn state_interval(mut self, state_interval: Duration) -> Self {
        self.state_interval = state_interval;
        self
    }
}

impl From<Duration> for DeviceAutosaveOptions {
    fn from(interval: Duration) -> Self {
        Self::new(interval)
    }
}

pub struct DeviceAutosave {
    path: Arc<Path>,
    device: Arc<Mutex<Device>>,
    tab: Arc<Mutex<Option<Tab>>>,
    autosave: Autosave,
}

struct DeviceTarget {
    device: Arc<Mutex<Device>>,
    tab: Arc<Mutex<Option<Tab>>>,
    session: Session,
    path: Arc<Path>,
    saved: Option<u64>,
    writing: Option<SessionState>,
    saved_state: Option<SessionState>,
}

impl Device {
    pub fn autosave(
        &self,
        session: &Session,
        path: impl Into<PathBuf>,
        options: impl Into<DeviceAutosaveOptions>,
    ) -> DeviceAutosave {
        let options = options.into();
        let path: Arc<Path> = Arc::from(path.into());
        let device = Arc::new(Mutex::new(self.clone()));
        let tab = Arc::new(Mutex::new(None));
        let target = DeviceTarget {
            device: Arc::clone(&device),
            tab: Arc::clone(&tab),
            session: session.clone(),
            path: Arc::clone(&path),
            saved: None,
            writing: None,
            saved_state: None,
        };
        let schedule = Schedule {
            interval: options.interval,
            periodic: Some(options.state_interval),
            label: "device",
        };
        DeviceAutosave {
            path,
            device,
            tab,
            autosave: Autosave::spawn(target, session.cookies().changes(), schedule),
        }
    }
}

impl DeviceAutosave {
    pub fn update(&self, change: impl FnOnce(&mut Device)) {
        change(&mut lock(&self.device));
        self.autosave.touch();
    }

    pub fn track(&self, tab: &Tab) {
        *lock(&self.tab) = Some(tab.clone());
        self.autosave.touch();
    }

    #[must_use]
    pub fn device(&self) -> Device {
        snapshot(&self.device, &self.tab)
    }

    pub async fn flush(&self) -> Result<()> {
        self.autosave.flush().await
    }

    pub async fn shutdown(self) -> Result<()> {
        self.autosave.shutdown().await
    }
}

impl std::fmt::Debug for DeviceAutosave {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DeviceAutosave")
            .field("path", &self.path)
            .finish_non_exhaustive()
    }
}

impl SaveTarget for DeviceTarget {
    fn prepare(&mut self) -> Option<SaveJob> {
        let live = self.session.cookies();
        let generation = *live.changes().borrow();
        let jar = live.snapshot();
        let jar_dirty = self.saved != Some(generation);
        let mut device = snapshot(&self.device, &self.tab);
        device.state = self.session.state();
        self.writing = Some(device.state.clone());
        let jar_path = device.jar_path.clone();
        device.jar = jar_path.is_none().then(|| jar.clone());
        let path = Arc::clone(&self.path);
        Some(SaveJob {
            generation,
            write: Box::new(move || {
                if let Some(jar_path) = jar_path.filter(|_| jar_dirty) {
                    jar.save_to(jar_path)?;
                }
                device.save_to(&*path)
            }),
        })
    }

    fn saved(&mut self, generation: u64) {
        self.saved = Some(generation);
        self.saved_state = self.writing.take();
    }

    fn unsaved(&self) -> bool {
        self.saved_state.as_ref() != Some(&self.session.state())
    }
}

fn snapshot(device: &Mutex<Device>, tab: &Mutex<Option<Tab>>) -> Device {
    let mut device = lock(device).clone();
    if let Some(tab) = lock(tab).as_ref() {
        device.page = tab.current();
    }
    device
}