mod cancellation;
mod value;
pub use cancellation::{CancelRegistration, CancellationSource, CancellationToken};
pub use fusor::coherence::{AsyncBoundary, BoundaryStatus};
pub use value::{AsyncRead, AsyncValue};
#[cfg(feature = "browser")]
pub mod browser;
#[cfg(feature = "browser")]
pub mod fetch;
use cancellation::InFlight;
use derive_where::derive_where;
use fusor::{Effect, OwnerHandle, Registration, Signal, batch, effect, signal, untrack};
use futures_util::future::LocalBoxFuture;
use std::{
cell::{Cell, RefCell},
future::Future,
rc::Rc,
};
#[derive(Debug)]
#[derive_where(Clone; K)]
pub struct Data<K, T> {
pub key: K,
pub value: Rc<T>,
}
#[derive(Debug)]
#[derive_where(Clone; K)]
pub enum ResourceState<K, T, E> {
Idle,
Loading {
key: K,
previous: Option<Data<K, T>>,
},
Ready(Data<K, T>),
Error {
key: K,
error: Rc<E>,
previous: Option<Data<K, T>>,
},
Disposed,
}
impl<K, T, E> ResourceState<K, T, E> {
pub fn data(&self) -> Option<&Data<K, T>> {
match self {
Self::Ready(data) => Some(data),
Self::Loading { previous, .. } | Self::Error { previous, .. } => previous.as_ref(),
Self::Idle | Self::Disposed => None,
}
}
pub fn is_loading(&self) -> bool {
matches!(self, Self::Loading { .. })
}
}
pub type Loader<I, O> = dyn Fn(I, CancellationToken) -> LocalBoxFuture<'static, O>;
pub type Spawner = dyn Fn(LocalBoxFuture<'static, ()>);
pub fn boxed_loader<I, O, F: Future<Output = O> + 'static>(
load: impl Fn(I, CancellationToken) -> F + 'static,
) -> Rc<Loader<I, O>> {
Rc::new(move |input, cancel| Box::pin(load(input, cancel)))
}
fn increment(counter: &Cell<u64>, name: &str) -> u64 {
let next = counter
.get()
.checked_add(1)
.unwrap_or_else(|| panic!("{name} overflow"));
counter.set(next);
next
}
struct Inner<K, T, E> {
in_flight: RefCell<Option<InFlight>>,
owner: OwnerHandle,
state: Signal<ResourceState<K, T, E>>,
key: RefCell<Option<K>>,
generation: Cell<u64>,
disposed: Cell<bool>,
subscription: RefCell<Option<Effect>>,
registrations: RefCell<Vec<Registration>>,
load: Rc<Loader<K, Result<T, E>>>,
spawn: Rc<Spawner>,
}
#[derive_where(Clone)]
pub struct Resource<K, T, E>(Rc<Inner<K, T, E>>);
impl<K: Clone + PartialEq + 'static, T: 'static, E: 'static> Resource<K, T, E> {
pub fn new<F: Future<Output = Result<T, E>> + 'static>(
owner: &OwnerHandle,
key: impl Fn() -> Option<K> + 'static,
load: impl Fn(K, CancellationToken) -> F + 'static,
spawn: impl Fn(LocalBoxFuture<'static, ()>) + 'static,
) -> Self {
Self::shared(owner, key, boxed_loader(load), Rc::new(spawn))
}
pub fn shared(
owner: &OwnerHandle,
key: impl Fn() -> Option<K> + 'static,
load: Rc<Loader<K, Result<T, E>>>,
spawn: Rc<Spawner>,
) -> Self {
let inner = Rc::new(Inner {
in_flight: RefCell::new(None),
owner: owner.clone(),
state: signal(ResourceState::Idle),
key: RefCell::new(None),
generation: Cell::new(0),
disposed: Cell::new(false),
subscription: RefCell::new(None),
registrations: RefCell::new(Vec::new()),
load,
spawn,
});
let weak = Rc::downgrade(&inner);
let cleanup = owner.on_cleanup(move || {
if let Some(inner) = weak.upgrade() {
inner.dispose();
}
});
inner.registrations.borrow_mut().push(cleanup);
if inner.disposed.get() {
return Self(inner);
}
let weak = Rc::downgrade(&inner);
let subscription = effect(move || {
let next = key();
if let Some(inner) = weak.upgrade() {
untrack(|| inner.set_key(next));
}
});
if inner.disposed.get() {
subscription.dispose();
} else {
*inner.subscription.borrow_mut() = Some(subscription);
}
let weak = Rc::downgrade(&inner);
if !owner.is_active() {
let activation = owner.on_activate(move || {
if let Some(inner) = weak.upgrade() {
inner.reload();
}
});
inner.registrations.borrow_mut().push(activation);
}
Self(inner)
}
pub fn get(&self) -> ResourceState<K, T, E> {
self.0.state.get()
}
pub fn with<R>(&self, read: impl FnOnce(&ResourceState<K, T, E>) -> R) -> R {
self.0.state.with(read)
}
pub fn refresh(&self) {
untrack(|| self.0.reload());
}
pub fn dispose(&self) {
self.0.dispose();
}
}
impl<K, T, E> Inner<K, T, E> {
fn invalidate(&self) -> u64 {
let generation = increment(&self.generation, "resource generation");
drop(self.in_flight.take());
generation
}
fn dispose(&self) {
if self.disposed.replace(true) {
return;
}
batch(|| {
self.invalidate();
drop(self.subscription.take());
self.publish(ResourceState::Disposed);
});
}
fn publish(&self, next: ResourceState<K, T, E>) {
drop(self.state.replace(next));
}
fn is_stopped(&self) -> bool {
self.disposed.get() || self.owner.is_disposed()
}
fn is_current(&self, generation: u64) -> bool {
!self.disposed.get() && self.owner.is_active() && self.generation.get() == generation
}
}
impl<K: Clone + 'static, T: 'static, E: 'static> Inner<K, T, E> {
fn set_key(self: &Rc<Self>, next: Option<K>)
where
K: PartialEq,
{
if self.is_stopped() || *self.key.borrow() == next {
return;
}
let retired_key = self.key.replace(next);
self.restart();
drop(retired_key);
}
fn reload(self: &Rc<Self>) {
if !self.is_stopped() {
self.restart();
}
}
fn restart(self: &Rc<Self>) {
let next = self.key.borrow().clone();
batch(|| {
let generation = self.invalidate();
if self.disposed.get() || self.generation.get() != generation {
return;
}
let Some(key) = next else {
self.publish(ResourceState::Idle);
return;
};
if self.owner.is_active() {
self.spawn_load(key, generation);
}
});
}
fn spawn_load(self: &Rc<Self>, key: K, generation: u64) {
let previous = self.state.with_untracked(|state| state.data().cloned());
let loading = ResourceState::Loading {
key: key.clone(),
previous: previous.clone(),
};
let weak = Rc::downgrade(self);
let load = self.load.clone();
let (in_flight, work) = InFlight::start(move |token| async move {
let result = load(key.clone(), token).await;
let Some(inner) = weak.upgrade().filter(|i| i.is_current(generation)) else {
return;
};
if let Some(in_flight) = inner.in_flight.take() {
in_flight.complete();
}
let next = match result {
Ok(value) => ResourceState::Ready(Data {
key,
value: Rc::new(value),
}),
Err(error) => ResourceState::Error {
key,
error: Rc::new(error),
previous,
},
};
inner.publish(next);
});
*self.in_flight.borrow_mut() = Some(in_flight);
self.publish(loading);
(self.spawn)(work);
}
}