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}