use std::marker::PhantomData;
use std::sync::{Arc, RwLock};
use crate::fiber::FiberId;
use crate::fiber_tree::with_current_fiber;
type EffectEventHandler<IN, OUT> = Arc<RwLock<Box<dyn Fn(IN) -> OUT + Send + Sync>>>;
pub(crate) struct EffectEventStorage<IN, OUT> {
pub(crate) handler: EffectEventHandler<IN, OUT>,
}
impl<IN, OUT> Clone for EffectEventStorage<IN, OUT> {
fn clone(&self) -> Self {
Self {
handler: self.handler.clone(),
}
}
}
pub struct EffectEvent<IN, OUT> {
pub(crate) fiber_id: FiberId,
pub(crate) hook_index: usize,
pub(crate) handler: EffectEventHandler<IN, OUT>,
pub(crate) _marker: PhantomData<(IN, OUT)>,
}
impl<IN, OUT> std::fmt::Debug for EffectEvent<IN, OUT> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EffectEvent")
.field("fiber_id", &self.fiber_id)
.field("hook_index", &self.hook_index)
.finish_non_exhaustive()
}
}
impl<IN, OUT> Clone for EffectEvent<IN, OUT> {
fn clone(&self) -> Self {
Self {
fiber_id: self.fiber_id,
hook_index: self.hook_index,
handler: self.handler.clone(),
_marker: PhantomData,
}
}
}
impl<IN, OUT> EffectEvent<IN, OUT>
where
IN: 'static,
OUT: 'static,
{
pub fn call(&self, input: IN) -> OUT {
let handler = self.handler.read().expect("EffectEvent lock poisoned");
handler(input)
}
}
pub fn use_effect_event<IN, OUT, F>(handler: F) -> EffectEvent<IN, OUT>
where
IN: 'static,
OUT: 'static,
F: Fn(IN) -> OUT + Send + Sync + 'static,
{
with_current_fiber(|fiber| {
let hook_index = fiber.next_hook_index();
let existing_storage: Option<EffectEventStorage<IN, OUT>> = fiber.get_hook(hook_index);
let storage = if let Some(storage) = existing_storage {
{
let mut guard = storage.handler.write().expect("EffectEvent lock poisoned");
*guard = Box::new(handler);
}
storage
} else {
let new_storage = EffectEventStorage {
handler: Arc::new(RwLock::new(
Box::new(handler) as Box<dyn Fn(IN) -> OUT + Send + Sync>
)),
};
fiber.set_hook(hook_index, new_storage.clone());
new_storage
};
EffectEvent {
fiber_id: fiber.id,
hook_index,
handler: storage.handler,
_marker: PhantomData,
}
})
.expect("use_effect_event must be called within a component render context")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
use std::sync::atomic::{AtomicI32, Ordering};
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
clear_fiber_tree();
}
#[test]
fn test_use_effect_event_basic() {
let _fiber_id = setup_test_fiber();
let effect_event = use_effect_event(|x: i32| x * 2);
assert_eq!(effect_event.call(5), 10);
cleanup_test();
}
#[test]
fn test_use_effect_event_with_unit_input() {
let _fiber_id = setup_test_fiber();
let counter = Arc::new(AtomicI32::new(0));
let counter_clone = counter.clone();
let effect_event = use_effect_event(move |_: ()| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
effect_event.call(());
effect_event.call(());
effect_event.call(());
assert_eq!(counter.load(Ordering::SeqCst), 3);
cleanup_test();
}
#[test]
fn test_effect_event_stability_across_renders() {
let fiber_id = setup_test_fiber();
let effect_event1 = use_effect_event(|x: i32| x + 1);
let handler_ptr1 = Arc::as_ptr(&effect_event1.handler);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let effect_event2 = use_effect_event(|x: i32| x + 100);
let handler_ptr2 = Arc::as_ptr(&effect_event2.handler);
assert_eq!(
handler_ptr1, handler_ptr2,
"Effect event should have stable Arc reference across renders"
);
assert_eq!(effect_event2.call(5), 105);
cleanup_test();
}
#[test]
fn test_effect_event_sees_current_state() {
let fiber_id = setup_test_fiber();
let state = Arc::new(AtomicI32::new(10));
let state_clone = state.clone();
let effect_event = use_effect_event(move |_: ()| state_clone.load(Ordering::SeqCst));
assert_eq!(effect_event.call(()), 10);
state.store(42, Ordering::SeqCst);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let state_clone2 = state.clone();
let effect_event2 = use_effect_event(move |_: ()| state_clone2.load(Ordering::SeqCst));
assert_eq!(effect_event2.call(()), 42);
assert_eq!(effect_event.call(()), 42);
cleanup_test();
}
#[test]
fn test_effect_event_clone_shares_handler() {
let _fiber_id = setup_test_fiber();
let counter = Arc::new(AtomicI32::new(0));
let counter_clone = counter.clone();
let effect_event = use_effect_event(move |_: ()| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
let effect_event_clone = effect_event.clone();
effect_event.call(());
effect_event_clone.call(());
assert_eq!(counter.load(Ordering::SeqCst), 2);
cleanup_test();
}
#[test]
fn test_multiple_effect_events() {
let _fiber_id = setup_test_fiber();
let effect1 = use_effect_event(|x: i32| x * 2);
let effect2 = use_effect_event(|x: i32| x + 10);
let effect3 = use_effect_event(|s: &str| s.len());
assert_eq!(effect1.call(5), 10);
assert_eq!(effect2.call(5), 15);
assert_eq!(effect3.call("hello"), 5);
cleanup_test();
}
#[test]
fn test_effect_event_with_return_value() {
let _fiber_id = setup_test_fiber();
let effect_event = use_effect_event(|input: (i32, i32)| {
let (a, b) = input;
format!("{} + {} = {}", a, b, a + b)
});
assert_eq!(effect_event.call((3, 4)), "3 + 4 = 7");
cleanup_test();
}
#[test]
fn test_effect_event_fiber_id_and_hook_index() {
let fiber_id = setup_test_fiber();
let effect1 = use_effect_event(|_: ()| {});
let effect2 = use_effect_event(|_: ()| {});
assert_eq!(effect1.fiber_id, fiber_id);
assert_eq!(effect2.fiber_id, fiber_id);
assert_eq!(effect1.hook_index, 0);
assert_eq!(effect2.hook_index, 1);
cleanup_test();
}
#[test]
#[should_panic(expected = "use_effect_event must be called within a component render context")]
fn test_use_effect_event_panics_outside_render() {
clear_fiber_tree();
let _ = use_effect_event(|_: ()| {});
}
#[test]
fn test_effect_event_handler_updated_each_render() {
let fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let cc1 = call_count.clone();
let effect_event = use_effect_event(move |_: ()| {
cc1.fetch_add(1, Ordering::SeqCst);
1
});
assert_eq!(effect_event.call(()), 1);
assert_eq!(call_count.load(Ordering::SeqCst), 1);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let cc2 = call_count.clone();
let _effect_event2 = use_effect_event(move |_: ()| {
cc2.fetch_add(1, Ordering::SeqCst);
2
});
assert_eq!(effect_event.call(()), 2);
assert_eq!(call_count.load(Ordering::SeqCst), 2);
cleanup_test();
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree, with_fiber_tree_mut};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use proptest::prelude::*;
use std::sync::atomic::{AtomicI32, Ordering};
static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
clear_fiber_tree();
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_effect_event_function_stability(
num_renders in 2usize..20
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let effect_event1 = use_effect_event(|x: i32| x + 1);
let handler_ptr1 = Arc::as_ptr(&effect_event1.handler);
for render_num in 1..num_renders {
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let multiplier = render_num as i32;
let effect_event = use_effect_event(move |x: i32| x * multiplier);
let handler_ptr = Arc::as_ptr(&effect_event.handler);
prop_assert_eq!(
handler_ptr,
handler_ptr1,
"Render {}: Effect event Arc pointer should be stable (expected {:?}, got {:?})",
render_num,
handler_ptr1,
handler_ptr
);
}
cleanup_test();
}
#[test]
fn prop_effect_event_stability_with_varying_handlers(
initial_offset in any::<i32>(),
offsets in prop::collection::vec(any::<i32>(), 1..10)
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let effect_event1 = use_effect_event(move |x: i32| x + initial_offset);
let handler_ptr1 = Arc::as_ptr(&effect_event1.handler);
let fiber_id1 = effect_event1.fiber_id;
let hook_index1 = effect_event1.hook_index;
for offset in &offsets {
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let offset_copy = *offset;
let effect_event = use_effect_event(move |x: i32| x + offset_copy);
prop_assert_eq!(
Arc::as_ptr(&effect_event.handler),
handler_ptr1,
"Handler Arc pointer should be stable"
);
prop_assert_eq!(
effect_event.fiber_id,
fiber_id1,
"Fiber ID should be stable"
);
prop_assert_eq!(
effect_event.hook_index,
hook_index1,
"Hook index should be stable"
);
}
cleanup_test();
}
#[test]
fn prop_effect_event_sees_current_state(
initial_state in any::<i32>(),
state_updates in prop::collection::vec(any::<i32>(), 1..10)
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let state = Arc::new(AtomicI32::new(initial_state));
let state_clone = state.clone();
let effect_event = use_effect_event(move |_: ()| {
state_clone.load(Ordering::SeqCst)
});
prop_assert_eq!(
effect_event.call(()),
initial_state,
"Should see initial state"
);
for new_state in &state_updates {
state.store(*new_state, Ordering::SeqCst);
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let state_clone2 = state.clone();
let _effect_event2 = use_effect_event(move |_: ()| {
state_clone2.load(Ordering::SeqCst)
});
prop_assert_eq!(
effect_event.call(()),
*new_state,
"Old effect event reference should see current state {}",
new_state
);
}
cleanup_test();
}
#[test]
fn prop_effect_event_handler_always_current(
num_renders in 2usize..15
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let effect_event = use_effect_event(|_: ()| 1i32);
prop_assert_eq!(effect_event.call(()), 1, "First render should return 1");
for render_num in 2..=num_renders {
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let expected = render_num as i32;
let _effect_event_new = use_effect_event(move |_: ()| expected);
prop_assert_eq!(
effect_event.call(()),
expected,
"Render {}: Old effect event should call current handler (expected {}, got {})",
render_num,
expected,
effect_event.call(())
);
}
cleanup_test();
}
#[test]
fn prop_multiple_effect_events_independent(
num_events in 2usize..5,
num_renders in 1usize..5
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let mut effect_events = Vec::new();
let mut handler_ptrs = Vec::new();
for i in 0..num_events {
let offset = i as i32;
let effect_event = use_effect_event(move |x: i32| x + offset);
handler_ptrs.push(Arc::as_ptr(&effect_event.handler));
effect_events.push(effect_event);
}
for (i, effect_event) in effect_events.iter().enumerate() {
let expected = 10 + i as i32;
prop_assert_eq!(
effect_event.call(10),
expected,
"Initial: Effect event {} should return {}",
i,
expected
);
}
for render_num in 1..=num_renders {
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
for (i, handler_ptr) in handler_ptrs.iter().enumerate().take(num_events) {
let multiplier = (render_num + 1) as i32;
let offset = i as i32;
let effect_event = use_effect_event(move |x: i32| x * multiplier + offset);
prop_assert_eq!(
Arc::as_ptr(&effect_event.handler),
*handler_ptr,
"Render {}: Effect event {} Arc pointer should be stable",
render_num,
i
);
}
for (i, effect_event) in effect_events.iter().enumerate() {
let multiplier = (render_num + 1) as i32;
let offset = i as i32;
let expected = 10 * multiplier + offset;
prop_assert_eq!(
effect_event.call(10),
expected,
"Render {}: Effect event {} should return {} (got {})",
render_num,
i,
expected,
effect_event.call(10)
);
}
}
cleanup_test();
}
}
}