use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use wasm_bindgen::JsValue;
use crate::error::LibfwError;
use crate::js::Callbacks;
#[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 Semaphore {
inner: Rc<SemaphoreInner>,
}
#[derive(Debug)]
struct SemaphoreInner {
max: usize,
available: Cell<usize>,
waiters: RefCell<VecDeque<Waker>>,
}
impl Semaphore {
pub fn new(max: usize) -> Self {
let max = max.max(1);
Semaphore {
inner: Rc::new(SemaphoreInner {
max,
available: Cell::new(max),
waiters: RefCell::new(VecDeque::new()),
}),
}
}
pub fn reset(&self) {
self.inner.available.set(self.inner.max);
self.inner.waiters.borrow_mut().clear();
}
pub fn acquire(&self) -> Acquire<'_> {
Acquire { sem: self }
}
fn try_acquire(&self) -> bool {
let avail = self.inner.available.get();
if avail > 0 {
self.inner.available.set(avail - 1);
true
} else {
false
}
}
fn release(&self) {
let avail = self.inner.available.get();
self.inner.available.set((avail + 1).min(self.inner.max));
let waiters = std::mem::take(&mut *self.inner.waiters.borrow_mut());
for w in waiters {
w.wake();
}
}
}
pub struct Acquire<'a> {
sem: &'a Semaphore,
}
impl Future for Acquire<'_> {
type Output = Permit;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Permit> {
if self.sem.try_acquire() {
return Poll::Ready(Permit {
sem: self.sem.clone(),
});
}
self.sem.inner.waiters.borrow_mut().push_back(cx.waker().clone());
Poll::Pending
}
}
pub struct Permit {
sem: Semaphore,
}
impl Drop for Permit {
fn drop(&mut self) {
self.sem.release();
}
}
#[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>>,
last_reported: Rc<Cell<u64>>,
semaphore: Semaphore,
}
impl Default for TaskControl {
fn default() -> Self {
TaskControl::new()
}
}
impl TaskControl {
pub fn new() -> Self {
TaskControl::with_max_parallel(libfw_core::DEFAULT_CONCURRENCY)
}
pub fn with_max_parallel(max_parallel: usize) -> 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)),
last_reported: Rc::new(Cell::new(0)),
semaphore: Semaphore::new(max_parallel),
}
}
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);
self.last_reported.set(0);
self.semaphore.reset();
}
pub fn semaphore(&self) -> &Semaphore {
&self.semaphore
}
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));
}
#[allow(dead_code)]
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)
}
}
pub fn report_progress_if(&self, callbacks: &Callbacks) -> Result<(), LibfwError> {
let total = self.total_bytes.get();
let done = self.done_bytes.get();
if total == 0 {
let last = self.last_reported.get();
if done != last {
self.last_reported.set(done);
return callbacks.on_progress(done, 0);
}
return Ok(());
}
let reported = done.min(total);
let last = self.last_reported.get().min(total);
if reported != last {
self.last_reported.set(reported);
return callbacks.on_progress(reported, total);
}
Ok(())
}
}
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::*;
use std::pin::pin;
use std::task::{Context, RawWaker, RawWakerVTable, Waker};
fn noop_waker() -> Waker {
fn noop(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
static VTABLE: RawWakerVTable =
RawWakerVTable::new(clone, noop, noop, noop);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}
#[test]
fn semaphore_blocks_when_exhausted_and_recovers() {
let sem = Semaphore::new(2);
let p1 = futures::executor::block_on(sem.acquire());
let p2 = futures::executor::block_on(sem.acquire());
let mut fut = pin!(sem.acquire());
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(fut.as_mut().poll(&mut cx).is_pending());
drop(p1);
assert!(fut.as_mut().poll(&mut cx).is_ready());
drop(p2);
sem.reset();
let _ = futures::executor::block_on(sem.acquire());
let _ = futures::executor::block_on(sem.acquire());
}
#[test]
fn semaphore_clones_share_the_pool() {
let a = Semaphore::new(1);
let b = a.clone();
let p = futures::executor::block_on(a.acquire());
let mut fut = pin!(b.acquire());
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
assert!(fut.as_mut().poll(&mut cx).is_pending());
drop(p);
assert!(fut.as_mut().poll(&mut cx).is_ready());
}
#[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");
}
}