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}