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}