use std::any::Any;
use std::cell::RefCell;
use crate::fiber::{AsyncCleanupFn, AsyncPendingEffect, CleanupFn, FiberId, PendingEffect};
use crate::fiber_tree::FiberTree;
thread_local! {
static EFFECT_QUEUE: RefCell<EffectQueue> = RefCell::new(EffectQueue::new());
}
pub struct EffectHookState {
pub deps: Option<Box<dyn Any + Send>>,
pub cleanup: Option<CleanupFn>,
}
impl EffectHookState {
pub fn new() -> Self {
Self {
deps: None,
cleanup: None,
}
}
pub fn with_deps<D: Any + Send + 'static>(deps: D) -> Self {
Self {
deps: Some(Box::new(deps)),
cleanup: None,
}
}
pub fn deps_changed<D: PartialEq + 'static>(&self, new_deps: &D) -> bool {
match &self.deps {
None => true, Some(boxed) => {
match boxed.downcast_ref::<D>() {
Some(old_deps) => old_deps != new_deps,
None => true, }
}
}
}
pub fn set_deps<D: Any + Send + 'static>(&mut self, deps: D) {
self.deps = Some(Box::new(deps));
}
pub fn take_cleanup(&mut self) -> Option<CleanupFn> {
self.cleanup.take()
}
pub fn set_cleanup(&mut self, cleanup: CleanupFn) {
self.cleanup = Some(cleanup);
}
}
impl Default for EffectHookState {
fn default() -> Self {
Self::new()
}
}
pub struct EffectQueue {
pending: Vec<(FiberId, PendingEffect)>,
pending_async: Vec<(FiberId, AsyncPendingEffect)>,
cleanups_to_run: Vec<CleanupFn>,
async_cleanups_to_run: Vec<AsyncCleanupFn>,
}
impl EffectQueue {
pub fn new() -> Self {
Self {
pending: Vec::new(),
pending_async: Vec::new(),
cleanups_to_run: Vec::new(),
async_cleanups_to_run: Vec::new(),
}
}
pub fn queue_effect(&mut self, fiber_id: FiberId, effect: PendingEffect) {
self.pending.push((fiber_id, effect));
}
pub fn queue_async_effect(&mut self, fiber_id: FiberId, effect: AsyncPendingEffect) {
self.pending_async.push((fiber_id, effect));
}
pub fn queue_cleanup(&mut self, cleanup: CleanupFn) {
self.cleanups_to_run.push(cleanup);
}
pub fn queue_async_cleanup(&mut self, cleanup: AsyncCleanupFn) {
self.async_cleanups_to_run.push(cleanup);
}
pub fn flush(&mut self, tree: &mut FiberTree) {
while let Some(cleanup) = self.cleanups_to_run.pop() {
cleanup();
}
for (fiber_id, pending) in self.pending.drain(..) {
if let Some(fiber) = tree.get_mut(fiber_id)
&& let Some(cleanup) = (pending.effect)()
{
fiber.cleanup_by_hook.insert(pending.hook_index, cleanup);
}
}
}
pub async fn flush_async(&mut self, tree: &mut FiberTree) {
while let Some(cleanup) = self.cleanups_to_run.pop() {
cleanup();
}
while let Some(async_cleanup) = self.async_cleanups_to_run.pop() {
async_cleanup().await;
}
for (fiber_id, pending) in self.pending.drain(..) {
if let Some(fiber) = tree.get_mut(fiber_id)
&& let Some(cleanup) = (pending.effect)()
{
fiber.cleanup_by_hook.insert(pending.hook_index, cleanup);
}
}
for (fiber_id, pending) in self.pending_async.drain(..) {
if let Some(fiber) = tree.get_mut(fiber_id)
&& let Some(async_cleanup) = (pending.effect)().await
{
fiber
.async_cleanup_by_hook
.insert(pending.hook_index, async_cleanup);
}
}
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty()
|| !self.pending_async.is_empty()
|| !self.cleanups_to_run.is_empty()
|| !self.async_cleanups_to_run.is_empty()
}
pub fn has_pending_async(&self) -> bool {
!self.pending_async.is_empty() || !self.async_cleanups_to_run.is_empty()
}
pub fn clear(&mut self) {
self.pending.clear();
self.pending_async.clear();
self.cleanups_to_run.clear();
self.async_cleanups_to_run.clear();
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn pending_async_count(&self) -> usize {
self.pending_async.len()
}
pub fn drain_async_effects(&mut self) -> Vec<(FiberId, AsyncPendingEffect)> {
self.pending_async.drain(..).collect()
}
pub fn cleanup_count(&self) -> usize {
self.cleanups_to_run.len()
}
pub fn async_cleanup_count(&self) -> usize {
self.async_cleanups_to_run.len()
}
}
impl Default for EffectQueue {
fn default() -> Self {
Self::new()
}
}
pub fn queue_effect(fiber_id: FiberId, effect: PendingEffect) {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().queue_effect(fiber_id, effect);
});
}
pub fn queue_async_effect(fiber_id: FiberId, effect: AsyncPendingEffect) {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().queue_async_effect(fiber_id, effect);
});
}
pub fn queue_cleanup(cleanup: CleanupFn) {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().queue_cleanup(cleanup);
});
}
pub fn queue_async_cleanup(cleanup: AsyncCleanupFn) {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().queue_async_cleanup(cleanup);
});
}
pub fn flush_effects_with_tree(tree: &mut FiberTree) {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().flush(tree);
});
}
pub fn flush_effects() {
crate::fiber_tree::with_fiber_tree_mut(|tree| {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().flush(tree);
});
});
}
pub async fn flush_async_effects() {
let has_async = has_pending_async_effects();
if has_async {
let (async_effects, async_cleanups) = EFFECT_QUEUE.with(|q| {
let mut queue = q.borrow_mut();
let effects = queue.pending_async.drain(..).collect::<Vec<_>>();
let cleanups = queue.async_cleanups_to_run.drain(..).collect::<Vec<_>>();
(effects, cleanups)
});
for async_cleanup in async_cleanups.into_iter().rev() {
async_cleanup().await;
}
for (fiber_id, pending) in async_effects {
let cleanup_opt = (pending.effect)().await;
if let Some(async_cleanup) = cleanup_opt {
crate::fiber_tree::with_fiber_tree_mut(|tree| {
if let Some(fiber) = tree.get_mut(fiber_id) {
fiber
.async_cleanup_by_hook
.insert(pending.hook_index, async_cleanup);
}
});
}
}
}
}
pub fn has_pending_effects() -> bool {
EFFECT_QUEUE.with(|q| q.borrow().has_pending())
}
pub fn has_pending_async_effects() -> bool {
EFFECT_QUEUE.with(|q| q.borrow().has_pending_async())
}
pub fn clear_effect_queue() {
EFFECT_QUEUE.with(|q| {
q.borrow_mut().clear();
});
}
pub fn with_effect_queue<R, F: FnOnce(&mut EffectQueue) -> R>(f: F) -> R {
EFFECT_QUEUE.with(|q| f(&mut q.borrow_mut()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
#[test]
fn test_effect_queue_creation() {
let queue = EffectQueue::new();
assert!(!queue.has_pending());
assert_eq!(queue.pending_count(), 0);
assert_eq!(queue.cleanup_count(), 0);
}
#[test]
fn test_queue_effect() {
let mut queue = EffectQueue::new();
let fiber_id = FiberId(1);
let effect = PendingEffect {
effect: Box::new(|| None),
hook_index: 0,
};
queue.queue_effect(fiber_id, effect);
assert!(queue.has_pending());
assert_eq!(queue.pending_count(), 1);
}
#[test]
fn test_queue_cleanup() {
let mut queue = EffectQueue::new();
let cleanup: CleanupFn = Box::new(|| {});
queue.queue_cleanup(cleanup);
assert!(queue.has_pending());
assert_eq!(queue.cleanup_count(), 1);
}
#[test]
fn test_flush_runs_cleanups_before_effects() {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
let execution_order = Arc::new(Mutex::new(Vec::new()));
let mut queue = EffectQueue::new();
let order_clone = execution_order.clone();
let cleanup: CleanupFn = Box::new(move || {
order_clone.lock().unwrap().push("cleanup");
});
queue.queue_cleanup(cleanup);
let order_clone = execution_order.clone();
let effect = PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push("effect");
None
}),
hook_index: 0,
};
queue.queue_effect(fiber_id, effect);
queue.flush(&mut tree);
let order = execution_order.lock().unwrap();
assert_eq!(order.len(), 2);
assert_eq!(order[0], "cleanup");
assert_eq!(order[1], "effect");
}
#[test]
fn test_flush_runs_cleanups_in_reverse_order() {
let mut tree = FiberTree::new();
let _ = tree.mount(None, None);
let execution_order = Arc::new(Mutex::new(Vec::new()));
let mut queue = EffectQueue::new();
for i in 1..=3 {
let order_clone = execution_order.clone();
let cleanup: CleanupFn = Box::new(move || {
order_clone.lock().unwrap().push(i);
});
queue.queue_cleanup(cleanup);
}
queue.flush(&mut tree);
let order = execution_order.lock().unwrap();
assert_eq!(*order, vec![3, 2, 1]);
}
#[test]
fn test_flush_runs_effects_in_declaration_order() {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
let execution_order = Arc::new(Mutex::new(Vec::new()));
let mut queue = EffectQueue::new();
for i in 1..=3 {
let order_clone = execution_order.clone();
let effect = PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push(i);
None
}),
hook_index: i,
};
queue.queue_effect(fiber_id, effect);
}
queue.flush(&mut tree);
let order = execution_order.lock().unwrap();
assert_eq!(*order, vec![1, 2, 3]);
}
#[test]
fn test_effect_returns_cleanup() {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
let mut queue = EffectQueue::new();
let effect = PendingEffect {
effect: Box::new(|| Some(Box::new(|| {}) as CleanupFn)),
hook_index: 0,
};
queue.queue_effect(fiber_id, effect);
queue.flush(&mut tree);
let fiber = tree.get(fiber_id).unwrap();
assert_eq!(fiber.cleanup_by_hook.len(), 1);
assert!(fiber.cleanup_by_hook.contains_key(&0));
}
#[test]
fn test_clear_queue() {
let mut queue = EffectQueue::new();
let fiber_id = FiberId(1);
queue.queue_effect(
fiber_id,
PendingEffect {
effect: Box::new(|| None),
hook_index: 0,
},
);
queue.queue_cleanup(Box::new(|| {}));
assert!(queue.has_pending());
queue.clear();
assert!(!queue.has_pending());
assert_eq!(queue.pending_count(), 0);
assert_eq!(queue.cleanup_count(), 0);
}
#[test]
fn test_effect_hook_state_creation() {
let state = EffectHookState::new();
assert!(state.deps.is_none());
assert!(state.cleanup.is_none());
}
#[test]
fn test_effect_hook_state_with_deps() {
let state = EffectHookState::with_deps((1, 2, 3));
assert!(state.deps.is_some());
}
#[test]
fn test_effect_hook_state_deps_changed() {
let mut state = EffectHookState::new();
assert!(state.deps_changed(&(1, 2)));
state.set_deps((1, 2));
assert!(!state.deps_changed(&(1, 2)));
assert!(state.deps_changed(&(1, 3)));
}
#[test]
fn test_effect_hook_state_cleanup() {
let mut state = EffectHookState::new();
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
state.set_cleanup(Box::new(move || {
called_clone.store(true, Ordering::SeqCst);
}));
assert!(state.cleanup.is_some());
let cleanup = state.take_cleanup();
assert!(state.cleanup.is_none());
cleanup.unwrap()();
assert!(called.load(Ordering::SeqCst));
}
#[test]
fn test_thread_local_queue_effect() {
clear_effect_queue();
let fiber_id = FiberId(1);
queue_effect(
fiber_id,
PendingEffect {
effect: Box::new(|| None),
hook_index: 0,
},
);
assert!(has_pending_effects());
clear_effect_queue();
assert!(!has_pending_effects());
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
use std::sync::{Arc, Mutex};
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_effect_execution_ordering(
cleanup_count in 1usize..10,
effect_count in 1usize..10
) {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
let mut queue = EffectQueue::new();
let execution_order = Arc::new(Mutex::new(Vec::new()));
for i in 0..cleanup_count {
let order_clone = execution_order.clone();
let cleanup: CleanupFn = Box::new(move || {
order_clone.lock().unwrap().push(format!("cleanup_{}", i));
});
queue.queue_cleanup(cleanup);
}
for i in 0..effect_count {
let order_clone = execution_order.clone();
let effect = PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push(format!("effect_{}", i));
None
}),
hook_index: i,
};
queue.queue_effect(fiber_id, effect);
}
queue.flush(&mut tree);
let order = execution_order.lock().unwrap();
let cleanup_end_idx = cleanup_count;
for i in 0..cleanup_count {
prop_assert!(
order[i].starts_with("cleanup_"),
"Cleanups should run before effects, but found {} at index {}",
order[i], i
);
}
for i in 0..cleanup_count {
let expected_cleanup_idx = cleanup_count - 1 - i;
prop_assert_eq!(
&order[i],
&format!("cleanup_{}", expected_cleanup_idx),
"Cleanups should run in reverse order"
);
}
for i in 0..effect_count {
let order_idx = cleanup_end_idx + i;
prop_assert_eq!(
&order[order_idx],
&format!("effect_{}", i),
"Effects should run in declaration order"
);
}
prop_assert_eq!(
order.len(),
cleanup_count + effect_count,
"All cleanups and effects should execute exactly once"
);
}
#[test]
fn prop_effect_dependency_tracking(
initial_deps in any::<(i32, String)>(),
same_deps_renders in 1usize..5,
changed_deps in any::<(i32, String)>()
) {
prop_assume!(initial_deps != changed_deps);
let mut effect_state = EffectHookState::new();
let mut run_count = 0;
prop_assert!(
effect_state.deps_changed(&initial_deps),
"Effect should run on first render (no previous deps)"
);
effect_state.set_deps(initial_deps.clone());
run_count += 1;
for _ in 0..same_deps_renders {
prop_assert!(
!effect_state.deps_changed(&initial_deps),
"Effect should NOT run when deps are equal"
);
}
prop_assert_eq!(
run_count, 1,
"Effect should run exactly once when deps don't change"
);
prop_assert!(
effect_state.deps_changed(&changed_deps),
"Effect should run when deps change"
);
effect_state.set_deps(changed_deps.clone());
run_count += 1;
prop_assert_eq!(
run_count, 2,
"Effect should run exactly twice: first render and when deps change"
);
prop_assert!(
!effect_state.deps_changed(&changed_deps),
"Effect should NOT run when deps remain equal after change"
);
}
#[test]
fn prop_effect_none_deps_runs_every_render(
render_count in 1usize..20
) {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
let mut queue = EffectQueue::new();
let execution_count = Arc::new(Mutex::new(0));
for _ in 0..render_count {
let count_clone = execution_count.clone();
let effect = PendingEffect {
effect: Box::new(move || {
*count_clone.lock().unwrap() += 1;
None
}),
hook_index: 0,
};
queue.queue_effect(fiber_id, effect);
queue.flush(&mut tree);
}
let final_count = *execution_count.lock().unwrap();
prop_assert_eq!(
final_count,
render_count,
"Effect with None deps should run after every render"
);
}
#[test]
fn prop_effect_empty_deps_runs_once(
render_count in 2usize..20
) {
let mut effect_state = EffectHookState::new();
let mut run_count = 0;
if effect_state.deps_changed(&()) {
run_count += 1;
effect_state.set_deps(());
}
for _ in 1..render_count {
if effect_state.deps_changed(&()) {
run_count += 1;
}
}
prop_assert_eq!(
run_count, 1,
"Effect with Some(()) deps should run only once on mount"
);
}
#[test]
fn prop_cleanup_runs_before_new_effect(
initial_deps in any::<i32>(),
changed_deps in any::<i32>()
) {
prop_assume!(initial_deps != changed_deps);
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
let mut queue = EffectQueue::new();
let execution_order = Arc::new(Mutex::new(Vec::new()));
let order_clone = execution_order.clone();
let effect1 = PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push("effect1");
let order_clone2 = order_clone.clone();
Some(Box::new(move || {
order_clone2.lock().unwrap().push("cleanup1");
}) as CleanupFn)
}),
hook_index: 0,
};
queue.queue_effect(fiber_id, effect1);
queue.flush(&mut tree);
if let Some(fiber) = tree.get_mut(fiber_id) {
if let Some(cleanup) = fiber.cleanup_by_hook.remove(&0) {
queue.queue_cleanup(cleanup);
}
}
let order_clone = execution_order.clone();
let effect2 = PendingEffect {
effect: Box::new(move || {
order_clone.lock().unwrap().push("effect2");
None
}),
hook_index: 0,
};
queue.queue_effect(fiber_id, effect2);
queue.flush(&mut tree);
let order = execution_order.lock().unwrap();
prop_assert_eq!(order.len(), 3, "Should have 3 executions");
prop_assert_eq!(&order[0], &"effect1".to_string(), "First effect runs first");
prop_assert_eq!(&order[1], &"cleanup1".to_string(), "Cleanup runs before new effect");
prop_assert_eq!(&order[2], &"effect2".to_string(), "New effect runs after cleanup");
}
}
}