Skip to main content

dynamic_config/builder/
watching.rs

1//! Starting the file watcher from a builder.
2//!
3//! The same watcher as the generated `start_watch()` — same debounce, same
4//! one-watcher-per-type registry — with each reload loading through this
5//! builder and installing into the type's snapshot.
6
7use serde::de::DeserializeOwned;
8
9use super::Builder;
10
11#[cfg(feature = "watch")]
12#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
13impl<T: DeserializeOwned + Send + Sync + 'static> Builder<T> {
14    /// Reloads on file changes until the returned handle is dropped.
15    ///
16    /// The same watcher as the attribute's `watch` — same debounce, same
17    /// registry: a type is watched once, whichever surface starts it, so a
18    /// builder watch while `start_watch()` runs (or the reverse) is
19    /// `AlreadyExists`. Each reload loads through this builder and installs
20    /// into the type's snapshot, firing `on_reload` hooks and waking
21    /// `changes()` exactly as any other install does; a configured
22    /// [`cache`](Self::cache) is rewritten after each clean reload.
23    ///
24    /// # Errors
25    ///
26    /// As the generated `start_watch()`: no watchable directory, a backend
27    /// that cannot start, or the type already being watched — plus a builder
28    /// with no installer, which has nothing to reload *into*.
29    pub fn watch(
30        &self,
31        debounce: core::time::Duration,
32    ) -> std::io::Result<crate::watch::WatchHandle> {
33        self.watch_with(debounce, crate::watch::WatchMode::Native)
34    }
35
36    /// [`watch`](Self::watch) with the detection strategy chosen explicitly
37    /// — polling is what network and overlay filesystems need.
38    ///
39    /// # Errors
40    ///
41    /// As [`watch`](Self::watch).
42    pub fn watch_with(
43        &self,
44        debounce: core::time::Duration,
45        mode: crate::watch::WatchMode,
46    ) -> std::io::Result<crate::watch::WatchHandle> {
47        let Some(install) = self.install else {
48            return Err(std::io::Error::new(
49                std::io::ErrorKind::InvalidInput,
50                "this builder is tied to no config type, so a reload would \
51                 have nowhere to install; start from the generated \
52                 `builder()` on a `#[dynamic_config]` type",
53            ));
54        };
55
56        let watched = self
57            .with_spec(|spec| Ok(crate::watch::Watched::from_spec(spec)))
58            .map_err(|error| {
59                std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string())
60            })?;
61
62        let reloader = self.clone();
63        let name = watch_name::<T>(&self.key);
64
65        let handle = crate::watch::spawn_with(
66            std::any::TypeId::of::<T>(),
67            name,
68            watched,
69            debounce,
70            mode,
71            move || {
72                // `load` already validates, so a refused configuration keeps
73                // the previous snapshot exactly like a parse failure.
74                let value = reloader.load()?;
75                install(value);
76                reloader.write_cache();
77
78                Ok(None)
79            },
80        )?;
81
82        if let Some(register) = self.register {
83            register(self);
84        }
85
86        Ok(handle)
87    }
88}
89
90/// The registry wants a `&'static str`; leaking one per `watch()` call
91/// would grow with every stop/start cycle, so the leak is memoized: one
92/// name per type, ever.
93#[cfg(feature = "watch")]
94fn watch_name<T: 'static>(key: &str) -> &'static str {
95    use std::collections::HashMap;
96    use std::sync::{Mutex, OnceLock};
97
98    static NAMES: OnceLock<Mutex<HashMap<std::any::TypeId, &'static str>>> = OnceLock::new();
99
100    let mut names = NAMES
101        .get_or_init(|| Mutex::new(HashMap::new()))
102        .lock()
103        .unwrap_or_else(std::sync::PoisonError::into_inner);
104
105    names
106        .entry(std::any::TypeId::of::<T>())
107        .or_insert_with(|| Box::leak(format!("builder:{key}").into_boxed_str()))
108}