use parking_lot::Mutex;
use std::any::Any;
use std::sync::Arc;
#[cfg(test)]
mod tests;
use crate::hook_context::with_hook_context;
use crate::panic_handler::spawn_catch_panic;
pub trait EffectDependencies: Any + Send + Sync {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool;
fn clone_deps(&self) -> Box<dyn EffectDependencies>;
fn debug_deps(&self) -> String;
fn deps_hash(&self) -> u64;
}
impl dyn EffectDependencies {
pub fn as_any(&self) -> &dyn Any {
self
}
}
impl EffectDependencies for () {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
other.as_any().downcast_ref::<()>().is_some()
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(())
}
fn debug_deps(&self) -> String {
"()".to_string()
}
fn deps_hash(&self) -> u64 {
0
}
}
macro_rules! impl_effect_deps_for_primitive {
($($t:ty),*) => {
$(
impl EffectDependencies for $t {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_val) = other.as_any().downcast_ref::<$t>() {
self == other_val
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(*self)
}
fn debug_deps(&self) -> String {
format!("{:?}", self)
}
fn deps_hash(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
)*
};
}
impl_effect_deps_for_primitive!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, bool, char
);
impl EffectDependencies for f32 {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_val) = other.as_any().downcast_ref::<f32>() {
self == other_val
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(*self)
}
fn debug_deps(&self) -> String {
format!("{:?}", self)
}
fn deps_hash(&self) -> u64 {
self.to_bits() as u64
}
}
impl EffectDependencies for f64 {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_val) = other.as_any().downcast_ref::<f64>() {
self == other_val
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(*self)
}
fn debug_deps(&self) -> String {
format!("{:?}", self)
}
fn deps_hash(&self) -> u64 {
self.to_bits()
}
}
impl EffectDependencies for String {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_val) = other.as_any().downcast_ref::<String>() {
self == other_val
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(self.clone())
}
fn debug_deps(&self) -> String {
format!("{:?}", self)
}
fn deps_hash(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
impl EffectDependencies for &'static str {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_val) = other.as_any().downcast_ref::<&'static str>() {
self == other_val
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(*self)
}
fn debug_deps(&self) -> String {
format!("{:?}", self)
}
fn deps_hash(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
pub struct CleanupFn {
#[allow(clippy::type_complexity)]
cleanup: Arc<Mutex<Option<Box<dyn FnOnce() + Send + 'static>>>>,
}
pub struct AsyncCleanupFn {
#[allow(clippy::type_complexity)]
cleanup: Arc<
Mutex<
Option<
Box<
dyn FnOnce() -> std::pin::Pin<
Box<dyn std::future::Future<Output = ()> + Send + 'static>,
> + Send
+ 'static,
>,
>,
>,
>,
}
impl CleanupFn {
pub fn new<F>(cleanup: F) -> Self
where
F: FnOnce() + Send + 'static,
{
Self {
cleanup: Arc::new(Mutex::new(Some(Box::new(cleanup)))),
}
}
pub fn cleanup(&self) {
if let Some(cleanup_fn) = self.cleanup.lock().take() {
cleanup_fn();
}
}
}
impl Clone for CleanupFn {
fn clone(&self) -> Self {
Self {
cleanup: self.cleanup.clone(),
}
}
}
impl AsyncCleanupFn {
pub fn new<F, Fut>(cleanup: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
Self {
cleanup: Arc::new(Mutex::new(Some(Box::new(move || {
Box::pin(cleanup())
as std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>
})))),
}
}
pub async fn cleanup(&self) {
let cleanup_fn = self.cleanup.lock().take();
if let Some(cleanup_fn) = cleanup_fn {
cleanup_fn().await;
}
}
}
impl Clone for AsyncCleanupFn {
fn clone(&self) -> Self {
Self {
cleanup: self.cleanup.clone(),
}
}
}
struct EffectState {
prev_deps: Option<Box<dyn EffectDependencies>>,
cleanup: Option<CleanupFn>,
initialized: bool,
}
impl EffectState {
fn new() -> Self {
Self {
prev_deps: None,
cleanup: None,
initialized: false,
}
}
}
struct AsyncEffectState {
prev_deps: Option<Box<dyn EffectDependencies>>,
cleanup: Option<AsyncCleanupFn>,
initialized: bool,
}
impl AsyncEffectState {
fn new() -> Self {
Self {
prev_deps: None,
cleanup: None,
initialized: false,
}
}
}
macro_rules! impl_effect_deps_for_tuple {
($($t:ident),*) => {
impl<$($t: EffectDependencies + Clone + PartialEq + std::fmt::Debug + 'static),*> EffectDependencies for ($($t,)*) {
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_tuple) = other.as_any().downcast_ref::<($($t,)*)>() {
self == other_tuple
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(self.clone())
}
fn debug_deps(&self) -> String {
format!("{:?}", self)
}
fn deps_hash(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.debug_deps().hash(&mut hasher);
hasher.finish()
}
}
};
}
impl_effect_deps_for_tuple!(T1);
impl_effect_deps_for_tuple!(T1, T2);
impl_effect_deps_for_tuple!(T1, T2, T3);
impl_effect_deps_for_tuple!(T1, T2, T3, T4);
impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5);
impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5, T6);
impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5, T6, T7);
impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
impl<T> EffectDependencies for Option<T>
where
T: EffectDependencies + Clone + PartialEq + std::fmt::Debug + 'static,
{
fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
if let Some(other_option) = other.as_any().downcast_ref::<Option<T>>() {
match (self, other_option) {
(None, None) => true,
(Some(a), Some(b)) => a.deps_eq(b),
_ => false,
}
} else {
false
}
}
fn clone_deps(&self) -> Box<dyn EffectDependencies> {
Box::new(self.clone())
}
fn debug_deps(&self) -> String {
match self {
None => "None".to_string(),
Some(value) => format!("Some({})", value.debug_deps()),
}
}
fn deps_hash(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
match self {
None => 0u8.hash(&mut hasher),
Some(value) => {
1u8.hash(&mut hasher);
value.deps_hash().hash(&mut hasher);
}
}
hasher.finish()
}
}
pub fn use_async_effect<Deps, F, Fut, C, CFut>(effect: F, deps: impl Into<Option<Deps>>)
where
Deps: EffectDependencies + Clone + PartialEq + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Option<C>> + Send + 'static,
C: FnOnce() -> CFut + Send + 'static,
CFut: std::future::Future<Output = ()> + Send + 'static,
{
let deps = deps.into();
with_hook_context(|ctx| {
let hook_index = ctx.next_hook_index();
let mut states = ctx.states.borrow_mut();
let effect_state = states
.entry(hook_index)
.or_insert_with(|| Box::new(AsyncEffectState::new()))
.downcast_mut::<AsyncEffectState>()
.expect("Async effect state type mismatch");
let should_run = match &deps {
None => {
true
}
Some(current_deps) => {
match &effect_state.prev_deps {
None => {
true
}
Some(prev_deps) => {
!current_deps.deps_eq(prev_deps.as_ref())
}
}
}
};
if should_run {
if let Some(cleanup) = effect_state.cleanup.take() {
tokio::spawn(spawn_catch_panic(async move {
cleanup.cleanup().await;
}));
}
if let Some(current_deps) = &deps {
effect_state.prev_deps = Some(current_deps.clone_deps());
} else {
effect_state.prev_deps = None;
}
tokio::spawn(spawn_catch_panic(async move {
if let Some(cleanup_fn) = effect().await {
let _async_cleanup = AsyncCleanupFn::new(cleanup_fn);
}
}));
effect_state.initialized = true;
}
});
}
pub fn use_effect<Deps, F, C>(effect: F, deps: impl Into<Option<Deps>>)
where
Deps: EffectDependencies + Clone + PartialEq + 'static,
F: FnOnce() -> Option<C> + 'static,
C: FnOnce() + Send + 'static,
{
let deps = deps.into();
with_hook_context(|ctx| {
let hook_index = ctx.next_hook_index();
let mut states = ctx.states.borrow_mut();
let effect_state = states
.entry(hook_index)
.or_insert_with(|| Box::new(EffectState::new()))
.downcast_mut::<EffectState>()
.expect("Effect state type mismatch");
let should_run = match &deps {
None => {
true
}
Some(current_deps) => {
match &effect_state.prev_deps {
None => {
true
}
Some(prev_deps) => {
!current_deps.deps_eq(prev_deps.as_ref())
}
}
}
};
if should_run {
if let Some(cleanup) = effect_state.cleanup.take() {
cleanup.cleanup();
}
if let Some(current_deps) = &deps {
effect_state.prev_deps = Some(current_deps.clone_deps());
} else {
effect_state.prev_deps = None;
}
if let Some(cleanup_fn) = effect() {
effect_state.cleanup = Some(CleanupFn::new(cleanup_fn));
}
effect_state.initialized = true;
}
});
}
pub fn use_effect_always<F, C>(effect: F)
where
F: FnOnce() -> C + 'static,
C: FnOnce() + Send + 'static,
{
use_effect::<(), _, C>(|| Some(effect()), None)
}
pub fn use_effect_once<F, C>(effect: F)
where
F: FnOnce() -> C + 'static,
C: FnOnce() + Send + 'static,
{
use_effect(|| Some(effect()), ())
}
pub fn use_async_effect_once<F, Fut, C, CFut>(effect: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = C> + Send + 'static,
C: FnOnce() -> CFut + Send + 'static,
CFut: std::future::Future<Output = ()> + Send + 'static,
{
use_async_effect(move || async move { Some(effect().await) }, ())
}
pub fn use_async_effect_always<F, Fut, C, CFut>(effect: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = C> + Send + 'static,
C: FnOnce() -> CFut + Send + 'static,
CFut: std::future::Future<Output = ()> + Send + 'static,
{
use_async_effect::<(), _, _, C, CFut>(move || async move { Some(effect().await) }, None)
}