use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::fiber::{Fiber, FiberId};
thread_local! {
static FIBER_TREE: RefCell<Option<FiberTree>> = const { RefCell::new(None) };
}
pub struct FiberTree {
pub(crate) fibers: HashMap<FiberId, Fiber>,
pub(crate) root: Option<FiberId>,
pub(crate) render_stack: Vec<FiberId>,
next_id: AtomicU64,
pub(crate) pending_unmount: Vec<FiberId>,
component_id_to_fiber: HashMap<u64, FiberId>,
fiber_to_component_id: HashMap<FiberId, u64>,
seen_this_render: HashSet<FiberId>,
}
impl FiberTree {
pub fn new() -> Self {
Self {
fibers: HashMap::new(),
root: None,
render_stack: Vec::new(),
next_id: AtomicU64::new(1),
pending_unmount: Vec::new(),
component_id_to_fiber: HashMap::new(),
fiber_to_component_id: HashMap::new(),
seen_this_render: HashSet::new(),
}
}
pub fn mount(&mut self, parent: Option<FiberId>, key: Option<String>) -> FiberId {
let id = FiberId(self.next_id.fetch_add(1, Ordering::SeqCst));
let fiber = Fiber::new(id, parent, key);
self.fibers.insert(id, fiber);
if let Some(parent_id) = parent
&& let Some(parent_fiber) = self.fibers.get_mut(&parent_id)
{
parent_fiber.children.push(id);
}
if parent.is_none() && self.root.is_none() {
self.root = Some(id);
}
id
}
pub fn get_or_create_fiber_by_component_id(&mut self, component_id: u64) -> FiberId {
if let Some(&fiber_id) = self.component_id_to_fiber.get(&component_id) {
self.seen_this_render.insert(fiber_id);
fiber_id
} else {
let parent = self.current_fiber();
let fiber_id = self.mount(parent, None);
self.component_id_to_fiber.insert(component_id, fiber_id);
self.fiber_to_component_id.insert(fiber_id, component_id);
self.seen_this_render.insert(fiber_id);
fiber_id
}
}
pub fn mark_unseen_for_unmount(&mut self) {
let unseen: Vec<FiberId> = self
.fiber_to_component_id
.keys()
.filter(|fiber_id| !self.seen_this_render.contains(fiber_id))
.copied()
.collect();
for fiber_id in unseen {
self.schedule_unmount(fiber_id);
}
self.seen_this_render.clear();
}
pub fn cleanup_component_id_mapping(&mut self, fiber_id: FiberId) {
if let Some(component_id) = self.fiber_to_component_id.remove(&fiber_id) {
self.component_id_to_fiber.remove(&component_id);
}
}
pub fn begin_render(&mut self, id: FiberId) {
self.render_stack.push(id);
if let Some(fiber) = self.fibers.get_mut(&id) {
fiber.hook_index = 0; }
}
pub fn end_render(&mut self) {
if let Some(fiber_id) = self.render_stack.last().copied() {
#[cfg(debug_assertions)]
if let Some(fiber) = self.fibers.get(&fiber_id) {
fiber.check_hook_order();
}
}
self.render_stack.pop();
}
pub fn current_fiber(&self) -> Option<FiberId> {
self.render_stack.last().copied()
}
pub fn schedule_unmount(&mut self, id: FiberId) {
self.pending_unmount.push(id);
}
pub fn prepare_for_render(&mut self) {
for fiber in self.fibers.values_mut() {
fiber.reset_hook_index();
}
}
pub fn get(&self, id: FiberId) -> Option<&Fiber> {
self.fibers.get(&id)
}
pub fn get_mut(&mut self, id: FiberId) -> Option<&mut Fiber> {
self.fibers.get_mut(&id)
}
pub fn remove(&mut self, id: FiberId) -> Option<Fiber> {
if let Some(fiber) = self.fibers.get(&id)
&& let Some(parent_id) = fiber.parent
&& let Some(parent) = self.fibers.get_mut(&parent_id)
{
parent.children.retain(|&child_id| child_id != id);
}
if self.root == Some(id) {
self.root = None;
}
self.cleanup_component_id_mapping(id);
self.fibers.remove(&id)
}
pub fn dirty_fibers(&self) -> HashSet<FiberId> {
self.fibers
.iter()
.filter(|(_, fiber)| fiber.dirty)
.map(|(id, _)| *id)
.collect()
}
pub fn mark_dirty(&mut self, id: FiberId) {
if let Some(fiber) = self.fibers.get_mut(&id) {
fiber.dirty = true;
}
}
pub fn mark_clean(&mut self, id: FiberId) {
if let Some(fiber) = self.fibers.get_mut(&id) {
fiber.dirty = false;
}
}
pub fn process_unmounts(&mut self) -> Vec<FiberId> {
use crate::context_stack::pop_context_for_fiber;
use crate::scheduler::effect_queue::{queue_async_cleanup, queue_cleanup};
let unmounted: Vec<FiberId> = self.pending_unmount.drain(..).collect();
for fiber_id in &unmounted {
if let Some(fiber) = self.fibers.get_mut(fiber_id) {
let mut sync_cleanups: Vec<_> = fiber.cleanup_by_hook.drain().collect();
sync_cleanups.sort_by(|a, b| b.0.cmp(&a.0)); for (_, cleanup) in sync_cleanups {
queue_cleanup(cleanup);
}
let mut async_cleanups: Vec<_> = fiber.async_cleanup_by_hook.drain().collect();
async_cleanups.sort_by(|a, b| b.0.cmp(&a.0)); for (_, async_cleanup) in async_cleanups {
queue_async_cleanup(async_cleanup);
}
}
pop_context_for_fiber(*fiber_id);
self.remove(*fiber_id);
}
unmounted
}
pub fn has_pending_unmounts(&self) -> bool {
!self.pending_unmount.is_empty()
}
pub fn pending_unmount_count(&self) -> usize {
self.pending_unmount.len()
}
}
impl Default for FiberTree {
fn default() -> Self {
Self::new()
}
}
pub fn set_fiber_tree(tree: FiberTree) {
FIBER_TREE.with(|t| {
*t.borrow_mut() = Some(tree);
});
}
pub fn with_fiber_tree<R, F: FnOnce(&FiberTree) -> R>(f: F) -> Option<R> {
FIBER_TREE.with(|t| t.borrow().as_ref().map(f))
}
pub fn with_fiber_tree_mut<R, F: FnOnce(&mut FiberTree) -> R>(f: F) -> Option<R> {
FIBER_TREE.with(|t| t.borrow_mut().as_mut().map(f))
}
pub fn clear_fiber_tree() {
FIBER_TREE.with(|t| {
*t.borrow_mut() = None;
});
}
pub fn with_current_fiber<R, F: FnOnce(&mut Fiber) -> R>(f: F) -> Option<R> {
FIBER_TREE.with(|t| {
let mut tree = t.borrow_mut();
let tree = tree.as_mut()?;
let current_id = tree.current_fiber()?;
tree.fibers.get_mut(¤t_id).map(f)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fiber_tree_creation() {
let tree = FiberTree::new();
assert!(tree.fibers.is_empty());
assert!(tree.root.is_none());
assert!(tree.render_stack.is_empty());
}
#[test]
fn test_mount_root_fiber() {
let mut tree = FiberTree::new();
let id = tree.mount(None, None);
assert_eq!(tree.root, Some(id));
assert!(tree.fibers.contains_key(&id));
}
#[test]
fn test_mount_child_fiber() {
let mut tree = FiberTree::new();
let parent_id = tree.mount(None, None);
let child_id = tree.mount(Some(parent_id), Some("child".to_string()));
let parent = tree.get(parent_id).unwrap();
assert!(parent.children.contains(&child_id));
let child = tree.get(child_id).unwrap();
assert_eq!(child.parent, Some(parent_id));
assert_eq!(child.key, Some("child".to_string()));
}
#[test]
fn test_render_stack() {
let mut tree = FiberTree::new();
let id1 = tree.mount(None, None);
let id2 = tree.mount(Some(id1), None);
assert!(tree.current_fiber().is_none());
tree.begin_render(id1);
assert_eq!(tree.current_fiber(), Some(id1));
tree.begin_render(id2);
assert_eq!(tree.current_fiber(), Some(id2));
tree.end_render();
assert_eq!(tree.current_fiber(), Some(id1));
tree.end_render();
assert!(tree.current_fiber().is_none());
}
#[test]
fn test_schedule_unmount() {
let mut tree = FiberTree::new();
let id = tree.mount(None, None);
tree.schedule_unmount(id);
assert!(tree.pending_unmount.contains(&id));
}
#[test]
fn test_prepare_for_render() {
let mut tree = FiberTree::new();
let id = tree.mount(None, None);
tree.begin_render(id);
{
let fiber = tree.get_mut(id).unwrap();
fiber.next_hook_index();
fiber.next_hook_index();
}
tree.end_render();
assert_eq!(tree.get(id).unwrap().hook_index, 2);
tree.prepare_for_render();
assert_eq!(tree.get(id).unwrap().hook_index, 0);
}
#[test]
fn test_remove_fiber() {
let mut tree = FiberTree::new();
let parent_id = tree.mount(None, None);
let child_id = tree.mount(Some(parent_id), None);
tree.remove(child_id);
assert!(!tree.fibers.contains_key(&child_id));
let parent = tree.get(parent_id).unwrap();
assert!(!parent.children.contains(&child_id));
}
#[test]
fn test_dirty_fibers() {
let mut tree = FiberTree::new();
let id1 = tree.mount(None, None);
let id2 = tree.mount(Some(id1), None);
let dirty = tree.dirty_fibers();
assert!(dirty.contains(&id1));
assert!(dirty.contains(&id2));
tree.mark_clean(id1);
let dirty = tree.dirty_fibers();
assert!(!dirty.contains(&id1));
assert!(dirty.contains(&id2));
}
#[test]
fn test_process_unmounts_removes_fibers() {
let mut tree = FiberTree::new();
let id1 = tree.mount(None, None);
let id2 = tree.mount(Some(id1), None);
tree.schedule_unmount(id2);
assert!(tree.has_pending_unmounts());
assert_eq!(tree.pending_unmount_count(), 1);
let unmounted = tree.process_unmounts();
assert_eq!(unmounted, vec![id2]);
assert!(!tree.fibers.contains_key(&id2));
assert!(!tree.has_pending_unmounts());
}
#[test]
fn test_process_unmounts_cleans_up_context() {
use crate::context_stack::{clear_context_stack, get_context, push_context};
clear_context_stack();
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
push_context(fiber_id, "test-context".to_string());
assert_eq!(get_context::<String>(), Some("test-context".to_string()));
tree.schedule_unmount(fiber_id);
tree.process_unmounts();
assert_eq!(get_context::<String>(), None);
clear_context_stack();
}
#[test]
fn test_process_unmounts_multiple_fibers() {
let mut tree = FiberTree::new();
let id1 = tree.mount(None, None);
let id2 = tree.mount(Some(id1), None);
let id3 = tree.mount(Some(id1), None);
tree.schedule_unmount(id2);
tree.schedule_unmount(id3);
assert_eq!(tree.pending_unmount_count(), 2);
let unmounted = tree.process_unmounts();
assert_eq!(unmounted.len(), 2);
assert!(unmounted.contains(&id2));
assert!(unmounted.contains(&id3));
assert!(!tree.fibers.contains_key(&id2));
assert!(!tree.fibers.contains_key(&id3));
assert!(tree.fibers.contains_key(&id1)); }
#[test]
fn test_process_unmounts_nested_context_cleanup() {
use crate::context_stack::{clear_context_stack, get_context, push_context};
clear_context_stack();
let mut tree = FiberTree::new();
let outer_fiber = tree.mount(None, None);
let inner_fiber = tree.mount(Some(outer_fiber), None);
push_context(outer_fiber, "outer".to_string());
push_context(inner_fiber, "inner".to_string());
assert_eq!(get_context::<String>(), Some("inner".to_string()));
tree.schedule_unmount(inner_fiber);
tree.process_unmounts();
assert_eq!(get_context::<String>(), Some("outer".to_string()));
tree.schedule_unmount(outer_fiber);
tree.process_unmounts();
assert_eq!(get_context::<String>(), None);
clear_context_stack();
}
#[test]
fn test_has_pending_unmounts() {
let mut tree = FiberTree::new();
let id = tree.mount(None, None);
assert!(!tree.has_pending_unmounts());
tree.schedule_unmount(id);
assert!(tree.has_pending_unmounts());
tree.process_unmounts();
assert!(!tree.has_pending_unmounts());
}
#[test]
fn test_get_or_create_fiber_by_component_id_creates_new() {
let mut tree = FiberTree::new();
let fiber_id = tree.get_or_create_fiber_by_component_id(42);
assert!(tree.fibers.contains_key(&fiber_id));
assert!(tree.seen_this_render.contains(&fiber_id));
assert_eq!(tree.component_id_to_fiber.get(&42), Some(&fiber_id));
assert_eq!(tree.fiber_to_component_id.get(&fiber_id), Some(&42));
}
#[test]
fn test_get_or_create_fiber_by_component_id_returns_existing() {
let mut tree = FiberTree::new();
let fiber_id1 = tree.get_or_create_fiber_by_component_id(42);
let fiber_id2 = tree.get_or_create_fiber_by_component_id(42);
assert_eq!(fiber_id1, fiber_id2);
assert_eq!(tree.fibers.len(), 1);
}
#[test]
fn test_get_or_create_fiber_by_component_id_different_ids() {
let mut tree = FiberTree::new();
let fiber_id1 = tree.get_or_create_fiber_by_component_id(42);
let fiber_id2 = tree.get_or_create_fiber_by_component_id(43);
assert_ne!(fiber_id1, fiber_id2);
assert_eq!(tree.fibers.len(), 2);
}
#[test]
fn test_get_or_create_fiber_by_component_id_with_parent() {
let mut tree = FiberTree::new();
let parent_id = tree.mount(None, None);
tree.begin_render(parent_id);
let child_fiber_id = tree.get_or_create_fiber_by_component_id(100);
tree.end_render();
let child = tree.get(child_fiber_id).unwrap();
assert_eq!(child.parent, Some(parent_id));
}
#[test]
fn test_mark_unseen_for_unmount_schedules_unseen() {
let mut tree = FiberTree::new();
let fiber_id1 = tree.get_or_create_fiber_by_component_id(1);
let fiber_id2 = tree.get_or_create_fiber_by_component_id(2);
tree.seen_this_render.clear();
tree.seen_this_render.insert(fiber_id1);
tree.mark_unseen_for_unmount();
assert!(tree.pending_unmount.contains(&fiber_id2));
assert!(!tree.pending_unmount.contains(&fiber_id1));
assert!(tree.seen_this_render.is_empty());
}
#[test]
fn test_mark_unseen_for_unmount_clears_seen_set() {
let mut tree = FiberTree::new();
tree.get_or_create_fiber_by_component_id(1);
assert!(!tree.seen_this_render.is_empty());
tree.mark_unseen_for_unmount();
assert!(tree.seen_this_render.is_empty());
}
#[test]
fn test_cleanup_component_id_mapping() {
let mut tree = FiberTree::new();
let fiber_id = tree.get_or_create_fiber_by_component_id(42);
assert!(tree.component_id_to_fiber.contains_key(&42));
assert!(tree.fiber_to_component_id.contains_key(&fiber_id));
tree.cleanup_component_id_mapping(fiber_id);
assert!(!tree.component_id_to_fiber.contains_key(&42));
assert!(!tree.fiber_to_component_id.contains_key(&fiber_id));
}
#[test]
fn test_remove_cleans_up_component_id_mapping() {
let mut tree = FiberTree::new();
let fiber_id = tree.get_or_create_fiber_by_component_id(42);
tree.remove(fiber_id);
assert!(!tree.component_id_to_fiber.contains_key(&42));
assert!(!tree.fiber_to_component_id.contains_key(&fiber_id));
}
#[test]
fn test_fiber_lifecycle_full_cycle() {
let mut tree = FiberTree::new();
let fiber_id = tree.get_or_create_fiber_by_component_id(42);
tree.mark_unseen_for_unmount();
let fiber_id2 = tree.get_or_create_fiber_by_component_id(42);
assert_eq!(fiber_id, fiber_id2);
tree.mark_unseen_for_unmount();
tree.mark_unseen_for_unmount();
assert!(tree.pending_unmount.contains(&fiber_id));
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_fiber_lifecycle_management(
component_ids in prop::collection::vec(1u64..10000, 1..20),
render_passes in 1usize..5
) {
let mut tree = FiberTree::new();
let mut created_fibers: HashMap<u64, FiberId> = HashMap::new();
for _pass in 0..render_passes {
for &component_id in &component_ids {
let fiber_id = tree.get_or_create_fiber_by_component_id(component_id);
if let Some(&existing_fiber_id) = created_fibers.get(&component_id) {
prop_assert_eq!(fiber_id, existing_fiber_id,
"Component ID {} should always map to same fiber", component_id);
} else {
created_fibers.insert(component_id, fiber_id);
}
prop_assert!(tree.fibers.contains_key(&fiber_id),
"Fiber {} should exist in tree", fiber_id.0);
prop_assert!(tree.seen_this_render.contains(&fiber_id),
"Fiber {} should be marked as seen", fiber_id.0);
tree.begin_render(fiber_id);
prop_assert_eq!(tree.current_fiber(), Some(fiber_id),
"Current fiber should be {} during render", fiber_id.0);
tree.end_render();
prop_assert!(tree.current_fiber().is_none(),
"Current fiber should be None after end_render");
}
tree.mark_unseen_for_unmount();
}
}
#[test]
fn prop_unseen_fibers_scheduled_for_unmount(
initial_ids in prop::collection::vec(1u64..10000, 1..10),
surviving_ids in prop::collection::vec(1u64..10000, 0..5)
) {
let mut tree = FiberTree::new();
let mut fiber_map: HashMap<u64, FiberId> = HashMap::new();
for &id in &initial_ids {
let fiber_id = tree.get_or_create_fiber_by_component_id(id);
fiber_map.insert(id, fiber_id);
}
tree.mark_unseen_for_unmount();
for &id in &surviving_ids {
tree.get_or_create_fiber_by_component_id(id);
}
tree.mark_unseen_for_unmount();
for (&component_id, &fiber_id) in &fiber_map {
let should_survive = surviving_ids.contains(&component_id);
let is_pending_unmount = tree.pending_unmount.contains(&fiber_id);
if !should_survive {
prop_assert!(is_pending_unmount,
"Fiber for component {} should be scheduled for unmount", component_id);
}
}
}
#[test]
fn prop_component_id_mapping_consistency(
component_ids in prop::collection::vec(1u64..10000, 1..20)
) {
let mut tree = FiberTree::new();
for &component_id in &component_ids {
let fiber_id = tree.get_or_create_fiber_by_component_id(component_id);
prop_assert_eq!(
tree.component_id_to_fiber.get(&component_id),
Some(&fiber_id),
"component_id_to_fiber should map {} to {:?}", component_id, fiber_id
);
prop_assert_eq!(
tree.fiber_to_component_id.get(&fiber_id),
Some(&component_id),
"fiber_to_component_id should map {:?} to {}", fiber_id, component_id
);
}
}
}
}