feat(app): shared offline-mode engine with connectivity actor and grace gate
New qbz_app::offline_mode (frontend-agnostic, ADR-006): three-state mode machine (Online / RealOffline session-scoped / InducedOffline persisted), Tauri-compatible offline_settings.db store with the issue #279 stream_first_track snapshot, and a layered connectivity actor (default-route signal on Linux, audio liveness window, IP-literal + vendor-diverse strict probes, asymmetric hysteresis with confirmation burst, captive-portal and suspend/resume handling). qbz-qobuz gains the process-wide offline_gate and ApiError::OfflineMode (the single Qobuz choke point; HTTP funnel wiring lands next). SubscriptionStateStore gains offline_playback_allowed: binary 30-day-grace playback gate for the offline cache, never a preview-degradation path. cc
This commit is contained in:
10 files changed
+1416
No files matched your search
@@ -31,6 +31,9 @@ pub enum ApiError {
|
||||
#[error("Track {0} is no longer available on Qobuz")]
|
||||
TrackUnavailable(u64),
|
||||
|
||||
#[error("Offline mode is active - Qobuz services are disabled")]
|
||||
OfflineMode,
|
||||
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(#[from] reqwest::Error),
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ pub mod cmaf;
|
||||
pub mod endpoints;
|
||||
pub mod error;
|
||||
pub mod link_resolver;
|
||||
pub mod offline_gate;
|
||||
pub mod performers;
|
||||
pub mod retry;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Process-wide offline gate — the single Qobuz choke point.
|
||||
//!
|
||||
//! Offline MODE (induced from Settings or detected loss of connectivity)
|
||||
//! means ZERO Qobuz services: while the gate is closed, every HTTP request
|
||||
//! the client would issue fails fast with [`crate::error::ApiError::OfflineMode`]
|
||||
//! instead of timing out against the network. The owner of the flag is the
|
||||
//! shared offline-mode engine in `qbz-app`; frontends never set it directly.
|
||||
//!
|
||||
//! The flag is process-global on purpose: there is exactly one Qobuz client
|
||||
//! per process, the gate must hold across re-logins, and a per-instance flag
|
||||
//! would silently reset when the client is rebuilt.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static OFFLINE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Close (true) or open (false) the gate. Called by the offline-mode engine
|
||||
/// on every mode transition.
|
||||
pub fn set_offline(offline: bool) {
|
||||
let was = OFFLINE.swap(offline, Ordering::Relaxed);
|
||||
if was != offline {
|
||||
log::info!(
|
||||
"[OfflineGate] Qobuz API gate {}",
|
||||
if offline { "CLOSED (offline mode)" } else { "OPEN" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the gate is currently closed (offline mode active).
|
||||
pub fn is_offline() -> bool {
|
||||
OFFLINE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gate_toggles() {
|
||||
set_offline(false);
|
||||
assert!(!is_offline());
|
||||
set_offline(true);
|
||||
assert!(is_offline());
|
||||
set_offline(false);
|
||||
assert!(!is_offline());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user