use std::fs;
use std::sync::{Condvar, Mutex, MutexGuard, PoisonError};
use std::thread;
use std::time::{Duration, SystemTime};
use svugy::{InteractiveSvg, RenderSvg};
use crate::failure::Failure;
use crate::input::{LoadedInput, ModelInput};
const POLL_INTERVAL: Duration = Duration::from_millis(250);
pub struct LiveDiagram {
state: Mutex<DiagramState>,
published: Condvar,
}
struct DiagramState {
svg: String,
svg_generation: u64,
diagnostics: Option<String>,
generation: u64,
}
pub struct Subscription<'a> {
live_diagram: &'a LiveDiagram,
seen_generation: Option<u64>,
}
pub struct LiveUpdate {
pub reloaded_generation: Option<u64>,
pub diagnostics: Option<String>,
}
pub struct FileWatcher {
input: ModelInput,
last_modified: Option<SystemTime>,
}
impl LiveDiagram {
pub fn new(svg: String) -> Self {
let state = DiagramState {
svg,
svg_generation: 0,
diagnostics: None,
generation: 0,
};
Self {
state: Mutex::new(state),
published: Condvar::new(),
}
}
pub fn publish_svg(&self, svg: String) {
let mut state = self.lock_state();
state.generation += 1;
state.svg_generation = state.generation;
state.svg = svg;
state.diagnostics = None;
self.published.notify_all();
}
pub fn publish_diagnostics(&self, diagnostics: String) {
let mut state = self.lock_state();
state.generation += 1;
state.diagnostics = Some(diagnostics);
self.published.notify_all();
}
pub fn subscribe(&self) -> Subscription<'_> {
Subscription {
live_diagram: self,
seen_generation: None,
}
}
pub fn svg(&self) -> String {
let state = self.lock_state();
state.svg.clone()
}
fn lock_state(&self) -> MutexGuard<'_, DiagramState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl Subscription<'_> {
pub fn next_update(&mut self) -> LiveUpdate {
let live_diagram = self.live_diagram;
let seen_generation = self.seen_generation;
let state = live_diagram.lock_state();
let state = live_diagram
.published
.wait_while(state, |state| Some(state.generation) == seen_generation)
.unwrap_or_else(PoisonError::into_inner);
let is_svg_unseen = seen_generation.is_none_or(|seen| state.svg_generation > seen);
let reloaded_generation = is_svg_unseen.then_some(state.svg_generation);
let diagnostics = state.diagnostics.clone();
self.seen_generation = Some(state.generation);
LiveUpdate {
reloaded_generation,
diagnostics,
}
}
}
impl FileWatcher {
pub fn new(input: ModelInput) -> Self {
let last_modified = Self::modification_time_of(&input);
Self {
input,
last_modified,
}
}
pub fn watch_forever(mut self, live_diagram: &LiveDiagram) -> ! {
loop {
thread::sleep(POLL_INTERVAL);
let modified = Self::modification_time_of(&self.input);
let is_unchanged = modified == self.last_modified;
if is_unchanged {
continue;
}
self.last_modified = modified;
self.rerender_into(live_diagram);
}
}
fn rerender_into(&self, live_diagram: &LiveDiagram) {
let loaded = self.input.load().inspect_err(Failure::eprint);
let Ok(LoadedInput { model, report }) = loaded else {
return;
};
report.eprint_diagnostics();
let Some(model) = model else {
let diagnostics = report.to_plain_text();
live_diagram.publish_diagnostics(diagnostics);
return;
};
let svg = InteractiveSvg::render(&model);
live_diagram.publish_svg(svg);
}
fn modification_time_of(input: &ModelInput) -> Option<SystemTime> {
let input_path = input.path();
let metadata = fs::metadata(input_path).ok()?;
metadata.modified().ok()
}
}
#[cfg(test)]
mod tests {
use std::thread;
use super::LiveDiagram;
#[test]
fn viewer_page_gets_the_whole_state_on_connect_then_only_what_it_missed() {
let live_diagram = LiveDiagram::new("<svg>v0</svg>".to_owned());
let mut viewer_page = live_diagram.subscribe();
let on_connect = viewer_page.next_update();
assert_eq!(on_connect.reloaded_generation, Some(0));
live_diagram.publish_svg("<svg>v1</svg>".to_owned());
live_diagram.publish_diagnostics("no component `nowhere`".to_owned());
let after_good_then_broken_save = viewer_page.next_update();
assert_eq!(after_good_then_broken_save.reloaded_generation, Some(1));
let missed_diagnostics = after_good_then_broken_save.diagnostics.as_deref();
assert_eq!(missed_diagnostics, Some("no component `nowhere`"));
let served_svg = live_diagram.svg();
assert_eq!(
served_svg, "<svg>v1</svg>",
"a broken save keeps the last good SVG served"
);
let after_fixing_save = thread::scope(|scope| {
let waiting_page = scope.spawn(|| viewer_page.next_update());
live_diagram.publish_svg("<svg>v3</svg>".to_owned());
waiting_page
.join()
.expect("the waiting page wakes on the publish")
});
assert_eq!(after_fixing_save.reloaded_generation, Some(3));
assert_eq!(after_fixing_save.diagnostics, None);
}
}