use std::fmt;
use std::marker::PhantomData;
use std::sync::Arc;
use crate::fiber::FiberId;
use crate::fiber_tree::with_current_fiber;
use crate::scheduler::batch::{StateUpdate, StateUpdateKind, queue_update};
type ReducerFn<S, A> = Arc<dyn Fn(&S, A) -> S + Send + Sync>;
pub(crate) struct ReducerStorage<S> {
pub(crate) reducer: Arc<dyn std::any::Any + Send + Sync>,
pub(crate) _marker: PhantomData<S>,
}
impl<S> Clone for ReducerStorage<S> {
fn clone(&self) -> Self {
Self {
reducer: self.reducer.clone(),
_marker: PhantomData,
}
}
}
struct ReducerFnWrapper<S, A>(ReducerFn<S, A>);
pub struct Dispatch<S, A> {
pub(crate) fiber_id: FiberId,
pub(crate) hook_index: usize,
pub(crate) reducer: ReducerFn<S, A>,
pub(crate) _marker: PhantomData<(S, A)>,
}
impl<S, A> fmt::Debug for Dispatch<S, A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Dispatch")
.field("fiber_id", &self.fiber_id)
.field("hook_index", &self.hook_index)
.finish_non_exhaustive()
}
}
impl<S, A> Clone for Dispatch<S, A> {
fn clone(&self) -> Self {
Self {
fiber_id: self.fiber_id,
hook_index: self.hook_index,
reducer: self.reducer.clone(),
_marker: PhantomData,
}
}
}
impl<S: Clone + Send + 'static, A: Send + 'static> Dispatch<S, A> {
pub fn dispatch(&self, action: A) {
let reducer = self.reducer.clone();
queue_update(
self.fiber_id,
StateUpdate {
hook_index: self.hook_index,
update: StateUpdateKind::Updater(Box::new(move |any| {
let current = any
.downcast_ref::<S>()
.expect("Reducer state type mismatch");
Box::new(reducer(current, action))
})),
},
);
}
}
pub fn use_reducer<S, A, R>(reducer: R, initial_state: S) -> (S, Dispatch<S, A>)
where
S: Clone + Send + 'static,
A: Send + 'static,
R: Fn(&S, A) -> S + Send + Sync + 'static,
{
with_current_fiber(|fiber| {
let hook_index = fiber.next_hook_index();
let reducer_arc: ReducerFn<S, A> = Arc::new(reducer);
let storage = fiber.get_or_init_hook(hook_index, || ReducerStorage {
reducer: Arc::new(ReducerFnWrapper(reducer_arc.clone()))
as Arc<dyn std::any::Any + Send + Sync>,
_marker: PhantomData::<S>,
});
let reducer_fn = storage
.reducer
.downcast_ref::<ReducerFnWrapper<S, A>>()
.expect("Reducer type mismatch")
.0
.clone();
let state_hook_index = fiber.next_hook_index();
let state = fiber.get_or_init_hook(state_hook_index, || initial_state);
let dispatch = Dispatch {
fiber_id: fiber.id,
hook_index: state_hook_index,
reducer: reducer_fn,
_marker: PhantomData,
};
(state, dispatch)
})
.expect("use_reducer 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};
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();
crate::scheduler::batch::clear_state_batch();
}
#[derive(Clone, Debug, PartialEq)]
enum TestAction {
Increment,
Add(i32),
}
fn test_reducer(state: &i32, action: TestAction) -> i32 {
match action {
TestAction::Increment => state + 1,
TestAction::Add(n) => state + n,
}
}
#[test]
fn test_use_reducer_initial_value() {
let _fiber_id = setup_test_fiber();
let (state, _dispatch) = use_reducer(test_reducer, 42);
assert_eq!(state, 42);
cleanup_test();
}
#[test]
fn test_use_reducer_returns_same_value_on_rerender() {
let fiber_id = setup_test_fiber();
let (state1, _dispatch1) = use_reducer(test_reducer, 100);
assert_eq!(state1, 100);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let (state2, _dispatch2) = use_reducer(test_reducer, 999);
assert_eq!(state2, 100);
cleanup_test();
}
#[test]
fn test_dispatch_queues_update() {
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(test_reducer, 0);
dispatch.dispatch(TestAction::Increment);
let has_updates =
crate::scheduler::batch::with_state_batch(|batch| batch.has_pending_updates());
assert!(has_updates);
let is_dirty =
crate::scheduler::batch::with_state_batch(|batch| batch.is_fiber_dirty(fiber_id));
assert!(is_dirty);
cleanup_test();
}
#[test]
fn test_dispatch_applies_reducer() {
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(test_reducer, 10);
dispatch.dispatch(TestAction::Add(5));
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
let final_value = fiber.get_hook::<i32>(1);
assert_eq!(final_value, Some(15));
});
cleanup_test();
}
#[test]
fn test_multiple_dispatches_batched() {
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(test_reducer, 0);
dispatch.dispatch(TestAction::Increment);
dispatch.dispatch(TestAction::Increment);
dispatch.dispatch(TestAction::Increment);
dispatch.dispatch(TestAction::Add(10));
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty =
crate::scheduler::batch::with_state_batch_mut(|batch| batch.end_batch(tree));
assert_eq!(dirty.len(), 1);
assert!(dirty.contains(&fiber_id));
let fiber = tree.get(fiber_id).unwrap();
let final_value = fiber.get_hook::<i32>(1);
assert_eq!(final_value, Some(13));
});
cleanup_test();
}
#[test]
fn test_dispatch_is_clone() {
let _fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(test_reducer, 0);
let dispatch_clone = dispatch.clone();
let _dispatch_clone2 = dispatch_clone.clone();
cleanup_test();
}
#[test]
fn test_dispatch_stability_across_renders() {
let fiber_id = setup_test_fiber();
let (_state1, dispatch1) = use_reducer(test_reducer, 0);
let dispatch1_fiber_id = dispatch1.fiber_id;
let dispatch1_hook_index = dispatch1.hook_index;
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let (_state2, dispatch2) = use_reducer(test_reducer, 999);
assert_eq!(dispatch1_fiber_id, dispatch2.fiber_id);
assert_eq!(dispatch1_hook_index, dispatch2.hook_index);
cleanup_test();
}
#[test]
fn test_multiple_reducers() {
let _fiber_id = setup_test_fiber();
fn string_reducer(state: &String, action: &str) -> String {
format!("{}{}", state, action)
}
let (count, _dispatch_count) = use_reducer(test_reducer, 0);
let (text, _dispatch_text) = use_reducer(string_reducer, String::new());
assert_eq!(count, 0);
assert_eq!(text, "");
cleanup_test();
}
#[test]
#[should_panic(expected = "use_reducer must be called within a component render context")]
fn test_use_reducer_panics_outside_render() {
clear_fiber_tree();
crate::scheduler::batch::clear_state_batch();
let _ = use_reducer(test_reducer, 0);
}
#[test]
fn test_complex_state_reducer() {
let fiber_id = setup_test_fiber();
#[derive(Clone, Debug, PartialEq)]
struct TodoState {
todos: Vec<String>,
count: usize,
}
#[derive(Clone)]
enum TodoAction {
Add(String),
}
fn todo_reducer(state: &TodoState, action: TodoAction) -> TodoState {
match action {
TodoAction::Add(text) => {
let mut todos = state.todos.clone();
todos.push(text);
TodoState {
todos,
count: state.count + 1,
}
}
}
}
let initial = TodoState {
todos: vec![],
count: 0,
};
let (_state, dispatch) = use_reducer(todo_reducer, initial);
dispatch.dispatch(TodoAction::Add("Task 1".to_string()));
dispatch.dispatch(TodoAction::Add("Task 2".to_string()));
dispatch.dispatch(TodoAction::Add("Task 3".to_string()));
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
let final_state = fiber.get_hook::<TodoState>(1).unwrap();
assert_eq!(final_state.todos.len(), 3);
assert_eq!(final_state.count, 3);
assert_eq!(final_state.todos[0], "Task 1");
assert_eq!(final_state.todos[1], "Task 2");
assert_eq!(final_state.todos[2], "Task 3");
});
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::*;
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();
crate::scheduler::batch::clear_state_batch();
}
fn counter_reducer(state: &i32, action: i32) -> i32 {
state + action
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_reducer_applies_actions_correctly(
initial_state in any::<i32>(),
actions in prop::collection::vec(-100i32..100, 1..20)
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(counter_reducer, initial_state);
for action in &actions {
dispatch.dispatch(*action);
}
let expected = actions.iter().fold(initial_state, |acc, action| {
counter_reducer(&acc, *action)
});
let final_value = with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
fiber.get_hook::<i32>(1)
}).flatten();
prop_assert_eq!(
final_value,
Some(expected),
"Reducer should apply actions correctly: expected {}, got {:?}",
expected,
final_value
);
cleanup_test();
}
#[test]
fn prop_reducer_with_complex_actions(
initial_state in 0i32..1000,
operations in prop::collection::vec(
prop_oneof![
Just(1i32), Just(-1i32), (1i32..50).prop_map(|n| n), (-50i32..-1).prop_map(|n| n), ],
1..30
)
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(counter_reducer, initial_state);
for op in &operations {
dispatch.dispatch(*op);
}
let expected = operations.iter().fold(initial_state, |acc, op| acc + op);
let final_value = with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
fiber.get_hook::<i32>(1)
}).flatten();
prop_assert_eq!(
final_value,
Some(expected),
"Complex operations should be applied correctly"
);
cleanup_test();
}
#[test]
fn prop_reducer_batches_multiple_dispatches(
initial_state in any::<i32>(),
num_dispatches in 2usize..50
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(counter_reducer, initial_state);
for i in 0..num_dispatches {
dispatch.dispatch(i as i32);
}
let dirty_count = crate::scheduler::batch::with_state_batch(|batch| {
batch.dirty_fiber_count()
});
prop_assert_eq!(
dirty_count,
1,
"Multiple dispatches should result in only ONE dirty fiber, got {}",
dirty_count
);
let result = with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty = crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree)
});
(dirty.len(), dirty.contains(&fiber_id))
});
let (dirty_len, contains_fiber) = result.unwrap();
prop_assert_eq!(
dirty_len,
1,
"Batch should return exactly one dirty fiber, got {}",
dirty_len
);
prop_assert!(
contains_fiber,
"The dirty fiber should be our fiber"
);
cleanup_test();
}
#[test]
fn prop_multiple_reducers_batch_independently(
initial1 in any::<i32>(),
initial2 in any::<i32>(),
actions1 in prop::collection::vec(-10i32..10, 1..10),
actions2 in prop::collection::vec(-10i32..10, 1..10)
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let (_state1, dispatch1) = use_reducer(counter_reducer, initial1);
let (_state2, dispatch2) = use_reducer(counter_reducer, initial2);
for action in &actions1 {
dispatch1.dispatch(*action);
}
for action in &actions2 {
dispatch2.dispatch(*action);
}
let dirty_count = crate::scheduler::batch::with_state_batch(|batch| {
batch.dirty_fiber_count()
});
prop_assert_eq!(
dirty_count,
1,
"Multiple reducers in same fiber should still result in one dirty fiber"
);
let expected1 = actions1.iter().fold(initial1, |acc, a| acc + a);
let expected2 = actions2.iter().fold(initial2, |acc, a| acc + a);
let result = with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
(fiber.get_hook::<i32>(1), fiber.get_hook::<i32>(3))
});
let (final1, final2) = result.unwrap();
prop_assert_eq!(final1, Some(expected1), "First reducer state should be correct");
prop_assert_eq!(final2, Some(expected2), "Second reducer state should be correct");
cleanup_test();
}
#[test]
fn prop_dispatch_function_stability(
initial_state in any::<i32>(),
num_renders in 2usize..10
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let (_state1, dispatch1) = use_reducer(counter_reducer, initial_state);
let dispatch1_fiber_id = dispatch1.fiber_id;
let dispatch1_hook_index = dispatch1.hook_index;
for render_num in 1..num_renders {
with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let (_state, dispatch) = use_reducer(counter_reducer, 999999);
prop_assert_eq!(
dispatch.fiber_id,
dispatch1_fiber_id,
"Render {}: dispatch fiber_id should be stable",
render_num
);
prop_assert_eq!(
dispatch.hook_index,
dispatch1_hook_index,
"Render {}: dispatch hook_index should be stable",
render_num
);
}
cleanup_test();
}
#[test]
fn prop_dispatch_works_after_rerender(
initial_state in any::<i32>(),
actions_before in prop::collection::vec(-10i32..10, 1..5),
actions_after in prop::collection::vec(-10i32..10, 1..5)
) {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let (_state, dispatch) = use_reducer(counter_reducer, initial_state);
for action in &actions_before {
dispatch.dispatch(*action);
}
with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
});
let intermediate = actions_before.iter().fold(initial_state, |acc, a| acc + a);
with_fiber_tree_mut(|tree| {
tree.begin_render(fiber_id);
});
let (state_after_rerender, dispatch_after) = use_reducer(counter_reducer, 999999);
prop_assert_eq!(
state_after_rerender,
intermediate,
"State after re-render should reflect previous dispatches"
);
for action in &actions_after {
dispatch_after.dispatch(*action);
}
let expected = actions_after.iter().fold(intermediate, |acc, a| acc + a);
let final_value = with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
fiber.get_hook::<i32>(1)
}).flatten();
prop_assert_eq!(
final_value,
Some(expected),
"Dispatch should work correctly after re-render"
);
cleanup_test();
}
}
}