Skip to main content

kithara_stream/
error.rs

1#![forbid(unsafe_code)]
2
3use std::{error::Error as StdError, io};
4
5use thiserror::Error;
6
7/// Unified source error, surfaced by every `Source` impl.
8///
9/// Concrete adapters (file, HLS, mocks) flatten their crate-local error
10/// types into one of these variants at the `Source` impl boundary so that
11/// `Source` itself stays object-safe (no associated `Error` type).
12///
13/// Adapter-specific failures that don't map to a generic variant flow
14/// through [`SourceError::Other`], which boxes the original typed error
15/// for `downcast` access.
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum SourceError {
19    #[error("network: {0}")]
20    Net(#[from] kithara_net::NetError),
21
22    #[error("storage: {0}")]
23    Storage(#[from] kithara_storage::StorageError),
24
25    #[error("invalid path: {0}")]
26    InvalidPath(String),
27
28    #[error("playlist parse: {0}")]
29    PlaylistParse(String),
30
31    #[error("variant not found: {0}")]
32    VariantNotFound(String),
33
34    #[error("segment not found: {0}")]
35    SegmentNotFound(String),
36
37    #[error("key processing: {0}")]
38    KeyProcessing(String),
39
40    #[error("invalid url: {0}")]
41    InvalidUrl(String),
42
43    #[error("cancelled")]
44    Cancelled,
45
46    #[error("timeout: {0}")]
47    Timeout(String),
48
49    /// Cooperative `wait_range` budget exceeded — caller passed
50    /// `Some(timeout)` and the source did not become ready within it.
51    /// Hot-path classifier for the audio worker's retry loop; diagnostic
52    /// detail is intentionally absent so emission allocates nothing.
53    #[error("wait_range budget exceeded")]
54    WaitBudgetExceeded,
55
56    /// `format_change_segment_range` not applicable in the current
57    /// state. Reasons (all expected steady states, not bugs):
58    /// - source has no init-bearing format-change concept (file
59    ///   source — default `Source` trait impl returns this);
60    /// - active HLS variant was activated by same-codec ABR with
61    ///   `served_from > 0`: init bytes live at natural `[0..init_size)`
62    ///   while virtual space starts at `byte_shift`, so init is
63    ///   unreachable via Stream reads. Same-codec post-switch
64    ///   playback continues through `byte_shift`; recovery via
65    ///   init probe is by design not applicable.
66    ///
67    /// Callers must treat this as "no recovery possible at this
68    /// site" (steady-state) — not an error to surface to the user.
69    #[error("format change not applicable to this source kind/state")]
70    FormatChangeNotApplicable,
71
72    #[error("io: {0}")]
73    Io(#[from] io::Error),
74
75    #[error("{0}")]
76    Other(Box<dyn StdError + Send + Sync>),
77}
78
79impl SourceError {
80    /// Wrap an arbitrary error in `Other`.
81    pub fn other<E>(err: E) -> Self
82    where
83        E: StdError + Send + Sync + 'static,
84    {
85        Self::Other(Box::new(err))
86    }
87}
88
89/// Errors produced by `kithara-stream`.
90#[derive(Debug, Error)]
91#[non_exhaustive]
92pub enum StreamError {
93    #[error("source error: {0}")]
94    Source(#[source] SourceError),
95}
96
97impl<E> From<E> for StreamError
98where
99    E: Into<SourceError>,
100{
101    fn from(err: E) -> Self {
102        Self::Source(err.into())
103    }
104}
105
106/// Result type for `kithara-stream`.
107pub type StreamResult<T> = Result<T, StreamError>;
108
109#[cfg(test)]
110mod tests {
111    use std::io::{Error as IoError, ErrorKind};
112
113    use kithara_test_utils::kithara;
114
115    use super::*;
116
117    #[kithara::test]
118    fn test_source_error_display() {
119        let io_err = IoError::new(ErrorKind::NotFound, "file missing");
120        let err = StreamError::Source(SourceError::Io(io_err));
121        assert!(err.to_string().contains("file missing"));
122    }
123
124    #[kithara::test]
125    fn test_stream_error_is_send_sync() {
126        fn assert_send_sync<T: Send + Sync>() {}
127        assert_send_sync::<StreamError>();
128        assert_send_sync::<SourceError>();
129    }
130}