Skip to main content

mini_build/
change.rs

1//! What changed, and how that gets reported to whoever is listening.
2//!
3//! These types were `mini_static::reload::ChangeType` and
4//! `mini_static::watcher::{ChangeEvent, Broadcaster}` when the pipeline lived inside the
5//! server. They are reproduced here rather than imported because the server keeps its
6//! own copies: `ChangeType` is the payload of its live-reload SSE frames, and the
7//! broadcaster feeds its connected browsers. Sharing them would make a build tool depend
8//! on an HTTP server, which is the coupling this crate exists to remove — and the two
9//! will drift apart, since this one has no reason to care what a browser does with a
10//! stylesheet.
11
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex};
14
15use std::sync::mpsc::{channel, Receiver, Sender};
16
17/// The kind of file that changed, classified by extension.
18///
19/// Classification drives which pipeline owns a file: `Css` goes to the CSS tool,
20/// `Script` to the JS tool, and everything else is copied rather than transformed.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum ChangeType {
23    /// A CSS stylesheet changed.
24    Css,
25    /// A JavaScript module changed.
26    Script,
27    /// An HTML page changed.
28    Html,
29    /// Some other file changed.
30    Other,
31}
32
33impl ChangeType {
34    /// Determine the change type from a file path's extension.
35    pub fn from_path(path: &Path) -> Self {
36        match path.extension().and_then(|e| e.to_str()) {
37            Some("css") => ChangeType::Css,
38            Some("js" | "mjs") => ChangeType::Script,
39            Some("html" | "htm") => ChangeType::Html,
40            _ => ChangeType::Other,
41        }
42    }
43
44    /// The stable string name for this change type.
45    pub fn as_str(&self) -> &'static str {
46        match self {
47            ChangeType::Css => "css",
48            ChangeType::Script => "script",
49            ChangeType::Html => "html",
50            ChangeType::Other => "other",
51        }
52    }
53}
54
55/// A file that was added, modified, or removed.
56#[derive(Clone, Debug)]
57pub struct ChangeEvent {
58    /// The path to the file that changed.
59    pub path: PathBuf,
60    /// What kind of file it is.
61    pub change_type: ChangeType,
62}
63
64/// Fans change events out to every subscriber.
65///
66/// A build reports what it produced through this, so a caller — a watch loop, a dev
67/// server wanting to reload a browser — can react without polling the output directory
68/// itself. Subscribers whose channel has closed are dropped on the next broadcast.
69#[derive(Clone, Default)]
70pub struct Broadcaster {
71    senders: Arc<Mutex<Vec<Sender<ChangeEvent>>>>,
72}
73
74impl Broadcaster {
75    /// Create a broadcaster with no subscribers.
76    pub fn new() -> Self {
77        Broadcaster::default()
78    }
79
80    /// Send `event` to every active subscriber, dropping any that have gone away.
81    pub fn broadcast(&self, event: ChangeEvent) {
82        let mut senders = self.senders.lock().unwrap();
83        senders.retain(|sender| sender.send(event.clone()).is_ok());
84    }
85
86    /// Subscribe to change events.
87    pub fn subscribe(&self) -> Receiver<ChangeEvent> {
88        let (tx, rx) = channel();
89        self.senders.lock().unwrap().push(tx);
90        rx
91    }
92
93    /// How many subscribers are currently attached.
94    #[cfg(test)]
95    pub fn subscriber_count(&self) -> usize {
96        self.senders.lock().unwrap().len()
97    }
98}
99
100#[cfg(test)]
101#[path = "../tests/unit/change.rs"]
102mod tests;