Skip to main content

limnifs_write/
progress.rs

1//! Progress reporting seam.
2//!
3//! OCP: the pipeline emits events through this module and never
4//! knows about terminals, rates, or formatting; consumers install
5//! any [`ProgressSink`] without the writer changing. MECE: progress
6//! lives here and nowhere else. The registry is process-wide
7//! (matching the writer's existing thread-local cache precedent)
8//! and optional — absent a sink, `emit_file` is a single relaxed
9//! load on the fast path.
10
11use std::path::Path;
12use std::sync::Arc;
13
14/// Receives progress events from the write pipeline.
15pub trait ProgressSink: Send + Sync {
16    /// A file was accepted for packing (after stat, before
17    /// compression completes).
18    fn on_file(&self, path: &Path, bytes: u64);
19}
20
21impl<F> ProgressSink for F
22where
23    F: Fn(&Path, u64) + Send + Sync,
24{
25    fn on_file(&self, path: &Path, bytes: u64) {
26        self(path, bytes)
27    }
28}
29
30static SINK: std::sync::RwLock<Option<Arc<dyn ProgressSink>>> = std::sync::RwLock::new(None);
31
32/// Install the process-wide sink (replaces any previous one).
33pub fn set_sink(sink: Arc<dyn ProgressSink>) {
34    *SINK.write().expect("progress sink lock poisoned") = Some(sink);
35}
36
37/// Remove the process-wide sink.
38pub fn clear_sink() {
39    *SINK.write().expect("progress sink lock poisoned") = None;
40}
41
42/// Emit a file event; a no-op when no sink is installed.
43pub fn emit_file(path: &Path, bytes: u64) {
44    // Clone the Arc out of the lock so sink code never runs under
45    // it (an emitting sink must not deadlock a concurrent set_sink).
46    let sink = SINK.read().expect("progress sink lock poisoned").clone();
47    if let Some(sink) = sink {
48        sink.on_file(path, bytes);
49    }
50}