228 lines
6.9 KiB
Rust
228 lines
6.9 KiB
Rust
//! Footer status derivation — a pure function that every client (desktop,
|
|
//! watch, phone) uses to decide which one-line indicator to show in its
|
|
//! status bar. No UI framework dependency.
|
|
//!
|
|
//! The truth-table is deliberately tied to the **worker state**, not the
|
|
//! queue depth: if the worker is sitting in `sleep(backoff)` after a
|
|
//! transient failure, the queue still has items but the worker is `Idle`
|
|
//! → we show `Offline` / `Online` based on `last_success_age` vs.
|
|
//! `last_failure_age`, not a misleading "Uploading…".
|
|
|
|
use std::time::Duration;
|
|
|
|
use crate::server_sync::WorkerState;
|
|
|
|
/// Derived observable for the client's status line.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum FooterStatus {
|
|
/// ffmpeg (or platform-equivalent) is flushing the last recording.
|
|
Finalizing,
|
|
/// Worker is uploading. `count` = pending-dir depth.
|
|
Uploading { count: usize },
|
|
/// Worker is polling `/api/oneliners`.
|
|
Synchronizing,
|
|
/// Never got a successful request yet, and never saw a failure either —
|
|
/// typical cold start with no network attempt so far.
|
|
NeverConnected,
|
|
/// Last event on record was a failure, or `last_success` is older
|
|
/// than the offline threshold.
|
|
Offline,
|
|
/// `last_success` is fresh enough, and no failure has overtaken it.
|
|
Online,
|
|
}
|
|
|
|
/// Decide which footer status to show. Pure function — no side effects,
|
|
/// fully table-testable.
|
|
///
|
|
/// Decision rules for the `Idle` branch:
|
|
/// - A failure younger than the last success (or a failure with no
|
|
/// prior success at all) wins → `Offline`.
|
|
/// - Otherwise fall back to success-age vs. threshold (classic staleness).
|
|
///
|
|
/// `last_*_age` arguments are `Option<Duration>` = "seconds since that
|
|
/// event, or `None` if it never happened". Smaller Duration = more recent.
|
|
pub fn pick_footer_status(
|
|
finalizing: bool,
|
|
worker_state: WorkerState,
|
|
queue_len: usize,
|
|
last_success_age: Option<Duration>,
|
|
last_failure_age: Option<Duration>,
|
|
offline_threshold: Duration,
|
|
) -> FooterStatus {
|
|
if finalizing {
|
|
return FooterStatus::Finalizing;
|
|
}
|
|
match worker_state {
|
|
WorkerState::Uploading => FooterStatus::Uploading {
|
|
count: queue_len.max(1),
|
|
},
|
|
WorkerState::Polling => FooterStatus::Synchronizing,
|
|
WorkerState::Idle => {
|
|
let failure_more_recent = match (last_success_age, last_failure_age) {
|
|
(Some(s), Some(f)) => f < s, // smaller age = younger
|
|
(None, Some(_)) => true, // only a failure on record
|
|
_ => false,
|
|
};
|
|
if failure_more_recent {
|
|
return FooterStatus::Offline;
|
|
}
|
|
match last_success_age {
|
|
None => FooterStatus::NeverConnected,
|
|
Some(age) if age > offline_threshold => FooterStatus::Offline,
|
|
Some(_) => FooterStatus::Online,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const THRESHOLD: Duration = Duration::from_secs(30);
|
|
|
|
/// Convenience: most tests don't care about the failure field.
|
|
fn status(
|
|
finalizing: bool,
|
|
worker_state: WorkerState,
|
|
queue_len: usize,
|
|
last_success_age: Option<Duration>,
|
|
) -> FooterStatus {
|
|
pick_footer_status(
|
|
finalizing,
|
|
worker_state,
|
|
queue_len,
|
|
last_success_age,
|
|
None,
|
|
THRESHOLD,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn finalizing_wins_over_everything() {
|
|
let s = pick_footer_status(
|
|
true,
|
|
WorkerState::Uploading,
|
|
5,
|
|
Some(Duration::from_millis(1)),
|
|
Some(Duration::from_millis(1)),
|
|
THRESHOLD,
|
|
);
|
|
assert_eq!(s, FooterStatus::Finalizing);
|
|
}
|
|
|
|
#[test]
|
|
fn uploading_when_queue_non_empty() {
|
|
let s = status(
|
|
false,
|
|
WorkerState::Uploading,
|
|
3,
|
|
Some(Duration::from_millis(1)),
|
|
);
|
|
assert_eq!(s, FooterStatus::Uploading { count: 3 });
|
|
}
|
|
|
|
#[test]
|
|
fn uploading_reports_at_least_one_even_if_scan_raced() {
|
|
let s = status(
|
|
false,
|
|
WorkerState::Uploading,
|
|
0,
|
|
Some(Duration::from_millis(1)),
|
|
);
|
|
assert_eq!(s, FooterStatus::Uploading { count: 1 });
|
|
}
|
|
|
|
#[test]
|
|
fn synchronizing_when_polling_and_no_queue() {
|
|
let s = status(
|
|
false,
|
|
WorkerState::Polling,
|
|
0,
|
|
Some(Duration::from_millis(1)),
|
|
);
|
|
assert_eq!(s, FooterStatus::Synchronizing);
|
|
}
|
|
|
|
#[test]
|
|
fn never_connected_without_last_success() {
|
|
let s = status(false, WorkerState::Idle, 0, None);
|
|
assert_eq!(s, FooterStatus::NeverConnected);
|
|
}
|
|
|
|
#[test]
|
|
fn offline_when_last_success_is_stale() {
|
|
let s = status(false, WorkerState::Idle, 0, Some(Duration::from_secs(120)));
|
|
assert_eq!(s, FooterStatus::Offline);
|
|
}
|
|
|
|
#[test]
|
|
fn online_when_last_success_is_fresh() {
|
|
let s = status(false, WorkerState::Idle, 0, Some(Duration::from_secs(5)));
|
|
assert_eq!(s, FooterStatus::Online);
|
|
}
|
|
|
|
#[test]
|
|
fn offline_threshold_is_inclusive_of_equal_age_as_online() {
|
|
let s = status(false, WorkerState::Idle, 0, Some(THRESHOLD));
|
|
assert_eq!(s, FooterStatus::Online);
|
|
}
|
|
|
|
#[test]
|
|
fn offline_while_queue_has_items_but_worker_backing_off() {
|
|
let s = status(false, WorkerState::Idle, 3, Some(Duration::from_secs(120)));
|
|
assert_eq!(s, FooterStatus::Offline);
|
|
}
|
|
|
|
#[test]
|
|
fn online_while_queue_has_items_but_worker_idle_and_recent_success() {
|
|
let s = status(false, WorkerState::Idle, 3, Some(Duration::from_secs(2)));
|
|
assert_eq!(s, FooterStatus::Online);
|
|
}
|
|
|
|
#[test]
|
|
fn never_connected_while_queue_has_items_and_no_success_ever() {
|
|
let s = status(false, WorkerState::Idle, 3, None);
|
|
assert_eq!(s, FooterStatus::NeverConnected);
|
|
}
|
|
|
|
#[test]
|
|
fn failure_younger_than_success_forces_offline() {
|
|
let s = pick_footer_status(
|
|
false,
|
|
WorkerState::Idle,
|
|
1,
|
|
Some(Duration::from_secs(10)),
|
|
Some(Duration::from_secs(1)),
|
|
THRESHOLD,
|
|
);
|
|
assert_eq!(s, FooterStatus::Offline);
|
|
}
|
|
|
|
#[test]
|
|
fn success_younger_than_failure_wins_back_online() {
|
|
let s = pick_footer_status(
|
|
false,
|
|
WorkerState::Idle,
|
|
0,
|
|
Some(Duration::from_secs(1)),
|
|
Some(Duration::from_secs(10)),
|
|
THRESHOLD,
|
|
);
|
|
assert_eq!(s, FooterStatus::Online);
|
|
}
|
|
|
|
#[test]
|
|
fn only_failure_no_success_is_offline() {
|
|
let s = pick_footer_status(
|
|
false,
|
|
WorkerState::Idle,
|
|
0,
|
|
None,
|
|
Some(Duration::from_secs(5)),
|
|
THRESHOLD,
|
|
);
|
|
assert_eq!(s, FooterStatus::Offline);
|
|
}
|
|
}
|