use std::cell::RefCell;
use crate::fiber::FiberId;
thread_local! {
static STRICT_MODE_ENABLED: RefCell<bool> = const { RefCell::new(false) };
}
pub fn is_strict_mode_enabled() -> bool {
#[cfg(debug_assertions)]
{
STRICT_MODE_ENABLED.with(|enabled| *enabled.borrow())
}
#[cfg(not(debug_assertions))]
{
false
}
}
pub fn set_strict_mode_enabled(enabled: bool) {
#[cfg(debug_assertions)]
{
STRICT_MODE_ENABLED.with(|e| {
*e.borrow_mut() = enabled;
});
}
#[cfg(not(debug_assertions))]
{
let _ = enabled; }
}
#[cfg(debug_assertions)]
pub struct StrictMode {
pub enabled: bool,
}
#[cfg(debug_assertions)]
impl StrictMode {
pub fn new(enabled: bool) -> Self {
Self { enabled }
}
pub fn wrap_render<F, R>(&self, fiber_id: FiberId, component_fn: F) -> R
where
F: Fn() -> R,
{
if self.enabled {
let _result1 = component_fn();
crate::fiber_tree::with_fiber_tree_mut(|tree| {
if let Some(fiber) = tree.get_mut(fiber_id) {
fiber.reset_hook_index();
}
});
component_fn()
} else {
component_fn()
}
}
pub fn wrap_render_with_diff<F, R>(&self, fiber_id: FiberId, component_fn: F) -> R
where
F: Fn() -> R,
R: PartialEq + std::fmt::Debug,
{
if self.enabled {
let result1 = component_fn();
crate::fiber_tree::with_fiber_tree_mut(|tree| {
if let Some(fiber) = tree.get_mut(fiber_id) {
fiber.reset_hook_index();
}
});
let result2 = component_fn();
if result1 != result2 {
tracing::warn!(
"Strict mode: Component rendered different results! \
This indicates an impure render. \
First: {:?}, Second: {:?}",
result1,
result2
);
}
result2
} else {
component_fn()
}
}
pub fn wrap_effect_on_mount<F, C>(&self, is_mount: bool, effect: F) -> Option<C>
where
F: Fn() -> Option<C>,
C: FnOnce(),
{
if self.enabled && is_mount {
let cleanup1 = effect();
if let Some(cleanup) = cleanup1 {
cleanup();
}
effect()
} else {
effect()
}
}
}
#[cfg(debug_assertions)]
impl Default for StrictMode {
fn default() -> Self {
Self::new(false)
}
}
#[cfg(not(debug_assertions))]
pub struct StrictMode;
#[cfg(not(debug_assertions))]
impl StrictMode {
pub fn new(_enabled: bool) -> Self {
Self
}
pub fn wrap_render<F, R>(&self, _fiber_id: FiberId, component_fn: F) -> R
where
F: Fn() -> R,
{
component_fn()
}
pub fn wrap_render_with_diff<F, R>(&self, _fiber_id: FiberId, component_fn: F) -> R
where
F: Fn() -> R,
R: PartialEq + std::fmt::Debug,
{
component_fn()
}
pub fn wrap_effect_on_mount<F, C>(&self, _is_mount: bool, effect: F) -> Option<C>
where
F: Fn() -> Option<C>,
C: FnOnce(),
{
effect()
}
}
#[cfg(not(debug_assertions))]
impl Default for StrictMode {
fn default() -> Self {
Self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, 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_strict_mode_creation() {
let strict_mode = StrictMode::new(true);
#[cfg(debug_assertions)]
assert!(strict_mode.enabled);
let strict_mode_disabled = StrictMode::new(false);
#[cfg(debug_assertions)]
assert!(!strict_mode_disabled.enabled);
}
#[test]
fn test_strict_mode_default() {
let strict_mode = StrictMode::default();
#[cfg(debug_assertions)]
assert!(!strict_mode.enabled);
}
#[test]
fn test_wrap_render_disabled() {
let fiber_id = setup_test_fiber();
let strict_mode = StrictMode::new(false);
let render_count = Arc::new(AtomicUsize::new(0));
let render_count_clone = render_count.clone();
let result = strict_mode.wrap_render(fiber_id, || {
render_count_clone.fetch_add(1, Ordering::SeqCst);
42
});
assert_eq!(result, 42);
assert_eq!(render_count.load(Ordering::SeqCst), 1);
cleanup_test();
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_render_enabled_double_renders() {
let fiber_id = setup_test_fiber();
let strict_mode = StrictMode::new(true);
let render_count = Arc::new(AtomicUsize::new(0));
let render_count_clone = render_count.clone();
let result = strict_mode.wrap_render(fiber_id, || {
render_count_clone.fetch_add(1, Ordering::SeqCst);
42
});
assert_eq!(result, 42);
assert_eq!(render_count.load(Ordering::SeqCst), 2);
cleanup_test();
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_render_resets_hook_index() {
let fiber_id = setup_test_fiber();
let strict_mode = StrictMode::new(true);
let hook_indices = Arc::new(std::sync::Mutex::new(Vec::new()));
let hook_indices_clone = hook_indices.clone();
strict_mode.wrap_render(fiber_id, || {
let index = crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.get(fiber_id).map(|f| f.hook_index).unwrap_or(999)
})
.unwrap_or(999);
hook_indices_clone.lock().unwrap().push(index);
});
let indices = hook_indices.lock().unwrap();
assert_eq!(indices.len(), 2);
assert_eq!(indices[0], 0);
assert_eq!(indices[1], 0);
cleanup_test();
}
#[test]
fn test_wrap_render_with_diff_disabled() {
let fiber_id = setup_test_fiber();
let strict_mode = StrictMode::new(false);
let render_count = Arc::new(AtomicUsize::new(0));
let render_count_clone = render_count.clone();
let result = strict_mode.wrap_render_with_diff(fiber_id, || {
render_count_clone.fetch_add(1, Ordering::SeqCst);
42
});
assert_eq!(result, 42);
assert_eq!(render_count.load(Ordering::SeqCst), 1);
cleanup_test();
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_render_with_diff_same_results() {
let fiber_id = setup_test_fiber();
let strict_mode = StrictMode::new(true);
let result = strict_mode.wrap_render_with_diff(fiber_id, || 42);
assert_eq!(result, 42);
cleanup_test();
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_render_with_diff_different_results() {
let fiber_id = setup_test_fiber();
let strict_mode = StrictMode::new(true);
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let result = strict_mode
.wrap_render_with_diff(fiber_id, || counter_clone.fetch_add(1, Ordering::SeqCst));
assert_eq!(result, 1);
cleanup_test();
}
#[test]
fn test_wrap_effect_on_mount_disabled() {
let strict_mode = StrictMode::new(false);
let effect_count = Arc::new(AtomicUsize::new(0));
let cleanup_count = Arc::new(AtomicUsize::new(0));
let effect_count_clone = effect_count.clone();
let cleanup_count_clone = cleanup_count.clone();
let cleanup = strict_mode.wrap_effect_on_mount(true, || {
effect_count_clone.fetch_add(1, Ordering::SeqCst);
let cleanup_count = cleanup_count_clone.clone();
Some(move || {
cleanup_count.fetch_add(1, Ordering::SeqCst);
})
});
assert_eq!(effect_count.load(Ordering::SeqCst), 1);
assert_eq!(cleanup_count.load(Ordering::SeqCst), 0);
assert!(cleanup.is_some());
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_effect_on_mount_enabled() {
let strict_mode = StrictMode::new(true);
let effect_count = Arc::new(AtomicUsize::new(0));
let cleanup_count = Arc::new(AtomicUsize::new(0));
let effect_count_clone = effect_count.clone();
let cleanup_count_clone = cleanup_count.clone();
let cleanup = strict_mode.wrap_effect_on_mount(true, || {
effect_count_clone.fetch_add(1, Ordering::SeqCst);
let cleanup_count = cleanup_count_clone.clone();
Some(move || {
cleanup_count.fetch_add(1, Ordering::SeqCst);
})
});
assert_eq!(effect_count.load(Ordering::SeqCst), 2);
assert_eq!(cleanup_count.load(Ordering::SeqCst), 1);
assert!(cleanup.is_some());
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_effect_on_mount_not_mount() {
let strict_mode = StrictMode::new(true);
let effect_count = Arc::new(AtomicUsize::new(0));
let effect_count_clone = effect_count.clone();
let _cleanup = strict_mode.wrap_effect_on_mount(false, || {
effect_count_clone.fetch_add(1, Ordering::SeqCst);
Option::<fn()>::None
});
assert_eq!(effect_count.load(Ordering::SeqCst), 1);
}
#[test]
fn test_strict_mode_global_flag() {
assert!(!is_strict_mode_enabled());
set_strict_mode_enabled(true);
#[cfg(debug_assertions)]
assert!(is_strict_mode_enabled());
set_strict_mode_enabled(false);
assert!(!is_strict_mode_enabled());
}
#[cfg(debug_assertions)]
#[test]
fn test_wrap_effect_no_cleanup() {
let strict_mode = StrictMode::new(true);
let effect_count = Arc::new(AtomicUsize::new(0));
let effect_count_clone = effect_count.clone();
let cleanup = strict_mode.wrap_effect_on_mount(true, || {
effect_count_clone.fetch_add(1, Ordering::SeqCst);
Option::<fn()>::None
});
assert_eq!(effect_count.load(Ordering::SeqCst), 2);
assert!(cleanup.is_none());
}
}