use core::fmt;
use dioxus::prelude::*;
use dioxus::signals::CopyValue;
use dioxus_core::{provide_root_context, spawn_forever, use_drop, ReactiveContext, Task};
use std::{
cell::{Ref, RefCell},
collections::{HashMap, HashSet},
future::Future,
hash::Hash,
mem,
rc::Rc,
sync::{Arc, Mutex},
time::Duration,
};
#[cfg(not(target_family = "wasm"))]
use tokio::time;
#[cfg(not(target_family = "wasm"))]
use tokio::time::Instant;
#[cfg(target_family = "wasm")]
use wasmtimer::tokio as time;
#[cfg(target_family = "wasm")]
use web_time::Instant;
pub trait MutationCapability
where
Self: 'static + Clone + PartialEq + Hash + Eq,
{
type Ok;
type Err;
type Keys: Hash + PartialEq + Clone;
fn run(&self, keys: &Self::Keys) -> impl Future<Output = Result<Self::Ok, Self::Err>>;
fn matches(&self, _keys: &Self::Keys) -> bool {
true
}
fn on_settled(
&self,
_keys: &Self::Keys,
_result: &Result<Self::Ok, Self::Err>,
) -> impl Future<Output = ()> {
async {}
}
}
pub enum MutationStateData<Q: MutationCapability> {
Pending,
Loading { res: Option<Result<Q::Ok, Q::Err>> },
Settled {
res: Result<Q::Ok, Q::Err>,
settlement_instant: Instant,
},
}
impl<Q> fmt::Debug for MutationStateData<Q>
where
Q: MutationCapability,
Q::Ok: fmt::Debug,
Q::Err: fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => f.write_str("Pending"),
Self::Loading { res } => write!(f, "Loading {{ {res:?} }}"),
Self::Settled { res, .. } => write!(f, "Settled {{ {res:?} }}"),
}
}
}
impl<Q: MutationCapability> MutationStateData<Q> {
pub fn is_ok(&self) -> bool {
matches!(self, MutationStateData::Settled { res: Ok(_), .. })
}
pub fn is_err(&self) -> bool {
matches!(self, MutationStateData::Settled { res: Err(_), .. })
}
pub fn is_loading(&self) -> bool {
matches!(self, MutationStateData::Loading { .. })
}
pub fn is_pending(&self) -> bool {
matches!(self, MutationStateData::Pending)
}
pub fn ok(&self) -> Option<&Q::Ok> {
match self {
Self::Settled { res: Ok(res), .. } => Some(res),
Self::Loading { res: Some(Ok(res)) } => Some(res),
_ => None,
}
}
pub fn unwrap(&self) -> &Result<Q::Ok, Q::Err> {
match self {
Self::Loading { res: Some(v) } => v,
Self::Settled { res, .. } => res,
_ => unreachable!(),
}
}
fn into_loading(self) -> MutationStateData<Q> {
match self {
MutationStateData::Pending => MutationStateData::Loading { res: None },
MutationStateData::Loading { res } => MutationStateData::Loading { res },
MutationStateData::Settled { res, .. } => MutationStateData::Loading { res: Some(res) },
}
}
}
pub struct MutationsStorage<Q: MutationCapability> {
storage: CopyValue<HashMap<Mutation<Q>, MutationData<Q>>>,
}
impl<Q: MutationCapability> Copy for MutationsStorage<Q> {}
impl<Q: MutationCapability> Clone for MutationsStorage<Q> {
fn clone(&self) -> Self {
*self
}
}
pub struct MutationData<Q: MutationCapability> {
state: Rc<RefCell<MutationStateData<Q>>>,
reactive_contexts: Arc<Mutex<HashSet<ReactiveContext>>>,
clean_task: Rc<RefCell<Option<Task>>>,
}
impl<Q: MutationCapability> Clone for MutationData<Q> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
reactive_contexts: self.reactive_contexts.clone(),
clean_task: self.clean_task.clone(),
}
}
}
impl<Q: MutationCapability> MutationsStorage<Q> {
fn new_in_root() -> Self {
Self {
storage: CopyValue::new_in_scope(HashMap::default(), ScopeId::ROOT),
}
}
fn insert_or_get_mutation(&mut self, mutation: Mutation<Q>) -> MutationData<Q> {
let mut storage = self.storage.write();
let mutation_data = storage.entry(mutation).or_insert_with(|| MutationData {
state: Rc::new(RefCell::new(MutationStateData::Pending)),
reactive_contexts: Arc::default(),
clean_task: Rc::default(),
});
if let Some(clean_task) = mutation_data.clean_task.take() {
clean_task.cancel();
}
mutation_data.clone()
}
fn update_tasks(&mut self, mutation: Mutation<Q>) {
let mut storage_clone = self.storage;
let mut storage = self.storage.write();
let mutation_data = storage.get_mut(&mutation).unwrap();
if mutation_data.reactive_contexts.lock().unwrap().is_empty() {
*mutation_data.clean_task.borrow_mut() = Some(spawn_forever(async move {
time::sleep(mutation.clean_time).await;
let mut storage = storage_clone.write();
storage.remove(&mutation);
}));
}
}
async fn run(mutation: &Mutation<Q>, data: &MutationData<Q>, keys: Q::Keys) {
let res =
mem::replace(&mut *data.state.borrow_mut(), MutationStateData::Pending).into_loading();
*data.state.borrow_mut() = res;
for reactive_context in data.reactive_contexts.lock().unwrap().iter() {
reactive_context.mark_dirty();
}
let res = mutation.mutation.run(&keys).await;
mutation.mutation.on_settled(&keys, &res).await;
*data.state.borrow_mut() = MutationStateData::Settled {
res,
settlement_instant: Instant::now(),
};
for reactive_context in data.reactive_contexts.lock().unwrap().iter() {
reactive_context.mark_dirty();
}
}
}
#[derive(PartialEq, Clone)]
pub struct Mutation<Q: MutationCapability> {
mutation: Q,
clean_time: Duration,
}
impl<Q: MutationCapability> Eq for Mutation<Q> {}
impl<Q: MutationCapability> Hash for Mutation<Q> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.mutation.hash(state);
}
}
impl<Q: MutationCapability> Mutation<Q> {
pub fn new(mutation: Q) -> Self {
Self {
mutation,
clean_time: Duration::ZERO,
}
}
pub fn clean_time(self, clean_time: Duration) -> Self {
Self { clean_time, ..self }
}
}
pub struct MutationReader<Q: MutationCapability> {
state: Rc<RefCell<MutationStateData<Q>>>,
}
impl<Q: MutationCapability> MutationReader<Q> {
pub fn state(&self) -> Ref<MutationStateData<Q>> {
self.state.borrow()
}
}
pub struct UseMutation<Q: MutationCapability> {
mutation: Signal<Mutation<Q>>,
}
impl<Q: MutationCapability> Clone for UseMutation<Q> {
fn clone(&self) -> Self {
*self
}
}
impl<Q: MutationCapability> Copy for UseMutation<Q> {}
impl<Q: MutationCapability> UseMutation<Q> {
pub fn read(&self) -> MutationReader<Q> {
let storage = consume_context::<MutationsStorage<Q>>();
let mutation_data = storage
.storage
.peek_unchecked()
.get(&self.mutation.peek())
.cloned()
.unwrap();
if let Some(reactive_context) = ReactiveContext::current() {
reactive_context.subscribe(mutation_data.reactive_contexts);
}
MutationReader {
state: mutation_data.state,
}
}
pub fn peek(&self) -> MutationReader<Q> {
let storage = consume_context::<MutationsStorage<Q>>();
let mutation_data = storage
.storage
.peek_unchecked()
.get(&self.mutation.peek())
.cloned()
.unwrap();
MutationReader {
state: mutation_data.state,
}
}
pub async fn mutate_async(&self, keys: Q::Keys) -> MutationReader<Q> {
let storage = consume_context::<MutationsStorage<Q>>();
let mutation = self.mutation.peek().clone();
let mutation_data = storage
.storage
.peek_unchecked()
.get(&mutation)
.cloned()
.unwrap();
MutationsStorage::run(&mutation, &mutation_data, keys).await;
MutationReader {
state: mutation_data.state,
}
}
pub fn mutate(&self, keys: Q::Keys) {
let storage = consume_context::<MutationsStorage<Q>>();
let mutation = self.mutation.peek().clone();
let mutation_data = storage
.storage
.peek_unchecked()
.get(&mutation)
.cloned()
.unwrap();
spawn(async move {
MutationsStorage::run(&mutation, &mutation_data, keys).await;
});
}
}
pub fn use_mutation<Q: MutationCapability>(mutation: Mutation<Q>) -> UseMutation<Q> {
let mut storage = match try_consume_context::<MutationsStorage<Q>>() {
Some(storage) => storage,
None => provide_root_context(MutationsStorage::<Q>::new_in_root()),
};
let mut make_mutation = |mutation: &Mutation<Q>, mut prev_mutation: Option<Mutation<Q>>| {
let _data = storage.insert_or_get_mutation(mutation.clone());
if let Some(prev_mutation) = prev_mutation.take() {
storage.update_tasks(prev_mutation);
}
};
let mut current_mutation = use_hook(|| {
make_mutation(&mutation, None);
Signal::new(mutation.clone())
});
if *current_mutation.read() != mutation {
let prev = mem::replace(&mut *current_mutation.write(), mutation.clone());
make_mutation(&mutation, Some(prev));
}
use_drop({
move || {
storage.update_tasks(current_mutation.peek().clone());
}
});
UseMutation {
mutation: current_mutation,
}
}