use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::JsValue;
use crate::error::LibfwError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskState {
Idle,
Downloading,
Uploading,
Paused,
Completed,
Failed,
}
impl TaskState {
pub fn as_str(self) -> &'static str {
match self {
TaskState::Idle => "idle",
TaskState::Downloading => "downloading",
TaskState::Uploading => "uploading",
TaskState::Paused => "paused",
TaskState::Completed => "completed",
TaskState::Failed => "failed",
}
}
}
#[derive(Debug, Clone)]
pub struct TaskControl {
state: Rc<Cell<TaskState>>,
active: Rc<Cell<TaskState>>,
cancelled: Rc<Cell<bool>>,
done_bytes: Rc<Cell<u64>>,
total_bytes: Rc<Cell<u64>>,
}
impl Default for TaskControl {
fn default() -> Self {
TaskControl::new()
}
}
impl TaskControl {
pub fn new() -> Self {
TaskControl {
state: Rc::new(Cell::new(TaskState::Idle)),
active: Rc::new(Cell::new(TaskState::Idle)),
cancelled: Rc::new(Cell::new(false)),
done_bytes: Rc::new(Cell::new(0)),
total_bytes: Rc::new(Cell::new(0)),
}
}
pub fn reset(&self) {
self.state.set(TaskState::Idle);
self.active.set(TaskState::Idle);
self.cancelled.set(false);
self.done_bytes.set(0);
self.total_bytes.set(0);
}
pub fn state(&self) -> TaskState {
self.state.get()
}
pub fn begin(&self, s: TaskState) {
self.active.set(s);
self.state.set(s);
}
pub fn complete(&self) {
self.state.set(TaskState::Completed);
}
pub fn fail(&self) {
self.state.set(TaskState::Failed);
}
pub fn pause(&self) {
if matches!(
self.state.get(),
TaskState::Downloading | TaskState::Uploading
) {
self.active.set(self.state.get());
self.state.set(TaskState::Paused);
}
}
pub fn resume(&self) {
if self.state.get() == TaskState::Paused {
self.state.set(self.active.get());
}
}
pub fn cancel(&self) {
self.cancelled.set(true);
if !matches!(self.state.get(), TaskState::Completed | TaskState::Failed) {
self.state.set(TaskState::Failed);
}
}
#[allow(dead_code)] pub fn is_cancelled(&self) -> bool {
self.cancelled.get()
}
pub fn check(&self) -> Result<(), LibfwError> {
if self.cancelled.get() {
Err(LibfwError::Cancelled)
} else {
Ok(())
}
}
pub async fn wait_ready(&self) -> Result<(), LibfwError> {
loop {
self.check()?;
if self.state.get() != TaskState::Paused {
return Ok(());
}
yield_to_event_loop().await;
}
}
pub fn add_progress(&self, bytes: u64) {
self.done_bytes.set(self.done_bytes.get().saturating_add(bytes));
}
pub fn subtract_progress(&self, bytes: u64) {
self.done_bytes.set(self.done_bytes.get().saturating_sub(bytes));
}
pub fn done_bytes(&self) -> u64 {
self.done_bytes.get()
}
pub fn total_bytes(&self) -> u64 {
self.total_bytes.get()
}
pub fn set_total(&self, bytes: u64) {
self.total_bytes.set(bytes);
}
pub fn progress(&self) -> f64 {
let total = self.total_bytes.get();
if total == 0 {
0.0
} else {
(self.done_bytes.get() as f64 / total as f64).clamp(0.0, 1.0)
}
}
}
async fn yield_to_event_loop() {
let promise = js_sys::Promise::resolve(&JsValue::UNDEFINED);
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idle_state_machine() {
let c = TaskControl::new();
assert_eq!(c.state(), TaskState::Idle);
c.begin(TaskState::Downloading);
assert_eq!(c.state(), TaskState::Downloading);
c.pause();
assert_eq!(c.state(), TaskState::Paused);
c.resume();
assert_eq!(c.state(), TaskState::Downloading);
c.complete();
assert_eq!(c.state(), TaskState::Completed);
}
#[test]
fn pause_restores_active_state() {
let c = TaskControl::new();
c.begin(TaskState::Uploading);
c.pause();
assert_eq!(c.state(), TaskState::Paused);
c.resume();
assert_eq!(c.state(), TaskState::Uploading);
}
#[test]
fn cancel_marks_failed_and_checks() {
let c = TaskControl::new();
c.begin(TaskState::Downloading);
c.cancel();
assert!(c.is_cancelled());
assert!(matches!(c.check(), Err(LibfwError::Cancelled)));
assert_eq!(c.state(), TaskState::Failed);
}
#[test]
fn progress_is_bounded() {
let c = TaskControl::new();
c.set_total(100);
c.add_progress(25);
assert_eq!(c.progress(), 0.25);
c.add_progress(200);
assert_eq!(c.progress(), 1.0);
}
#[test]
fn clones_share_counters_and_flags() {
let c = TaskControl::new();
let task = c.clone();
c.set_total(100);
task.add_progress(40);
assert_eq!(c.done_bytes(), 40);
assert_eq!(c.progress(), 0.4);
c.begin(TaskState::Downloading);
c.pause();
assert_eq!(task.state(), TaskState::Paused);
c.resume();
assert_eq!(task.state(), TaskState::Downloading);
c.cancel();
assert!(matches!(task.check(), Err(LibfwError::Cancelled)));
assert!(task.is_cancelled());
}
#[test]
fn state_names_are_stable() {
assert_eq!(TaskState::Idle.as_str(), "idle");
assert_eq!(TaskState::Paused.as_str(), "paused");
assert_eq!(TaskState::Completed.as_str(), "completed");
assert_eq!(TaskState::Failed.as_str(), "failed");
}
}