use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::hooks::use_effect;
#[derive(Clone)]
pub struct TimeoutHandle {
cancelled: Arc<AtomicBool>,
}
impl TimeoutHandle {
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
}
pub fn use_timeout<F>(callback: F, delay_ms: u64) -> TimeoutHandle
where
F: FnOnce() + Send + 'static,
{
let cancelled = Arc::new(AtomicBool::new(false));
let cancelled_for_effect = cancelled.clone();
let callback = Arc::new(Mutex::new(Some(callback)));
use_effect(
move || {
let cancelled = cancelled_for_effect.clone();
let cancelled_for_spawn = cancelled.clone();
let callback = callback.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
if !cancelled_for_spawn.load(Ordering::SeqCst) {
if let Some(cb) = callback.lock().ok().and_then(|mut guard| guard.take()) {
cb();
}
}
});
Some(Box::new(move || {
cancelled.store(true, Ordering::SeqCst);
}) as Box<dyn FnOnce() + Send>)
},
Some(()), );
TimeoutHandle { cancelled }
}
#[derive(Clone)]
pub struct IntervalHandle {
paused: Arc<AtomicBool>,
cancelled: Arc<AtomicBool>,
}
impl IntervalHandle {
pub fn pause(&self) {
self.paused.store(true, Ordering::SeqCst);
}
pub fn resume(&self) {
self.paused.store(false, Ordering::SeqCst);
}
pub fn is_paused(&self) -> bool {
self.paused.load(Ordering::SeqCst)
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
}
pub fn use_interval<F>(callback: F, interval_ms: u64) -> IntervalHandle
where
F: Fn() + Send + Sync + 'static,
{
let paused = Arc::new(AtomicBool::new(false));
let cancelled = Arc::new(AtomicBool::new(false));
let paused_for_effect = paused.clone();
let cancelled_for_effect = cancelled.clone();
let callback = Arc::new(callback);
use_effect(
move || {
let paused = paused_for_effect.clone();
let cancelled = cancelled_for_effect.clone();
let cancelled_for_spawn = cancelled.clone();
let paused_for_spawn = paused.clone();
let callback = callback.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(interval_ms));
loop {
interval.tick().await;
if cancelled_for_spawn.load(Ordering::SeqCst) {
break;
}
if !paused_for_spawn.load(Ordering::SeqCst) {
callback();
}
}
});
Some(Box::new(move || {
cancelled.store(true, Ordering::SeqCst);
}) as Box<dyn FnOnce() + Send>)
},
Some(()), );
IntervalHandle { paused, cancelled }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fiber::FiberId;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
clear_fiber_tree();
}
#[test]
fn test_timeout_handle_cancel() {
let handle = TimeoutHandle {
cancelled: Arc::new(AtomicBool::new(false)),
};
assert!(!handle.is_cancelled());
handle.cancel();
assert!(handle.is_cancelled());
}
#[test]
fn test_interval_handle_pause_resume() {
let handle = IntervalHandle {
paused: Arc::new(AtomicBool::new(false)),
cancelled: Arc::new(AtomicBool::new(false)),
};
assert!(!handle.is_paused());
handle.pause();
assert!(handle.is_paused());
handle.resume();
assert!(!handle.is_paused());
}
#[test]
fn test_interval_handle_cancel() {
let handle = IntervalHandle {
paused: Arc::new(AtomicBool::new(false)),
cancelled: Arc::new(AtomicBool::new(false)),
};
assert!(!handle.is_cancelled());
handle.cancel();
assert!(handle.is_cancelled());
}
#[test]
fn test_use_timeout_returns_handle() {
let _fiber_id = setup_test_fiber();
let handle = use_timeout(|| {}, 1000);
assert!(!handle.is_cancelled());
cleanup_test();
}
#[test]
fn test_use_timeout_can_cancel() {
let _fiber_id = setup_test_fiber();
let handle = use_timeout(|| {}, 1000);
handle.cancel();
assert!(handle.is_cancelled());
cleanup_test();
}
#[test]
fn test_use_interval_returns_handle() {
let _fiber_id = setup_test_fiber();
let handle = use_interval(|| {}, 1000);
assert!(!handle.is_cancelled());
assert!(!handle.is_paused());
handle.cancel();
cleanup_test();
}
#[test]
fn test_use_interval_can_pause_resume() {
let _fiber_id = setup_test_fiber();
let handle = use_interval(|| {}, 1000);
handle.pause();
assert!(handle.is_paused());
handle.resume();
assert!(!handle.is_paused());
handle.cancel();
cleanup_test();
}
#[test]
fn test_use_interval_can_cancel() {
let _fiber_id = setup_test_fiber();
let handle = use_interval(|| {}, 1000);
handle.cancel();
assert!(handle.is_cancelled());
cleanup_test();
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::fiber::FiberId;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
use proptest::prelude::*;
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
clear_fiber_tree();
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_timeout_cancel_idempotent(cancel_count in 1usize..10) {
let _fiber_id = setup_test_fiber();
let handle = use_timeout(|| {}, 10000);
for _ in 0..cancel_count {
handle.cancel();
prop_assert!(handle.is_cancelled());
}
cleanup_test();
}
#[test]
fn prop_timeout_handle_clone_shares_state(_dummy in 0..1i32) {
let _fiber_id = setup_test_fiber();
let handle1 = use_timeout(|| {}, 10000);
let handle2 = handle1.clone();
prop_assert!(!handle1.is_cancelled());
prop_assert!(!handle2.is_cancelled());
handle1.cancel();
prop_assert!(handle1.is_cancelled());
prop_assert!(handle2.is_cancelled());
cleanup_test();
}
#[test]
fn prop_interval_pause_resume_consistent(ops in prop::collection::vec(prop_oneof![Just(true), Just(false)], 1..20)) {
let _fiber_id = setup_test_fiber();
let handle = use_interval(|| {}, 10000);
#[allow(unused_assignments)]
let mut expected_paused = false;
for op in ops {
if op {
handle.pause();
expected_paused = true;
} else {
handle.resume();
expected_paused = false;
}
prop_assert_eq!(handle.is_paused(), expected_paused);
}
handle.cancel();
cleanup_test();
}
#[test]
fn prop_interval_cancel_idempotent(cancel_count in 1usize..10) {
let _fiber_id = setup_test_fiber();
let handle = use_interval(|| {}, 10000);
for _ in 0..cancel_count {
handle.cancel();
prop_assert!(handle.is_cancelled());
}
cleanup_test();
}
#[test]
fn prop_interval_handle_clone_shares_state(_dummy in 0..1i32) {
let _fiber_id = setup_test_fiber();
let handle1 = use_interval(|| {}, 10000);
let handle2 = handle1.clone();
handle1.pause();
prop_assert!(handle1.is_paused());
prop_assert!(handle2.is_paused());
handle2.resume();
prop_assert!(!handle1.is_paused());
prop_assert!(!handle2.is_paused());
handle1.cancel();
prop_assert!(handle1.is_cancelled());
prop_assert!(handle2.is_cancelled());
cleanup_test();
}
#[test]
fn prop_multiple_timeouts_independent(count in 2usize..10) {
let _fiber_id = setup_test_fiber();
let handles: Vec<_> = (0..count)
.map(|_| use_timeout(|| {}, 10000))
.collect();
for handle in handles.iter().take(count / 2) {
handle.cancel();
}
for (i, handle) in handles.iter().enumerate() {
if i < count / 2 {
prop_assert!(handle.is_cancelled(), "Handle {} should be cancelled", i);
} else {
prop_assert!(!handle.is_cancelled(), "Handle {} should not be cancelled", i);
}
}
for handle in handles.iter().skip(count / 2) {
handle.cancel();
}
cleanup_test();
}
#[test]
fn prop_multiple_intervals_independent(count in 2usize..10) {
let _fiber_id = setup_test_fiber();
let handles: Vec<_> = (0..count)
.map(|_| use_interval(|| {}, 10000))
.collect();
for handle in handles.iter().take(count / 2) {
handle.pause();
}
for (i, handle) in handles.iter().enumerate() {
if i < count / 2 {
prop_assert!(handle.is_paused(), "Handle {} should be paused", i);
} else {
prop_assert!(!handle.is_paused(), "Handle {} should not be paused", i);
}
}
for handle in &handles {
handle.cancel();
}
cleanup_test();
}
}
}