use std::time::Instant;
use tokio::sync::broadcast;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryKind {
Connection,
Data,
}
#[derive(Debug, Clone)]
pub enum TranscriptionEvent {
PreflightStarted { t: Instant },
PreflightCompleted { success: bool, t: Instant },
RequestStarted {
endpoint: String,
t: Instant,
},
ConnectionEstablished { t: Instant },
UploadProgress {
bytes_sent: u64,
total: u64,
t: Instant,
},
UploadComplete { total: u64, t: Instant },
ResponseHeaders { status: u16, t: Instant },
DownloadProgress {
bytes_received: u64,
total: Option<u64>,
t: Instant,
},
ResponseComplete { total: u64, t: Instant },
RequestCompleted { success: bool, t: Instant },
RetryScheduled {
kind: RetryKind,
attempt: u32,
max: u32,
reason: String,
delay: std::time::Duration,
t: Instant,
},
Status { message: String, t: Instant },
PasteStarted { t: Instant },
PasteProgress {
chars_pasted: u64,
total_chars: u64,
t: Instant,
},
PasteCompleted { t: Instant },
Done { t: Instant },
Failed { reason: String, t: Instant },
}
pub trait TelemetrySink: Send + Sync {
fn emit(&self, event: TranscriptionEvent);
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoOpSink;
impl TelemetrySink for NoOpSink {
fn emit(&self, _event: TranscriptionEvent) {}
}
pub struct BroadcastSink {
tx: broadcast::Sender<TranscriptionEvent>,
}
impl BroadcastSink {
pub fn new(capacity: usize) -> Self {
let (tx, _rx) = broadcast::channel(capacity);
Self { tx }
}
pub fn subscribe(&self) -> broadcast::Receiver<TranscriptionEvent> {
self.tx.subscribe()
}
pub fn receiver_count(&self) -> usize {
self.tx.receiver_count()
}
}
impl TelemetrySink for BroadcastSink {
fn emit(&self, event: TranscriptionEvent) {
let _ = self.tx.send(event);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct MockSink {
events: Mutex<Vec<TranscriptionEvent>>,
}
impl MockSink {
fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
}
}
fn recorded(&self) -> Vec<TranscriptionEvent> {
self.events
.lock()
.expect("test: mock sink lock poisoned")
.clone()
}
}
impl TelemetrySink for MockSink {
fn emit(&self, event: TranscriptionEvent) {
self.events
.lock()
.expect("test: mock sink lock poisoned")
.push(event);
}
}
#[test]
fn noop_sink_drops_events_without_panicking() {
let sink = NoOpSink;
sink.emit(TranscriptionEvent::Done { t: Instant::now() });
sink.emit(TranscriptionEvent::Failed {
reason: "test".to_string(),
t: Instant::now(),
});
}
#[test]
fn mock_sink_records_events_in_order() {
let sink = MockSink::new();
sink.emit(TranscriptionEvent::RequestStarted {
endpoint: "https://example.com".to_string(),
t: Instant::now(),
});
sink.emit(TranscriptionEvent::ConnectionEstablished { t: Instant::now() });
let events = sink.recorded();
assert_eq!(events.len(), 2);
assert!(matches!(
events[0],
TranscriptionEvent::RequestStarted { .. }
));
assert!(matches!(
events[1],
TranscriptionEvent::ConnectionEstablished { .. }
));
}
#[test]
fn broadcast_sink_delivers_to_every_active_subscriber() {
let sink = BroadcastSink::new(16);
let mut rx1 = sink.subscribe();
let mut rx2 = sink.subscribe();
assert_eq!(sink.receiver_count(), 2);
sink.emit(TranscriptionEvent::Done { t: Instant::now() });
let e1 = rx1
.try_recv()
.expect("test: rx1 should have received the event");
let e2 = rx2
.try_recv()
.expect("test: rx2 should have received the event");
assert!(matches!(e1, TranscriptionEvent::Done { .. }));
assert!(matches!(e2, TranscriptionEvent::Done { .. }));
}
#[test]
fn broadcast_sink_drops_when_no_subscribers_are_attached() {
let sink = BroadcastSink::new(16);
assert_eq!(sink.receiver_count(), 0);
sink.emit(TranscriptionEvent::Done { t: Instant::now() });
let mut rx = sink.subscribe();
assert!(
rx.try_recv().is_err(),
"late subscribers must not see events emitted before they subscribed"
);
}
#[test]
fn broadcast_sink_is_usable_behind_arc_dyn() {
let sink: Arc<dyn TelemetrySink> = Arc::new(BroadcastSink::new(16));
let sink_a = Arc::clone(&sink);
let sink_b = Arc::clone(&sink);
sink_a.emit(TranscriptionEvent::Done { t: Instant::now() });
sink_b.emit(TranscriptionEvent::Done { t: Instant::now() });
}
#[test]
fn all_transcription_event_variants_are_clone() {
let events = vec![
TranscriptionEvent::RequestStarted {
endpoint: "x".to_string(),
t: Instant::now(),
},
TranscriptionEvent::ConnectionEstablished { t: Instant::now() },
TranscriptionEvent::UploadProgress {
bytes_sent: 100,
total: 1000,
t: Instant::now(),
},
TranscriptionEvent::UploadComplete {
total: 1000,
t: Instant::now(),
},
TranscriptionEvent::ResponseHeaders {
status: 200,
t: Instant::now(),
},
TranscriptionEvent::DownloadProgress {
bytes_received: 50,
total: Some(500),
t: Instant::now(),
},
TranscriptionEvent::ResponseComplete {
total: 500,
t: Instant::now(),
},
TranscriptionEvent::RequestCompleted {
success: true,
t: Instant::now(),
},
TranscriptionEvent::RetryScheduled {
kind: RetryKind::Connection,
attempt: 1,
max: 5,
reason: "timeout".to_string(),
delay: std::time::Duration::ZERO,
t: Instant::now(),
},
TranscriptionEvent::PasteStarted { t: Instant::now() },
TranscriptionEvent::PasteProgress {
chars_pasted: 50,
total_chars: 100,
t: Instant::now(),
},
TranscriptionEvent::PasteCompleted { t: Instant::now() },
TranscriptionEvent::Done { t: Instant::now() },
TranscriptionEvent::Failed {
reason: "x".to_string(),
t: Instant::now(),
},
];
for e in &events {
let _cloned = e.clone();
}
}
#[test]
fn broadcast_sink_keeps_working_after_a_receiver_is_dropped() {
let sink = BroadcastSink::new(16);
let mut rx_keep = sink.subscribe();
{
let _rx_drop = sink.subscribe();
}
assert_eq!(sink.receiver_count(), 1);
sink.emit(TranscriptionEvent::Done { t: Instant::now() });
let evt = rx_keep
.try_recv()
.expect("test: remaining receiver should still deliver");
assert!(matches!(evt, TranscriptionEvent::Done { .. }));
}
}