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 handle = self.watch_as(
48            crate::watch::WatchKey::Type(std::any::TypeId::of::<T>()),
49            watch_name::<T>(&self.key),
50            debounce,
51            mode,
52        )?;
53
54        if let Some(register) = self.register {
55            register(self);
56        }
57
58        Ok(handle)
59    }
60
61    /// The watch body, with the registry identity chosen by the caller:
62    /// the type's `TypeId` here, an instance number from
63    /// [`Dynamic`](crate::Dynamic). One copy of the reload closure, so the
64    /// two surfaces cannot drift on what a reload does.
65    pub(crate) fn watch_as(
66        &self,
67        key: crate::watch::WatchKey,
68        name: &'static str,
69        debounce: core::time::Duration,
70        mode: crate::watch::WatchMode,
71    ) -> std::io::Result<crate::watch::WatchHandle> {
72        let Some(install) = self.install.clone() else {
73            return Err(std::io::Error::new(
74                std::io::ErrorKind::InvalidInput,
75                "this builder is tied to no config type, so a reload would \
76                 have nowhere to install; start from the generated \
77                 `builder()` on a `#[dynamic_config]` type, or wrap this \
78                 builder in a `Dynamic`",
79            ));
80        };
81
82        let watched = self
83            .with_spec(|spec| Ok(crate::watch::Watched::from_spec(spec)))
84            .map_err(|error| {
85                std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string())
86            })?;
87
88        let reloader = self.clone();
89
90        crate::watch::spawn_with(key, name, watched, debounce, mode, move || {
91            // `load` already validates, so a refused configuration keeps
92            // the previous snapshot exactly like a parse failure.
93            let value = reloader.load()?;
94            install.install(value);
95            reloader.write_cache();
96
97            Ok(None)
98        })
99    }
100}
101
102/// The registry wants a `&'static str`; leaking one per `watch()` call
103/// would grow with every stop/start cycle, so the leak is memoized: one
104/// name per type, ever.
105#[cfg(feature = "watch")]
106fn watch_name<T: 'static>(key: &str) -> &'static str {
107    use std::collections::HashMap;
108    use std::sync::{Mutex, OnceLock};
109
110    static NAMES: OnceLock<Mutex<HashMap<std::any::TypeId, &'static str>>> = OnceLock::new();
111
112    let mut names = NAMES
113        .get_or_init(|| Mutex::new(HashMap::new()))
114        .lock()
115        .unwrap_or_else(std::sync::PoisonError::into_inner);
116
117    names
118        .entry(std::any::TypeId::of::<T>())
119        .or_insert_with(|| Box::leak(format!("builder:{key}").into_boxed_str()))
120}