use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::future::Future;
use std::rc::Rc;
use wasm_bindgen_futures::spawn_local;
use crate::reactive::ScopeId;
use crate::scope::current_scope_id;
#[derive(Default)]
struct ScopeTasks {
tasks: Vec<Rc<TaskState>>,
latest: HashMap<String, Rc<TaskState>>,
}
struct TaskState {
cancelled: Cell<bool>,
}
#[derive(Clone)]
pub struct TaskHandle {
inner: Rc<TaskState>,
}
thread_local! {
static TASKS: RefCell<HashMap<ScopeId, ScopeTasks>> = RefCell::new(HashMap::new());
}
impl TaskHandle {
fn new() -> Self {
Self {
inner: Rc::new(TaskState {
cancelled: Cell::new(false),
}),
}
}
fn cancelled() -> Self {
let handle = Self::new();
handle.cancel();
handle
}
pub fn cancel(&self) {
self.inner.cancelled.set(true);
}
pub fn is_cancelled(&self) -> bool {
self.inner.cancelled.get()
}
}
pub fn spawn(fut: impl Future<Output = ()> + 'static) {
spawn_local(fut);
}
pub fn spawn_scoped(fut: impl Future<Output = ()> + 'static) -> TaskHandle {
let scope_id = current_scope_id()
.expect("pocopine::spawn_scoped called outside a handler / lifecycle context");
spawn_for_scope(scope_id, fut)
}
pub fn spawn_for_scope(scope_id: ScopeId, fut: impl Future<Output = ()> + 'static) -> TaskHandle {
if crate::scope::Scope::find(scope_id).is_none() {
return TaskHandle::cancelled();
}
let handle = TaskHandle::new();
TASKS.with(|tasks| {
tasks
.borrow_mut()
.entry(scope_id)
.or_default()
.tasks
.push(handle.inner.clone());
});
let inner = handle.inner.clone();
spawn_local(async move {
fut.await;
let _ = inner;
});
handle
}
pub fn spawn_latest(
task_name: impl Into<Cow<'static, str>>,
fut: impl Future<Output = ()> + 'static,
) -> TaskHandle {
let scope_id = current_scope_id()
.expect("pocopine::spawn_latest called outside a handler / lifecycle context");
let task_name = task_name.into().into_owned();
spawn_latest_for_scope(scope_id, task_name, fut)
}
pub fn spawn_latest_for_scope(
scope_id: ScopeId,
task_name: impl Into<Cow<'static, str>>,
fut: impl Future<Output = ()> + 'static,
) -> TaskHandle {
if crate::scope::Scope::find(scope_id).is_none() {
return TaskHandle::cancelled();
}
let task_name = task_name.into().into_owned();
let handle = TaskHandle::new();
TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
let scope_tasks = tasks.entry(scope_id).or_default();
if let Some(prev) = scope_tasks
.latest
.insert(task_name.clone(), handle.inner.clone())
{
prev.cancelled.set(true);
}
scope_tasks.tasks.push(handle.inner.clone());
});
let inner = handle.inner.clone();
spawn_local(async move {
fut.await;
TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
let Some(scope_tasks) = tasks.get_mut(&scope_id) else {
return;
};
if scope_tasks
.latest
.get(&task_name)
.is_some_and(|current| Rc::ptr_eq(current, &inner))
{
scope_tasks.latest.remove(&task_name);
}
});
});
handle
}
pub fn clear_scope(scope_id: ScopeId) {
TASKS.with(|tasks| {
if let Some(scope_tasks) = tasks.borrow_mut().remove(&scope_id) {
for task in scope_tasks.tasks {
task.cancelled.set(true);
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_for_scope_returns_cancelled_handle_when_scope_is_gone() {
let handle = spawn_for_scope(ScopeId(u64::MAX), async move {});
assert!(handle.is_cancelled());
}
#[test]
fn spawn_latest_for_scope_returns_cancelled_handle_when_scope_is_gone() {
let handle = spawn_latest_for_scope(ScopeId(u64::MAX), "search", async move {});
assert!(handle.is_cancelled());
}
}