wsb-daemon 0.248.6265

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
//! Waking reconciliation when the working tree changes.
//!
//! The sweep alone is correct but slow: an edit waits up to one sweep before
//! anything notices, and a person moving between machines feels every one of
//! those seconds. A watcher does not decide anything — it only says "look
//! again now", and everything about what may move is still decided by the
//! reconciliation itself. That is deliberate: an event-driven synchroniser
//! that trusts its events has to be right about every event it did not get,
//! and this one only has to be right about the sweep it still runs.
//!
//! The watch scope is git's own answer to what matters, through the same
//! module the fleet watcher uses — ignored trees are not watched, `.git` and
//! `.wsb` never are, and one watcher covers every root so the machine spends
//! one inotify instance rather than one per repository.

use std::path::PathBuf;
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;

/// How long a batch of edits is allowed to settle before it wakes anything.
///
/// Short enough that a save feels immediate, long enough that a build writing
/// a thousand files wakes reconciliation once rather than a thousand times.
const DEBOUNCE: Duration = Duration::from_millis(300);

/// A "look again" signal that cannot be missed.
///
/// A flag under a mutex rather than a channel: a nudge that arrives while
/// reconciliation is already running must not be lost, and must not queue up
/// either — the answer to "the tree changed while you were working" is one
/// more pass, however many times it changed.
#[derive(Debug, Default)]
pub struct Nudge {
    pending: Mutex<bool>,
    signal: Condvar,
}

impl Nudge {
    /// Ask for one more pass, now.
    pub fn wake(&self) {
        let mut pending = self.pending.lock().expect("the nudge lock is poisoned");
        *pending = true;
        self.signal.notify_all();
    }

    /// Wait for a nudge, or for `timeout` — whichever comes first.
    ///
    /// The timeout is what makes the watcher an optimisation rather than a
    /// dependency: if every event were lost, this would still return, and the
    /// pass that follows would find the same difference the event described.
    pub fn wait(&self, timeout: Duration) {
        let pending = self.pending.lock().expect("the nudge lock is poisoned");
        if *pending {
            drop(pending);
            self.take();
            return;
        }
        let (mut pending, _) = self
            .signal
            .wait_timeout(pending, timeout)
            .expect("the nudge lock is poisoned");
        *pending = false;
    }

    fn take(&self) {
        let mut pending = self.pending.lock().expect("the nudge lock is poisoned");
        *pending = false;
    }
}

/// Watch every registered workspace's repositories, and nudge on any change.
///
/// Returns the number of roots watched. A machine with no registered
/// workspace watches nothing, which is a fact rather than a failure: the
/// watcher is started again when a workspace registers.
pub fn spawn(roots: Vec<PathBuf>, nudge: Arc<Nudge>) -> Result<usize, String> {
    if roots.is_empty() {
        return Ok(0);
    }
    let count = roots.len();
    crate::web::fs_scope::spawn_scoped_watcher(
        "peer-continuity".to_string(),
        roots,
        // Continuity covers submodules too, and each is its own repository
        // here rather than a boundary to stop at — a change inside one is a
        // change that repository must reconcile.
        true,
        DEBOUNCE,
        move |events| {
            if events.is_empty() {
                return;
            }
            nudge.wake();
        },
    )?;
    Ok(count)
}

#[cfg(test)]
#[path = "watch_tests.rs"]
mod tests;