use std::any::Any;
use std::any::TypeId;
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct FiberId(pub u64);
use std::future::Future;
use std::pin::Pin;
pub type CleanupFn = Box<dyn FnOnce() + Send + 'static>;
pub type AsyncCleanupFn =
Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
pub type AsyncEffectFuture = Pin<Box<dyn Future<Output = Option<AsyncCleanupFn>> + Send>>;
pub type AsyncEffectFn = Box<dyn FnOnce() -> AsyncEffectFuture + Send + 'static>;
pub struct PendingEffect {
pub effect: Box<dyn FnOnce() -> Option<CleanupFn> + 'static>,
pub hook_index: usize,
}
pub struct AsyncPendingEffect {
pub effect: AsyncEffectFn,
pub hook_index: usize,
}
pub struct Fiber {
pub id: FiberId,
pub hooks: Vec<Box<dyn Any + Send>>,
pub hook_index: usize,
pub pending_effects: Vec<PendingEffect>,
pub cleanups: Vec<CleanupFn>,
pub cleanup_by_hook: HashMap<usize, CleanupFn>,
pub async_cleanup_by_hook: HashMap<usize, AsyncCleanupFn>,
pub provided_contexts: Vec<TypeId>,
pub parent: Option<FiberId>,
pub children: Vec<FiberId>,
pub dirty: bool,
pub key: Option<String>,
#[cfg(debug_assertions)]
pub previous_hook_types: Vec<&'static str>,
#[cfg(debug_assertions)]
pub current_hook_types: Vec<&'static str>,
}
impl Fiber {
pub fn new(id: FiberId, parent: Option<FiberId>, key: Option<String>) -> Self {
Self {
id,
hooks: Vec::new(),
hook_index: 0,
pending_effects: Vec::new(),
cleanups: Vec::new(),
cleanup_by_hook: HashMap::new(),
async_cleanup_by_hook: HashMap::new(),
provided_contexts: Vec::new(),
parent,
children: Vec::new(),
dirty: true,
key,
#[cfg(debug_assertions)]
previous_hook_types: Vec::new(),
#[cfg(debug_assertions)]
current_hook_types: Vec::new(),
}
}
pub fn next_hook_index(&mut self) -> usize {
let index = self.hook_index;
self.hook_index += 1;
index
}
pub fn reset_hook_index(&mut self) {
self.hook_index = 0;
#[cfg(debug_assertions)]
{
self.previous_hook_types = std::mem::take(&mut self.current_hook_types);
}
}
#[cfg(debug_assertions)]
pub fn track_hook_call(&mut self, hook_name: &'static str) {
self.current_hook_types.push(hook_name);
}
#[cfg(not(debug_assertions))]
#[inline(always)]
pub fn track_hook_call(&mut self, _hook_name: &'static str) {
}
#[cfg(debug_assertions)]
pub fn check_hook_order(&self) {
if self.previous_hook_types.is_empty() {
return;
}
if self.current_hook_types.len() != self.previous_hook_types.len() {
tracing::warn!(
fiber_id = ?self.id,
previous_count = self.previous_hook_types.len(),
current_count = self.current_hook_types.len(),
"Hook count changed between renders. This may indicate conditional hook calls, which can lead to bugs."
);
return;
}
for (index, (current, previous)) in self
.current_hook_types
.iter()
.zip(self.previous_hook_types.iter())
.enumerate()
{
if current != previous {
tracing::warn!(
fiber_id = ?self.id,
hook_index = index,
previous_hook = previous,
current_hook = current,
"Hook order changed between renders. Hooks must be called in the same order on every render."
);
}
}
}
pub fn get_hook<T: Clone + 'static>(&self, index: usize) -> Option<T> {
self.hooks
.get(index)
.and_then(|h| h.downcast_ref::<T>())
.cloned()
}
pub fn set_hook<T: Send + 'static>(&mut self, index: usize, value: T) {
if index >= self.hooks.len() {
self.hooks.resize_with(index + 1, || Box::new(()));
}
self.hooks[index] = Box::new(value);
}
pub fn get_or_init_hook<T, F>(&mut self, index: usize, initializer: F) -> T
where
T: Clone + Send + 'static,
F: FnOnce() -> T,
{
if index >= self.hooks.len() {
self.hooks.resize_with(index + 1, || Box::new(()));
}
if self.hooks[index].downcast_ref::<T>().is_none() {
self.hooks[index] = Box::new(initializer());
}
self.hooks[index]
.downcast_ref::<T>()
.cloned()
.expect("Hook type mismatch")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fiber_creation() {
let fiber = Fiber::new(FiberId(1), None, None);
assert_eq!(fiber.id, FiberId(1));
assert_eq!(fiber.hook_index, 0);
assert!(fiber.hooks.is_empty());
assert!(fiber.parent.is_none());
assert!(fiber.children.is_empty());
assert!(fiber.dirty);
}
#[test]
fn test_fiber_with_parent_and_key() {
let fiber = Fiber::new(FiberId(2), Some(FiberId(1)), Some("my-key".to_string()));
assert_eq!(fiber.id, FiberId(2));
assert_eq!(fiber.parent, Some(FiberId(1)));
assert_eq!(fiber.key, Some("my-key".to_string()));
}
#[test]
fn test_next_hook_index() {
let mut fiber = Fiber::new(FiberId(1), None, None);
assert_eq!(fiber.next_hook_index(), 0);
assert_eq!(fiber.next_hook_index(), 1);
assert_eq!(fiber.next_hook_index(), 2);
assert_eq!(fiber.hook_index, 3);
}
#[test]
fn test_reset_hook_index() {
let mut fiber = Fiber::new(FiberId(1), None, None);
fiber.next_hook_index();
fiber.next_hook_index();
assert_eq!(fiber.hook_index, 2);
fiber.reset_hook_index();
assert_eq!(fiber.hook_index, 0);
}
#[test]
fn test_get_or_init_hook() {
let mut fiber = Fiber::new(FiberId(1), None, None);
let value: i32 = fiber.get_or_init_hook(0, || 42);
assert_eq!(value, 42);
let value: i32 = fiber.get_or_init_hook(0, || 100);
assert_eq!(value, 42);
}
#[test]
fn test_set_and_get_hook() {
let mut fiber = Fiber::new(FiberId(1), None, None);
fiber.set_hook(0, 42i32);
assert_eq!(fiber.get_hook::<i32>(0), Some(42));
fiber.set_hook(0, 100i32);
assert_eq!(fiber.get_hook::<i32>(0), Some(100));
}
#[test]
fn test_fiber_id_traits() {
let id1 = FiberId(1);
let id2 = FiberId(1);
let id3 = FiberId(2);
let id1_clone = id1;
assert_eq!(id1, id1_clone);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
let debug_str = format!("{:?}", id1);
assert!(debug_str.contains("FiberId"));
}
#[test]
fn test_pending_effect_creation() {
let effect = PendingEffect {
effect: Box::new(|| None),
hook_index: 0,
};
assert_eq!(effect.hook_index, 0);
}
#[test]
fn test_pending_effect_with_cleanup() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let cleanup_called = Arc::new(AtomicBool::new(false));
let cleanup_called_clone = cleanup_called.clone();
let effect = PendingEffect {
effect: Box::new(move || {
Some(Box::new(move || {
cleanup_called_clone.store(true, Ordering::SeqCst);
}) as CleanupFn)
}),
hook_index: 1,
};
let cleanup = (effect.effect)();
assert!(cleanup.is_some());
cleanup.unwrap()();
assert!(cleanup_called.load(Ordering::SeqCst));
}
#[test]
fn test_async_pending_effect_creation() {
let effect = AsyncPendingEffect {
effect: Box::new(|| Box::pin(async { None })),
hook_index: 0,
};
assert_eq!(effect.hook_index, 0);
}
#[tokio::test]
async fn test_async_pending_effect_execution() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let effect_ran = Arc::new(AtomicBool::new(false));
let effect_ran_clone = effect_ran.clone();
let effect = AsyncPendingEffect {
effect: Box::new(move || {
let effect_ran = effect_ran_clone.clone();
Box::pin(async move {
effect_ran.store(true, Ordering::SeqCst);
None
})
}),
hook_index: 0,
};
let cleanup = (effect.effect)().await;
assert!(effect_ran.load(Ordering::SeqCst));
assert!(cleanup.is_none());
}
#[tokio::test]
async fn test_async_pending_effect_with_async_cleanup() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let effect_ran = Arc::new(AtomicBool::new(false));
let cleanup_ran = Arc::new(AtomicBool::new(false));
let effect_ran_clone = effect_ran.clone();
let cleanup_ran_clone = cleanup_ran.clone();
let effect = AsyncPendingEffect {
effect: Box::new(move || {
let effect_ran = effect_ran_clone.clone();
let cleanup_ran = cleanup_ran_clone.clone();
Box::pin(async move {
effect_ran.store(true, Ordering::SeqCst);
Some(Box::new(move || {
let cleanup_ran = cleanup_ran.clone();
Box::pin(async move {
cleanup_ran.store(true, Ordering::SeqCst);
}) as Pin<Box<dyn Future<Output = ()> + Send>>
}) as AsyncCleanupFn)
})
}),
hook_index: 1,
};
let cleanup = (effect.effect)().await;
assert!(effect_ran.load(Ordering::SeqCst));
assert!(cleanup.is_some());
cleanup.unwrap()().await;
assert!(cleanup_ran.load(Ordering::SeqCst));
}
}
#[test]
#[cfg(debug_assertions)]
fn test_hook_order_detection() {
let mut fiber = Fiber::new(FiberId(1), None, None);
fiber.track_hook_call("use_state");
fiber.track_hook_call("use_effect");
fiber.track_hook_call("use_state");
fiber.reset_hook_index();
fiber.track_hook_call("use_state");
fiber.track_hook_call("use_effect");
fiber.track_hook_call("use_state");
fiber.check_hook_order();
assert_eq!(fiber.current_hook_types.len(), 3);
assert_eq!(fiber.previous_hook_types.len(), 3);
}
#[test]
#[cfg(debug_assertions)]
fn test_hook_count_change_detection() {
let mut fiber = Fiber::new(FiberId(1), None, None);
fiber.track_hook_call("use_state");
fiber.track_hook_call("use_effect");
fiber.reset_hook_index();
fiber.track_hook_call("use_state");
fiber.track_hook_call("use_effect");
fiber.track_hook_call("use_state");
fiber.check_hook_order();
assert_eq!(fiber.previous_hook_types.len(), 2);
assert_eq!(fiber.current_hook_types.len(), 3);
}
#[test]
#[cfg(debug_assertions)]
fn test_hook_order_change_detection() {
let mut fiber = Fiber::new(FiberId(1), None, None);
fiber.track_hook_call("use_state");
fiber.track_hook_call("use_effect");
fiber.reset_hook_index();
fiber.track_hook_call("use_effect");
fiber.track_hook_call("use_state");
fiber.check_hook_order();
assert_eq!(fiber.previous_hook_types[0], "use_state");
assert_eq!(fiber.current_hook_types[0], "use_effect");
}