Skip to main content

cranpose_core/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code)]
3
4pub extern crate self as cranpose_core;
5
6mod callbacks;
7mod composer;
8pub mod composer_context;
9mod composition;
10mod composition_locals;
11pub mod concurrency;
12mod debug_trace;
13mod effect_key;
14mod emit;
15pub mod env_flags;
16#[cfg(any(feature = "internal", test))]
17mod frame_clock;
18mod hooks;
19mod launched_effect;
20pub mod owned;
21pub mod platform;
22mod recompose;
23mod retention;
24pub mod runtime;
25mod slot;
26pub mod snapshot_double_index_heap;
27pub mod snapshot_id_set;
28pub mod snapshot_pinning;
29pub mod snapshot_state_observer;
30pub mod snapshot_v2;
31mod snapshot_weak_set;
32mod state;
33pub mod subcompose;
34
35#[cfg(feature = "internal")]
36#[doc(hidden)]
37pub mod internal {
38    pub use crate::frame_clock::{FrameCallbackRegistration, FrameClock};
39}
40pub use callbacks::{CallbackHolder, CallbackHolder1, ParamSlot, ParamState, ReturnSlot};
41pub use composer::{BranchGroupGuard, CapturedCompositionContext, Composer, ValueSlotHandle};
42pub(crate) use composer::{ComposerCore, EmittedNode, ParentAttachMode, ParentFrame};
43pub use composition::{Composition, ROOT_RENDER_REPLAY_LIMIT};
44pub use composition_locals::{
45    CompositionLocal, CompositionLocalProvider, ProvidedValue, StaticCompositionLocal,
46    compositionLocalOf, compositionLocalOfWithPolicy, staticCompositionLocalOf,
47};
48pub(crate) use composition_locals::{LocalStateEntry, StaticLocalEntry};
49pub use concurrency::{
50    CollectEvents, CoroutineScope, Delay, EventChannel, EventSender, EventStream, EventStreamNext,
51    ProduceScope, collectAsState, delay, interval, launchBlocking, produceState,
52    rememberCoroutineScope, rememberEventStream, spawn_ui_task, withBlocking,
53};
54#[doc(hidden)]
55pub use debug_trace::{
56    debug_label_current_scope, debug_live_recompose_scope_count,
57    debug_recompose_scope_registry_stats, debug_scope_invalidation_sources, debug_scope_label,
58};
59pub use hooks::{
60    derivedStateOf, mutableStateList, mutableStateListOf, mutableStateMap, mutableStateMapOf,
61    mutableStateOf, mutableStateOfNeverEqual, ownedMutableStateOf, ownedMutableStateOfNeverEqual,
62    remember, rememberKeyed, rememberMutableStateOf, rememberMutableStateOfNeverEqual,
63    rememberUpdatedState, try_mutableStateOf,
64};
65#[cfg(feature = "internal")]
66#[doc(hidden)]
67pub use hooks::{withFrameMillis, withFrameNanos};
68pub use launched_effect::{
69    __launched_effect_async_impl, __launched_effect_impl, CancelToken, LaunchedEffect,
70    LaunchedEffectAsync, LaunchedEffectScope, TaskSite,
71};
72pub use owned::Owned;
73pub use platform::{Clock, RuntimeScheduler, SchedulerRef, scheduler_ref};
74pub use retention::{RetentionBudget, RetentionEvictionPolicy, RetentionMode, RetentionPolicy};
75#[doc(hidden)]
76pub use runtime::{
77    DefaultScheduler, Runtime, RuntimeHandle, StateId, TaskHandle, UiDispatcher,
78    current_runtime_handle, label_next_ui_task, schedule_frame, schedule_node_update,
79};
80pub use slot::{
81    SlotDebugAnchor, SlotDebugEntry, SlotDebugEntryKind, SlotDebugGroup, SlotDebugScope,
82    SlotDebugSnapshot, SlotRetentionDebugStats, SlotTable, SlotTableDebugStats,
83    SlotTableLocalDebugStats, SlotTableMutationDebugStats,
84};
85#[doc(hidden)]
86pub use snapshot_state_observer::SnapshotStateObserver;
87
88/// Runs the provided closure inside a mutable snapshot and applies the result.
89///
90/// Use this function when you need to update `MutableState` from outside the
91/// composition or layout phase, typically in event handlers or async tasks.
92///
93/// # Why is this needed?
94/// Cranpose uses a snapshot system (MVCC) to isolate state changes. Modifications
95/// made to `MutableState` are only visible to the current thread's active snapshot.
96/// To make changes visible to the rest of the system (and trigger recomposition),
97/// they must be "applied" by committing the snapshot. This helper handles that
98/// lifecycle for you.
99///
100/// # Example
101///
102/// ```ignore
103/// // Inside a button click handler
104/// run_in_mutable_snapshot(|| {
105///     count.set(count.value() + 1);
106/// });
107/// ```
108///
109/// # Important
110/// ALL UI event handlers (keyboard, mouse, touch, animations) that modify state
111/// MUST use this function or [`dispatch_ui_event`].
112pub fn run_in_mutable_snapshot<T>(block: impl FnOnce() -> T) -> Result<T, &'static str> {
113    let snapshot = snapshot_v2::take_mutable_snapshot(None, None);
114
115    let _applied_guard = AppliedSnapshotFlagGuard::enter();
116    let value = snapshot.enter(block);
117
118    match snapshot.apply() {
119        snapshot_v2::SnapshotApplyResult::Success => Ok(value),
120        snapshot_v2::SnapshotApplyResult::Failure => Err("Snapshot apply failed"),
121    }
122}
123
124struct AppliedSnapshotFlagGuard {
125    previous: bool,
126}
127
128impl AppliedSnapshotFlagGuard {
129    fn enter() -> Self {
130        let previous = IN_APPLIED_SNAPSHOT.with(|flag| {
131            let previous = flag.get();
132            flag.set(true);
133            previous
134        });
135        Self { previous }
136    }
137}
138
139impl Drop for AppliedSnapshotFlagGuard {
140    fn drop(&mut self) {
141        IN_APPLIED_SNAPSHOT.with(|flag| flag.set(self.previous));
142    }
143}
144
145/// Dispatches a UI event in a proper snapshot context.
146///
147/// This is a convenience wrapper around [`run_in_mutable_snapshot`] that returns
148/// `Option<T>` instead of `Result<T, &str>`.
149///
150/// # Example
151/// ```ignore
152/// // In a keyboard event handler:
153/// dispatch_ui_event(|| {
154///     text_field_state.edit(|buffer| {
155///         buffer.insert("a");
156///     });
157/// });
158/// ```
159pub fn dispatch_ui_event<T>(block: impl FnOnce() -> T) -> Option<T> {
160    run_in_mutable_snapshot(block).ok()
161}
162
163thread_local! {
164    pub(crate) static IN_EVENT_HANDLER: Cell<bool> = const { Cell::new(false) };
165    pub(crate) static IN_APPLIED_SNAPSHOT: Cell<bool> = const { Cell::new(false) };
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
169pub struct CompositionPassDebugStats {
170    pub commands_len: usize,
171    pub commands_cap: usize,
172    pub command_payload_len_bytes: usize,
173    pub command_payload_cap_bytes: usize,
174    pub sync_children_len: usize,
175    pub sync_children_cap: usize,
176    pub sync_child_ids_len: usize,
177    pub sync_child_ids_cap: usize,
178    pub side_effects_len: usize,
179    pub side_effects_cap: usize,
180}
181
182#[must_use]
183pub struct EventHandlerScopeGuard {
184    previous: bool,
185}
186
187impl Drop for EventHandlerScopeGuard {
188    fn drop(&mut self) {
189        IN_EVENT_HANDLER.with(|flag| flag.set(self.previous));
190    }
191}
192
193pub fn enter_event_handler_scope() -> EventHandlerScopeGuard {
194    let previous = IN_EVENT_HANDLER.with(|flag| {
195        let previous = flag.get();
196        flag.set(true);
197        previous
198    });
199    EventHandlerScopeGuard { previous }
200}
201
202/// Returns true if currently in an event handler context.
203pub fn in_event_handler() -> bool {
204    IN_EVENT_HANDLER.with(|c| c.get())
205}
206
207/// Returns true if currently in an applied snapshot context.
208pub fn in_applied_snapshot() -> bool {
209    IN_APPLIED_SNAPSHOT.with(|c| c.get())
210}
211
212use std::{
213    any::{Any, TypeId},
214    cell::{Cell, Ref, RefCell, RefMut},
215    cmp::Reverse,
216    collections::BinaryHeap,
217    hash::{Hash, Hasher},
218    ops::{Deref, DerefMut},
219    rc::{Rc, Weak},
220    sync::OnceLock,
221};
222
223#[cfg(test)]
224pub use runtime::{TestRuntime, TestScheduler};
225use smallvec::SmallVec;
226
227use crate::collections::map::{HashMap, HashSet};
228
229pub type Key = u64;
230pub type NodeId = usize;
231
232#[cfg(any(test, debug_assertions))]
233#[derive(Clone, Debug, PartialEq, Eq)]
234struct LocationKeyDebugInfo {
235    file: String,
236    line: u32,
237    column: u32,
238}
239
240#[cfg(any(test, debug_assertions))]
241thread_local! {
242    static LOCATION_KEY_REGISTRY: RefCell<HashMap<Key, LocationKeyDebugInfo>> =
243        RefCell::new(HashMap::default());
244    static LOCATION_KEY_COLLISION_COUNT: Cell<usize> = const { Cell::new(0) };
245}
246
247#[cfg(any(test, debug_assertions))]
248fn register_location_key_debug_info(key: Key, file: &str, line: u32, column: u32) {
249    let info = LocationKeyDebugInfo {
250        file: file.to_owned(),
251        line,
252        column,
253    };
254    let collision = LOCATION_KEY_REGISTRY.with(|registry| {
255        let mut registry = registry.borrow_mut();
256        match registry.entry(key) {
257            std::collections::hash_map::Entry::Vacant(entry) => {
258                entry.insert(info);
259                None
260            }
261            std::collections::hash_map::Entry::Occupied(entry) => {
262                let existing = entry.get();
263                (existing != &info).then(|| (existing.clone(), info))
264            }
265        }
266    });
267    if let Some((existing, incoming)) = collision {
268        LOCATION_KEY_COLLISION_COUNT.with(|count| {
269            count.set(count.get().saturating_add(1));
270        });
271        log::error!("location key collision: key={key} first={existing:?} second={incoming:?}");
272    }
273}
274
275#[cfg(all(debug_assertions, not(test)))]
276fn location_key_diagnostics_enabled() -> bool {
277    crate::env_flag!("CRANPOSE_LOCATION_KEY_DIAGNOSTICS")
278}
279
280#[cfg(test)]
281pub(crate) fn register_location_key_debug_info_for_test(
282    key: Key,
283    file: &str,
284    line: u32,
285    column: u32,
286) {
287    register_location_key_debug_info(key, file, line, column);
288}
289
290#[cfg(test)]
291pub(crate) fn location_key_debug_collision_count_for_test() -> usize {
292    LOCATION_KEY_COLLISION_COUNT.with(Cell::get)
293}
294
295#[cfg(test)]
296pub(crate) fn location_key_debug_info_for_test(key: Key) -> Option<LocationKeyDebugInfo> {
297    LOCATION_KEY_REGISTRY.with(|registry| registry.borrow().get(&key).cloned())
298}
299
300#[cfg(test)]
301pub(crate) fn slot_validation_diagnostics_enabled() -> bool {
302    true
303}
304
305#[cfg(all(debug_assertions, not(test)))]
306pub(crate) fn slot_validation_diagnostics_enabled() -> bool {
307    crate::env_flag!("CRANPOSE_VALIDATE_SLOTS")
308}
309
310fn source_location_key(file: &str, line: u32, column: u32) -> Key {
311    avalanche_location_key(source_location_hash(file, line, column))
312}
313
314fn source_location_hash(file: &str, line: u32, column: u32) -> u64 {
315    let mut hash = 0xcbf2_9ce4_8422_2325u64;
316    hash = fnv1a_location_key_bytes(hash, file.as_bytes());
317    hash = fnv1a_location_key_bytes(hash, &[0xff]);
318    hash = fnv1a_location_key_bytes(hash, &line.to_le_bytes());
319    hash = fnv1a_location_key_bytes(hash, &[0xfe]);
320    hash = fnv1a_location_key_bytes(hash, &column.to_le_bytes());
321    hash
322}
323
324fn fnv1a_location_key_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
325    for byte in bytes {
326        hash ^= u64::from(*byte);
327        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
328    }
329    hash
330}
331
332fn avalanche_location_key(mut value: u64) -> u64 {
333    value ^= value >> 33;
334    value = value.wrapping_mul(0xff51_afd7_ed55_8ccd);
335    value ^= value >> 33;
336    value = value.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
337    value ^ (value >> 33)
338}
339
340#[doc(hidden)]
341#[track_caller]
342pub fn caller_location_key() -> Key {
343    let caller = std::panic::Location::caller();
344    location_key(caller.file(), caller.line(), caller.column())
345}
346
347#[doc(hidden)]
348#[track_caller]
349pub fn composable_identity_key(definition: Key) -> Key {
350    (definition.wrapping_mul(0x0000_0100_0000_01b3) ^ caller_location_key())
351        .wrapping_mul(0x0000_0100_0000_01b3)
352}
353
354#[doc(hidden)]
355pub fn composable_definition_key(
356    file: &str,
357    line: u32,
358    column: u32,
359    marker: std::any::TypeId,
360) -> Key {
361    let mut hasher = std::collections::hash_map::DefaultHasher::new();
362    std::hash::Hash::hash(&marker, &mut hasher);
363    location_key(file, line, column) ^ avalanche_location_key(std::hash::Hasher::finish(&hasher))
364}
365
366#[doc(hidden)]
367pub fn cached_composable_definition_key(
368    cell: &OnceLock<Key>,
369    file: &str,
370    line: u32,
371    column: u32,
372    marker: TypeId,
373) -> Key {
374    *cell.get_or_init(|| composable_definition_key(file, line, column, marker))
375}
376
377pub fn location_key(file: &str, line: u32, column: u32) -> Key {
378    let key = source_location_key(file, line, column);
379    #[cfg(test)]
380    register_location_key_debug_info(key, file, line, column);
381    #[cfg(all(debug_assertions, not(test)))]
382    if location_key_diagnostics_enabled() {
383        register_location_key_debug_info(key, file, line, column);
384    }
385    key
386}
387
388#[doc(hidden)]
389pub fn __branch_group_scope_deferred(key: Key) -> Option<BranchGroupGuard> {
390    with_current_composer_opt(|composer| composer.__branch_group_deferred(key))
391}
392
393#[doc(hidden)]
394pub fn branch_location_key(file: &str, line: u32, column: u32, branch: u32) -> Key {
395    let mut hash = source_location_hash(file, line, column);
396    hash = fnv1a_location_key_bytes(hash, &[0xfd]);
397    hash = fnv1a_location_key_bytes(hash, &branch.to_le_bytes());
398    let key = avalanche_location_key(hash);
399    #[cfg(test)]
400    register_location_key_debug_info(key, file, line, column);
401    #[cfg(all(debug_assertions, not(test)))]
402    if location_key_diagnostics_enabled() {
403        register_location_key_debug_info(key, file, line, column);
404    }
405    key
406}
407
408#[doc(hidden)]
409pub fn cached_branch_location_key(
410    cell: &OnceLock<Key>,
411    file: &str,
412    line: u32,
413    column: u32,
414    branch: u32,
415) -> Key {
416    *cell.get_or_init(|| branch_location_key(file, line, column, branch))
417}
418
419/// Stable identifier for a slot in the slot table.
420///
421/// Anchors provide positional stability: they maintain their identity even when
422/// the slot table is reorganized (e.g., during conditional rendering or group moves).
423/// This prevents effect states from being prematurely removed during recomposition.
424#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
425pub struct AnchorId {
426    id: u32,
427    generation: u32,
428}
429
430impl AnchorId {
431    pub(crate) const INVALID: AnchorId = AnchorId {
432        id: 0,
433        generation: 0,
434    };
435
436    pub(crate) fn new(id: usize) -> Self {
437        Self {
438            id: crate::slot::checked_usize_to_u32(id, "anchor id"),
439            generation: 1,
440        }
441    }
442
443    /// Check if this anchor is valid (non-zero).
444    pub fn is_valid(&self) -> bool {
445        self.id != 0
446    }
447}
448
449pub(crate) type ScopeId = usize;
450pub(crate) type FrameCallbackId = u64;
451type LocalStackSnapshot = Rc<Vec<composer::LocalContext>>;
452
453#[derive(Clone)]
454pub(crate) struct LocalKey(Rc<()>);
455
456impl LocalKey {
457    fn new() -> Self {
458        Self(Rc::new(()))
459    }
460
461    pub(crate) fn entry_source(&self) -> Key {
462        avalanche_location_key(Rc::as_ptr(&self.0) as usize as u64)
463    }
464}
465
466impl std::fmt::Debug for LocalKey {
467    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468        f.debug_tuple("LocalKey")
469            .field(&(Rc::as_ptr(&self.0) as usize))
470            .finish()
471    }
472}
473
474impl PartialEq for LocalKey {
475    fn eq(&self, other: &Self) -> bool {
476        Rc::ptr_eq(&self.0, &other.0)
477    }
478}
479
480impl Eq for LocalKey {}
481
482impl Hash for LocalKey {
483    fn hash<H: Hasher>(&self, state: &mut H) {
484        Rc::as_ptr(&self.0).hash(state);
485    }
486}
487
488thread_local! {
489    static EMPTY_LOCAL_STACK: LocalStackSnapshot = Rc::new(Vec::new());
490    #[cfg(debug_assertions)]
491    static DEBUG_SCOPE_LABELS: RefCell<HashMap<usize, &'static str>> = RefCell::new(HashMap::default());
492    #[cfg(debug_assertions)]
493    static DEBUG_SCOPE_INVALIDATION_SOURCES: RefCell<HashMap<usize, HashSet<String>>> =
494        RefCell::new(HashMap::default());
495    #[cfg(all(test, debug_assertions))]
496    static DEBUG_SCOPE_TRACKING_OVERRIDE: Cell<Option<bool>> = const { Cell::new(None) };
497}
498
499fn empty_local_stack() -> LocalStackSnapshot {
500    EMPTY_LOCAL_STACK.with(Rc::clone)
501}
502
503enum RecomposeCallback {
504    Static(fn(&Composer)),
505    Dynamic(Box<dyn FnMut(&Composer) + 'static>),
506}
507
508pub(crate) struct RecomposeScopeInner {
509    runtime: RuntimeHandle,
510    invalid: Cell<bool>,
511    enqueued: Cell<bool>,
512    active: Cell<bool>,
513    deactivations: Cell<u64>,
514    composed_once: Cell<bool>,
515    pending_recompose: Cell<bool>,
516    force_reuse: Cell<bool>,
517    force_recompose: Cell<bool>,
518    retention_mode: Cell<RetentionMode>,
519    parent_hint: Cell<Option<NodeId>>,
520    recompose: RefCell<Option<RecomposeCallback>>,
521    parent_scope: RefCell<Option<Weak<RecomposeScopeInner>>>,
522    lifetime_owner_scope: RefCell<Option<Weak<RecomposeScopeInner>>>,
523    local_stack: RefCell<LocalStackSnapshot>,
524    slots_storage_key: Cell<usize>,
525    slots_runtime_state: RefCell<Option<std::rc::Weak<crate::composer::ComposerRuntimeState>>>,
526    state_subscriptions: RefCell<HashSet<StateId>>,
527    invalidation_sources: RefCell<Option<HashSet<StateId>>>,
528}
529
530impl RecomposeScopeInner {
531    fn new(runtime: RuntimeHandle) -> Self {
532        runtime.increment_live_recompose_scope_count();
533        Self {
534            runtime,
535            invalid: Cell::new(false),
536            enqueued: Cell::new(false),
537            active: Cell::new(true),
538            deactivations: Cell::new(0),
539            composed_once: Cell::new(false),
540            pending_recompose: Cell::new(false),
541            force_reuse: Cell::new(false),
542            force_recompose: Cell::new(false),
543            retention_mode: Cell::new(RetentionMode::DisposeWhenInactive),
544            parent_hint: Cell::new(None),
545            recompose: RefCell::new(None),
546            parent_scope: RefCell::new(None),
547            lifetime_owner_scope: RefCell::new(None),
548            local_stack: RefCell::new(empty_local_stack()),
549            slots_storage_key: Cell::new(0),
550            slots_runtime_state: RefCell::new(None),
551            state_subscriptions: RefCell::new(HashSet::default()),
552            invalidation_sources: RefCell::new(Some(HashSet::default())),
553        }
554    }
555
556    fn id(&self) -> ScopeId {
557        std::ptr::from_ref(self).addr()
558    }
559}
560
561impl Drop for RecomposeScopeInner {
562    fn drop(&mut self) {
563        let id = self.id();
564        self.runtime.decrement_live_recompose_scope_count();
565        let subscriptions = std::mem::take(self.state_subscriptions.get_mut());
566        for state_id in subscriptions {
567            self.runtime.unregister_state_scope(state_id, id);
568        }
569        #[cfg(debug_assertions)]
570        {
571            let _ = DEBUG_SCOPE_LABELS.try_with(|labels| {
572                labels.borrow_mut().remove(&id);
573            });
574            let _ = DEBUG_SCOPE_INVALIDATION_SOURCES.try_with(|sources| {
575                sources.borrow_mut().remove(&id);
576            });
577        }
578        if self.enqueued.replace(false) {
579            self.runtime.mark_scope_recomposed(id);
580        }
581    }
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
585pub struct RecomposeScopeRegistryDebugStats {
586    pub len: usize,
587    pub capacity: usize,
588}
589
590#[derive(Clone)]
591pub struct RecomposeScope {
592    inner: Rc<RecomposeScopeInner>,
593}
594
595impl PartialEq for RecomposeScope {
596    fn eq(&self, other: &Self) -> bool {
597        Rc::ptr_eq(&self.inner, &other.inner)
598    }
599}
600
601impl Eq for RecomposeScope {}
602
603impl Hash for RecomposeScope {
604    fn hash<H: Hasher>(&self, state: &mut H) {
605        self.id().hash(state);
606    }
607}
608
609impl RecomposeScope {
610    fn new(runtime: RuntimeHandle) -> Self {
611        Self {
612            inner: Rc::new(RecomposeScopeInner::new(runtime)),
613        }
614    }
615
616    pub(crate) fn downgrade(&self) -> Weak<RecomposeScopeInner> {
617        Rc::downgrade(&self.inner)
618    }
619
620    pub fn id(&self) -> ScopeId {
621        self.inner.id()
622    }
623
624    pub fn is_invalid(&self) -> bool {
625        self.inner.invalid.get()
626    }
627
628    pub fn is_active(&self) -> bool {
629        self.inner.active.get()
630    }
631
632    /// Total deactivations along this scope's owner chain. A retained slot
633    /// composition records this at compose time; a later mismatch means some
634    /// enclosing composition was deactivated (its owned effects cancelled)
635    /// since the slot last composed, so the retained content must recompose
636    /// once to restart them — no flag on the slot's own scopes carries that
637    /// trace, because deactivation walks stop at slot-host boundaries.
638    pub fn owner_chain_deactivation_epoch(&self) -> u64 {
639        let mut total = 0u64;
640        let mut current = Some(self.clone());
641        while let Some(scope) = current {
642            total = total.wrapping_add(scope.inner.deactivations.get());
643            let structural_parent = scope.inner.parent_scope.borrow().clone();
644            let lifetime_owner = scope.inner.lifetime_owner_scope.borrow().clone();
645            let next = structural_parent.or(lifetime_owner);
646            current = next
647                .and_then(|parent| parent.upgrade())
648                .map(|inner| RecomposeScope { inner });
649        }
650        total
651    }
652
653    pub(crate) fn is_effectively_active(&self) -> bool {
654        let mut current = Some(self.clone());
655        while let Some(scope) = current {
656            if !scope.is_active() {
657                return false;
658            }
659            let structural_parent = scope.inner.parent_scope.borrow().clone();
660            let lifetime_owner = scope.inner.lifetime_owner_scope.borrow().clone();
661            let next = structural_parent.or(lifetime_owner);
662            current = match next {
663                Some(parent) => {
664                    let Some(inner) = parent.upgrade() else {
665                        return false;
666                    };
667                    Some(RecomposeScope { inner })
668                }
669                None => None,
670            };
671        }
672        true
673    }
674
675    fn record_state_subscription(&self, state_id: StateId) {
676        self.inner.state_subscriptions.borrow_mut().insert(state_id);
677    }
678
679    fn record_unknown_invalidation_source(&self) {
680        *self.inner.invalidation_sources.borrow_mut() = None;
681    }
682
683    fn record_state_invalidation_source(&self, state_id: StateId) {
684        let mut sources = self.inner.invalidation_sources.borrow_mut();
685        if let Some(source_set) = sources.as_mut() {
686            source_set.insert(state_id);
687        }
688    }
689
690    fn enqueue_invalidation(&self) {
691        self.inner.invalid.set(true);
692        if !self.is_effectively_active() {
693            return;
694        }
695        if !self.inner.enqueued.replace(true) {
696            self.inner
697                .runtime
698                .register_invalid_scope(self.id(), self.downgrade());
699        }
700    }
701
702    fn invalidate(&self) {
703        self.record_unknown_invalidation_source();
704        self.enqueue_invalidation();
705    }
706
707    pub(crate) fn invalidate_from_state(&self, state_id: StateId) {
708        self.record_state_invalidation_source(state_id);
709        self.enqueue_invalidation();
710    }
711
712    fn mark_recomposed(&self) {
713        self.inner.invalid.set(false);
714        self.inner.force_reuse.set(false);
715        self.inner.force_recompose.set(false);
716        self.inner
717            .invalidation_sources
718            .borrow_mut()
719            .replace(HashSet::default());
720        if self.inner.enqueued.replace(false) {
721            self.inner.runtime.mark_scope_recomposed(self.id());
722        }
723        let pending = self.inner.pending_recompose.replace(false);
724        if pending {
725            if self.inner.active.get() {
726                self.invalidate();
727            } else {
728                self.inner.invalid.set(true);
729            }
730        }
731    }
732
733    fn set_recompose(&self, callback: Box<dyn FnMut(&Composer) + 'static>) {
734        *self.inner.recompose.borrow_mut() = Some(RecomposeCallback::Dynamic(callback));
735    }
736
737    fn set_recompose_fn(&self, callback: fn(&Composer)) {
738        *self.inner.recompose.borrow_mut() = Some(RecomposeCallback::Static(callback));
739    }
740
741    fn run_recompose(&self, composer: &Composer) -> bool {
742        let callback = self.inner.recompose.borrow_mut().take();
743        if let Some(callback) = callback {
744            let callback = match callback {
745                RecomposeCallback::Static(callback) => {
746                    callback(composer);
747                    RecomposeCallback::Static(callback)
748                }
749                RecomposeCallback::Dynamic(mut callback) => {
750                    callback(composer);
751                    RecomposeCallback::Dynamic(callback)
752                }
753            };
754            let mut slot = self.inner.recompose.borrow_mut();
755            if slot.is_none() {
756                *slot = Some(callback);
757            }
758            true
759        } else {
760            false
761        }
762    }
763
764    fn has_recompose_callback(&self) -> bool {
765        self.inner.recompose.borrow().is_some()
766    }
767
768    fn snapshot_locals(&self, stack: LocalStackSnapshot) {
769        *self.inner.local_stack.borrow_mut() = stack;
770    }
771
772    fn local_stack(&self) -> LocalStackSnapshot {
773        self.inner.local_stack.borrow().clone()
774    }
775
776    fn set_parent_hint(&self, parent: Option<NodeId>) {
777        self.inner.parent_hint.set(parent);
778    }
779
780    fn set_parent_scope(&self, parent: Option<RecomposeScope>) {
781        *self.inner.parent_scope.borrow_mut() = parent.map(|scope| scope.downgrade());
782    }
783
784    fn parent_scope(&self) -> Option<RecomposeScope> {
785        self.inner
786            .parent_scope
787            .borrow()
788            .as_ref()
789            .and_then(Weak::upgrade)
790            .map(|inner| RecomposeScope { inner })
791    }
792
793    fn set_lifetime_owner_scope(&self, owner: Option<RecomposeScope>) {
794        *self.inner.lifetime_owner_scope.borrow_mut() = owner.map(|scope| scope.downgrade());
795    }
796
797    #[cfg(test)]
798    fn lifetime_owner_scope(&self) -> Option<RecomposeScope> {
799        self.inner
800            .lifetime_owner_scope
801            .borrow()
802            .as_ref()
803            .and_then(Weak::upgrade)
804            .map(|inner| RecomposeScope { inner })
805    }
806
807    fn callback_promotion_target(&self) -> Option<RecomposeScope> {
808        let mut current = self.parent_scope();
809        while let Some(scope) = current {
810            if scope.has_recompose_callback() {
811                return Some(scope);
812            }
813            current = scope.parent_scope();
814        }
815        None
816    }
817
818    fn parent_hint(&self) -> Option<NodeId> {
819        self.inner.parent_hint.get()
820    }
821
822    fn set_slots_host(&self, host: &Rc<SlotsHost>) {
823        self.inner.slots_storage_key.set(host.storage_key());
824        *self.inner.slots_runtime_state.borrow_mut() =
825            host.runtime_state().map(|state| Rc::downgrade(&state));
826    }
827
828    pub(crate) fn slots_storage_key(&self) -> Option<usize> {
829        let key = self.inner.slots_storage_key.get();
830        (key != 0).then_some(key)
831    }
832
833    pub(crate) fn slots_runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
834        self.inner
835            .slots_runtime_state
836            .borrow()
837            .as_ref()
838            .and_then(std::rc::Weak::upgrade)
839    }
840
841    pub fn deactivate(&self) {
842        if !self.inner.active.replace(false) {
843            return;
844        }
845        self.inner
846            .deactivations
847            .set(self.inner.deactivations.get() + 1);
848        if self.inner.enqueued.replace(false) {
849            self.inner.runtime.mark_scope_recomposed(self.id());
850        }
851    }
852
853    pub(crate) fn defer_until_reactivated(&self) {
854        if self.inner.enqueued.replace(false) {
855            self.inner.runtime.mark_scope_recomposed(self.id());
856        }
857    }
858
859    pub fn reactivate(&self) {
860        self.inner.active.set(true);
861        if self.inner.invalid.get()
862            && self.is_effectively_active()
863            && !self.inner.enqueued.replace(true)
864        {
865            self.inner
866                .runtime
867                .register_invalid_scope(self.id(), self.downgrade());
868        }
869    }
870
871    pub fn force_reuse(&self) {
872        self.inner.force_reuse.set(true);
873        self.inner.force_recompose.set(false);
874        self.inner.pending_recompose.set(true);
875    }
876
877    pub(crate) fn request_pending_recompose(&self) {
878        self.inner.pending_recompose.set(true);
879    }
880
881    pub fn force_recompose(&self) {
882        self.inner.force_recompose.set(true);
883        self.inner.force_reuse.set(false);
884        self.inner.pending_recompose.set(false);
885    }
886
887    pub(crate) fn set_retention_mode(&self, mode: RetentionMode) {
888        self.inner.retention_mode.set(mode);
889    }
890
891    pub(crate) fn retention_mode(&self) -> RetentionMode {
892        self.inner.retention_mode.get()
893    }
894
895    pub fn should_recompose(&self) -> bool {
896        if self.inner.force_recompose.replace(false) {
897            self.inner.force_reuse.set(false);
898            return true;
899        }
900        if self.inner.force_reuse.replace(false) {
901            return false;
902        }
903        self.is_invalid()
904    }
905
906    pub fn has_composed_once(&self) -> bool {
907        self.inner.composed_once.get()
908    }
909
910    fn mark_composed_once(&self) {
911        self.inner.composed_once.set(true);
912    }
913
914    fn invalidated_only_by(&self, allowed_sources: &HashSet<StateId>) -> Option<bool> {
915        let sources = self.inner.invalidation_sources.borrow();
916        let sources = sources.as_ref()?;
917        if sources.is_empty() {
918            return None;
919        }
920        Some(
921            sources
922                .iter()
923                .all(|source| allowed_sources.contains(source)),
924        )
925    }
926
927    fn has_unknown_invalidation_source(&self) -> bool {
928        self.inner.invalidation_sources.borrow().is_none()
929    }
930}
931
932#[cfg(test)]
933impl RecomposeScope {
934    pub(crate) fn new_for_test(runtime: RuntimeHandle) -> Self {
935        Self::new(runtime)
936    }
937}
938
939#[derive(Debug, Clone, Copy, Default)]
940pub struct RecomposeOptions {
941    pub force_reuse: bool,
942    pub force_recompose: bool,
943    pub retention: RetentionMode,
944}
945
946#[derive(Debug, Clone, PartialEq, Eq)]
947pub enum NodeError {
948    Missing {
949        id: NodeId,
950    },
951    TypeMismatch {
952        id: NodeId,
953        expected: &'static str,
954    },
955    MissingContext {
956        id: NodeId,
957        reason: &'static str,
958    },
959    AlreadyExists {
960        id: NodeId,
961    },
962    MalformedCommandPayload {
963        tag: &'static str,
964    },
965    SlotHostUnavailable {
966        operation: &'static str,
967        reason: &'static str,
968    },
969    RecompositionLimitExceeded {
970        operation: &'static str,
971        limit: usize,
972    },
973}
974
975impl std::fmt::Display for NodeError {
976    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
977        match self {
978            NodeError::Missing { id } => write!(f, "node {id} missing"),
979            NodeError::TypeMismatch { id, expected } => {
980                write!(f, "node {id} type mismatch; expected {expected}")
981            }
982            NodeError::MissingContext { id, reason } => {
983                write!(f, "missing context for node {id}: {reason}")
984            }
985            NodeError::AlreadyExists { id } => {
986                write!(f, "node {id} already exists")
987            }
988            NodeError::MalformedCommandPayload { tag } => {
989                write!(f, "command queue missing or invalid {tag} payload")
990            }
991            NodeError::SlotHostUnavailable { operation, reason } => {
992                write!(f, "{operation} cannot access slot host: {reason}")
993            }
994            NodeError::RecompositionLimitExceeded { operation, limit } => {
995                write!(
996                    f,
997                    "{operation} exceeded {limit} iterations while reconciling composition"
998                )
999            }
1000        }
1001    }
1002}
1003
1004impl std::error::Error for NodeError {}
1005
1006pub use subcompose::{
1007    ContentTypeReusePolicy, DefaultSlotReusePolicy, SlotId, SlotReusePolicy, SubcomposeState,
1008};
1009
1010#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1011pub enum Phase {
1012    Compose,
1013    Measure,
1014    Layout,
1015}
1016
1017pub use composer_context::{note_nested_slots_host, with_composer as with_current_composer};
1018
1019#[allow(non_snake_case)]
1020pub fn withCurrentComposer<R>(f: impl FnOnce(&Composer) -> R) -> R {
1021    composer_context::with_composer(f)
1022}
1023
1024fn with_current_composer_opt<R>(f: impl FnOnce(&Composer) -> R) -> Option<R> {
1025    composer_context::try_with_composer(f)
1026}
1027
1028#[doc(hidden)]
1029pub fn current_recompose_scope_invalidated_only_by(
1030    allowed_sources: impl IntoIterator<Item = StateId>,
1031) -> Option<bool> {
1032    with_current_composer_opt(|composer| {
1033        let allowed_sources = allowed_sources.into_iter().collect();
1034        let mut scope = composer.current_recompose_scope();
1035        let mut saw_unknown_source = false;
1036        while let Some(current) = scope {
1037            if current.has_unknown_invalidation_source() {
1038                saw_unknown_source = true;
1039                scope = current.parent_scope();
1040                continue;
1041            }
1042            if let Some(matches) = current.invalidated_only_by(&allowed_sources) {
1043                return Some(matches);
1044            }
1045            scope = current.parent_scope();
1046        }
1047        saw_unknown_source.then_some(false)
1048    })
1049    .flatten()
1050}
1051
1052fn key_scoped<K: Hash, R>(
1053    key: &K,
1054    caller: &'static std::panic::Location<'static>,
1055    content: impl FnOnce() -> R,
1056) -> R {
1057    let seed = explicit_group_key_seed(key, caller);
1058    with_current_composer(|composer| composer.with_group_seed(seed, |_| content()))
1059}
1060
1061#[track_caller]
1062pub fn with_key<K: Hash>(key: &K, content: impl FnOnce()) {
1063    key_scoped(key, std::panic::Location::caller(), content);
1064}
1065
1066/// Scopes composition identity to `keys` for the duration of `content`.
1067///
1068/// Mirrors Jetpack Compose's `key(vararg keys) { content }`. A call keeps its
1069/// identity by call-site position alone by default, so a slot survives a
1070/// recomposition even when what a caller passes in changes; wrapping the
1071/// call in `key(keys, || ...)` folds `keys` into that identity instead, so
1072/// changing `keys` discards the previous state and starts fresh, and two
1073/// `key` calls at the same call site with different `keys` never share
1074/// state. Pass a tuple to key on more than one value, e.g.
1075/// `key((a, b), || ...)`.
1076#[track_caller]
1077pub fn key<K: Hash, R>(keys: K, content: impl FnOnce() -> R) -> R {
1078    key_scoped(&keys, std::panic::Location::caller(), content)
1079}
1080
1081/// Composes `content` under an identity that is `key` alone, the same at
1082/// every call site, so the subtree can be emitted from any parent and keep
1083/// its remembered values, its running effects and its nodes.
1084///
1085/// Move content by emitting it under a different parent, in the same pass
1086/// or a later one: when the old parent stops emitting it, the subtree is
1087/// retained under `key`; when a parent emits it, the retained subtree is
1088/// taken back. A site that asks for content still attached elsewhere
1089/// composes nothing and is recomposed once the content is retained, so the
1090/// order in which the two parents recompose does not matter.
1091///
1092/// Content is shown at most once. Two live sites with the same key leave
1093/// the later one empty. Retained content is kept
1094/// until a parent takes it back or [`forget_movable`] releases it, so an
1095/// item the app closes for good must be forgotten or its state stays in
1096/// memory. A subtree may cross into and out of a `SubcomposeLayout` slot,
1097/// whose content lives in a slot table of its own: the table that held it
1098/// gives up its anchors and the one taking it over issues new ones.
1099#[track_caller]
1100pub fn movable<K: Hash>(key: K, content: impl FnOnce()) {
1101    let id = hash_key(&key);
1102    with_current_composer(|composer| composer.with_movable_group(id, |_| content()));
1103}
1104
1105/// Content written once that keeps its identity wherever it is shown.
1106///
1107/// [`movable`] names content by a key, which means writing the content out
1108/// at every parent that might show it and trusting the keys to match. When
1109/// the parents are alternatives -- a pane docked in a stack or pulled out
1110/// into a window of its own, a tab in a strip or torn into its own window --
1111/// that is the same body written twice, and two bodies that must be kept in
1112/// step by hand are two bodies that drift.
1113///
1114/// This hands back the content as a value instead. Write the body once,
1115/// remember it, and call [`MovableContent::show`] from whichever parent is
1116/// showing it this pass:
1117///
1118/// ```ignore
1119/// let pane = rememberMovableContentOf(move || PaneBody(state));
1120/// if docked {
1121///     Box(Modifier::empty().offset(0.0, y), BoxSpec::default(), || pane.show());
1122/// } else {
1123///     Box(Modifier::empty().window(config), BoxSpec::default(), || pane.show());
1124/// }
1125/// ```
1126///
1127/// The identity comes from the `remember`, so two of these at one call site
1128/// -- one per row of a list -- are two pieces of content, and the same one
1129/// survives recomposition. Everything [`movable`] says about ordering,
1130/// showing content at most once, and releasing it still holds; release this
1131/// one with [`MovableContent::forget`].
1132///
1133/// Content that has to be recognised from somewhere this value cannot reach
1134/// -- another composable that shows the same pane in another arrangement --
1135/// wants [`movableContentOf`] instead, which takes the identity as a key.
1136///
1137/// Mirrors Jetpack Compose's `movableContentOf`.
1138#[allow(non_snake_case)]
1139#[track_caller]
1140pub fn rememberMovableContentOf(content: impl Fn() + 'static) -> MovableContent {
1141    let runtime = with_current_composer(|composer| composer.runtime_handle());
1142    let id = remember(|| runtime.next_movable_content_id()).with(|id| *id);
1143    MovableContent {
1144        id,
1145        content: Rc::new(content),
1146    }
1147}
1148
1149/// Movable content whose identity is `key`, the same at every call site.
1150///
1151/// The keyed twin of [`rememberMovableContentOf`], and the same identity
1152/// [`movable`] uses: content shown by this value and content shown by
1153/// `movable` under the same key is one piece of content. Reach for this when
1154/// the parents that can show it are in different composables, so no single
1155/// value can be handed to all of them.
1156#[allow(non_snake_case)]
1157pub fn movableContentOf<K: Hash>(key: K, content: impl Fn() + 'static) -> MovableContent {
1158    MovableContent {
1159        id: hash_key(&key),
1160        content: Rc::new(content),
1161    }
1162}
1163
1164/// Movable content held as a value. See [`rememberMovableContentOf`].
1165#[derive(Clone)]
1166pub struct MovableContent {
1167    id: Key,
1168    content: Rc<dyn Fn()>,
1169}
1170
1171impl MovableContent {
1172    /// Shows the content here. Calling this from a different parent than
1173    /// last pass moves the content, with its remembered values, its running
1174    /// effects and its nodes.
1175    pub fn show(&self) {
1176        let content = Rc::clone(&self.content);
1177        let id = self.id;
1178        with_current_composer(|composer| composer.with_movable_group(id, |_| content()));
1179    }
1180
1181    /// Releases the content's state once no parent is showing it, for
1182    /// content the application has closed for good. See [`forget_movable`].
1183    pub fn forget(&self) {
1184        forget_movable_id(self.id);
1185    }
1186}
1187
1188impl std::fmt::Debug for MovableContent {
1189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1190        f.debug_struct("MovableContent")
1191            .field("id", &self.id)
1192            .finish()
1193    }
1194}
1195
1196impl PartialEq for MovableContent {
1197    fn eq(&self, other: &Self) -> bool {
1198        self.id == other.id && Rc::ptr_eq(&self.content, &other.content)
1199    }
1200}
1201
1202/// Releases the state of [`movable`] content with identity `key` that no
1203/// parent is showing. Call it when the item is closed for good, from an
1204/// event handler or inside composition. Content a parent is still showing
1205/// is unaffected. Needs a runtime: an active composition or one created on
1206/// this thread.
1207pub fn forget_movable<K: Hash>(key: K) {
1208    let id = hash_key(&key);
1209    forget_movable_id(id);
1210}
1211
1212fn forget_movable_id(id: Key) {
1213    let runtime = composer_context::try_with_composer(|composer| composer.runtime_handle())
1214        .or_else(runtime::current_runtime_handle);
1215    match runtime {
1216        Some(runtime) => runtime.forget_movable(id),
1217        None => log::error!("forget_movable called without an active runtime"),
1218    }
1219}
1220
1221#[derive(Default)]
1222struct DisposableEffectState {
1223    key: Option<effect_key::EffectKey>,
1224    cleanup: Option<Box<dyn FnOnce()>>,
1225}
1226
1227impl DisposableEffectState {
1228    fn should_run(&self, key: &effect_key::EffectKey) -> bool {
1229        match &self.key {
1230            Some(current) => key.differs_from(current),
1231            None => true,
1232        }
1233    }
1234
1235    fn set_key(&mut self, key: effect_key::EffectKey) {
1236        self.key = Some(key);
1237    }
1238
1239    fn set_cleanup(&mut self, cleanup: Option<Box<dyn FnOnce()>>) {
1240        self.cleanup = cleanup;
1241    }
1242
1243    fn run_cleanup(&mut self) {
1244        if let Some(cleanup) = self.cleanup.take() {
1245            cleanup();
1246        }
1247    }
1248}
1249
1250impl Drop for DisposableEffectState {
1251    fn drop(&mut self) {
1252        self.run_cleanup();
1253    }
1254}
1255
1256#[derive(Clone, Copy, Debug, Default)]
1257pub struct DisposableEffectScope;
1258
1259#[derive(Default)]
1260pub struct DisposableEffectResult {
1261    cleanup: Option<Box<dyn FnOnce()>>,
1262}
1263
1264impl DisposableEffectScope {
1265    pub fn on_dispose(&self, cleanup: impl FnOnce() + 'static) -> DisposableEffectResult {
1266        DisposableEffectResult::new(cleanup)
1267    }
1268}
1269
1270impl DisposableEffectResult {
1271    pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
1272        Self {
1273            cleanup: Some(Box::new(cleanup)),
1274        }
1275    }
1276
1277    fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
1278        self.cleanup
1279    }
1280}
1281
1282#[allow(non_snake_case)]
1283pub fn SideEffect(effect: impl FnOnce() + 'static) {
1284    with_current_composer(|composer| composer.register_side_effect(effect));
1285}
1286
1287pub fn __disposable_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
1288where
1289    K: PartialEq + 'static,
1290    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1291{
1292    with_current_composer(|composer| {
1293        composer.with_group(group_key, |composer| {
1294            let key = effect_key::EffectKey::new(keys);
1295            let state = composer.remember_effect::<DisposableEffectState>();
1296            if state.with(|state| state.should_run(&key)) {
1297                state.update(|state| {
1298                    state.run_cleanup();
1299                    state.set_key(key);
1300                });
1301                let state_for_effect = state.clone();
1302                let mut effect_opt = Some(effect);
1303                composer.register_side_effect(move || {
1304                    if let Some(effect) = effect_opt.take() {
1305                        let result = effect(DisposableEffectScope);
1306                        state_for_effect.update(|state| state.set_cleanup(result.into_cleanup()));
1307                    }
1308                });
1309            }
1310        });
1311    });
1312}
1313
1314/// Runs `effect` when this call site first enters composition, and again
1315/// whenever `keys` no longer equals the value it ran with last time; the
1316/// previous run's [`DisposableEffectScope::on_dispose`] cleanup, if any,
1317/// runs first, and also when the call site leaves composition entirely.
1318///
1319/// `keys` may be a tuple to depend on more than one value, matching Jetpack
1320/// Compose's `DisposableEffect(vararg keys)`.
1321#[allow(non_snake_case)]
1322#[track_caller]
1323pub fn DisposableEffect<K, F>(keys: K, effect: F)
1324where
1325    K: PartialEq + 'static,
1326    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1327{
1328    __disposable_effect_impl(crate::caller_location_key(), keys, effect);
1329}
1330
1331#[macro_export]
1332macro_rules! clone_captures {
1333    ($($alias:ident $(= $value:expr)?),+ $(,)?; $body:expr) => {{
1334        $(let $alias = $crate::clone_captures!(@clone $alias $(= $value)?);)+
1335        $body
1336    }};
1337    (@clone $alias:ident = $value:expr) => {
1338        ($value).clone()
1339    };
1340    (@clone $alias:ident) => {
1341        $alias.clone()
1342    };
1343}
1344
1345pub fn with_node_mut<N: Node + 'static, R>(
1346    id: NodeId,
1347    f: impl FnOnce(&mut N) -> R,
1348) -> Result<R, NodeError> {
1349    with_current_composer(|composer| composer.with_node_mut(id, f))
1350}
1351
1352pub fn push_parent(id: NodeId) {
1353    with_current_composer(|composer| composer.push_parent(id));
1354}
1355
1356pub fn pop_parent() {
1357    with_current_composer(|composer| composer.pop_parent());
1358}
1359
1360pub trait Node: Any {
1361    fn mount(&mut self) {}
1362    fn update(&mut self) {}
1363    fn unmount(&mut self) {}
1364    /// Adds `child` to this node's child list, returning whether the list
1365    /// actually changed. A node that already holds the child returns `false`:
1366    /// callers record a structural change from this answer, and a structural
1367    /// change re-lowers the whole subtree, so claiming one for a no-op costs a
1368    /// full rebuild per call.
1369    fn insert_child(&mut self, _child: NodeId) -> bool {
1370        false
1371    }
1372    /// Removes `child` from this node's child list, returning whether the list
1373    /// actually changed. Only this node can answer that: a caller inspecting
1374    /// the child's parent pointer gets it wrong when the child was reparented
1375    /// while still listed here.
1376    fn remove_child(&mut self, _child: NodeId) -> bool {
1377        false
1378    }
1379    fn move_child(&mut self, _from: usize, _to: usize) {}
1380    fn update_children(&mut self, _children: &[NodeId]) {}
1381    fn children(&self) -> Vec<NodeId> {
1382        Vec::new()
1383    }
1384    /// Copies child IDs into the provided scratch buffer without allocating a
1385    /// fresh container for every traversal.
1386    fn collect_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1387        out.clear();
1388        out.extend(self.children());
1389    }
1390    fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1391        self.collect_children_into(out);
1392    }
1393    /// Called after the node is created to record its own ID.
1394    /// Useful for nodes that need to store their ID for later operations.
1395    fn set_node_id(&mut self, _id: NodeId) {}
1396    /// Called when this node is attached to a parent.
1397    /// Nodes with parent tracking should set their parent reference here.
1398    fn on_attached_to_parent(&mut self, _parent: NodeId) {}
1399    /// Called when this node is removed from its parent.
1400    /// Nodes with parent tracking should clear their parent reference here.
1401    fn on_removed_from_parent(&mut self) {}
1402    /// Get this node's parent ID (for nodes that track parents).
1403    /// Returns None if node has no parent or doesn't track parents.
1404    fn parent(&self) -> Option<NodeId> {
1405        None
1406    }
1407    /// Mark this node as needing layout (for nodes with dirty flags).
1408    /// Called during bubbling to propagate dirtiness up the tree.
1409    fn mark_needs_layout(&self) {}
1410    /// Check if this node needs layout (for nodes with dirty flags).
1411    fn needs_layout(&self) -> bool {
1412        false
1413    }
1414    /// Mark this node as needing measure (size may have changed).
1415    /// Called during bubbling when children are added/removed.
1416    fn mark_needs_measure(&self) {}
1417    /// Check if this node needs measure (for nodes with dirty flags).
1418    fn needs_measure(&self) -> bool {
1419        false
1420    }
1421    /// Mark this node as needing semantics recomputation.
1422    fn mark_needs_semantics(&self) {}
1423    /// Check if this node needs semantics recomputation.
1424    fn needs_semantics(&self) -> bool {
1425        false
1426    }
1427    /// Set parent reference for dirty flag bubbling ONLY.
1428    /// This is a minimal version of on_attached_to_parent that doesn't trigger
1429    /// registry updates or other side effects. Used during measurement when we
1430    /// need to establish parent connections for bubble_measure_dirty without
1431    /// causing the full attachment lifecycle.
1432    ///
1433    /// An existing parent is preserved until the structural attach operation
1434    /// removes the node from its previous container.
1435    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1436        if self.parent().is_none() {
1437            self.on_attached_to_parent(parent);
1438        }
1439    }
1440
1441    /// Returns a recycle pool key when this node supports shell reuse.
1442    fn recycle_key(&self) -> Option<TypeId> {
1443        None
1444    }
1445
1446    /// Bounds how many recyclable shells of this node type should be retained.
1447    fn recycle_pool_limit(&self) -> Option<usize> {
1448        None
1449    }
1450
1451    /// Clears live attachments before the node shell enters a recycle pool.
1452    fn prepare_for_recycle(&mut self) {}
1453
1454    /// Optionally provides a compact replacement box for this recycled shell.
1455    ///
1456    /// Returning `Some` lets nodes move pooled survivors onto fresh compact
1457    /// storage so the recycle pool does not pin large spike-era allocations.
1458    fn rehouse_for_recycle(&self) -> Option<Box<dyn Node>> {
1459        None
1460    }
1461
1462    /// Optionally moves a live node onto a fresh box during applier compaction.
1463    ///
1464    /// This is used after large-majority teardowns where a small surviving live
1465    /// tree can otherwise pin allocator pages from a much larger spike-era node
1466    /// population. Implementations must preserve the node's live state.
1467    fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn Node>> {
1468        None
1469    }
1470
1471    /// Returns the node-owned heap retained beyond the node's own box allocation.
1472    fn debug_heap_bytes(&self) -> usize {
1473        0
1474    }
1475}
1476
1477/// Unified API for bubbling layout dirty flags from a node to the root (Applier context).
1478///
1479/// This is the canonical function for dirty bubbling during the apply phase (structural changes).
1480/// Call this after mutations like insert/remove/move that happen during apply.
1481///
1482/// # Behavior
1483/// 1. Marks the starting node as needing layout
1484/// 2. Walks up the parent chain, marking each ancestor
1485/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1486/// 4. Stops at the root (node with no parent)
1487///
1488/// # Performance
1489/// This function is O(height) in the worst case, but typically O(1) due to early exit
1490/// when encountering an already-dirty ancestor.
1491///
1492/// # Usage
1493/// - Call from composer mutations (insert/remove/move) during apply phase
1494/// - Call from applier-level operations that modify the tree structure
1495pub fn bubble_layout_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1496    bubble_layout_dirty_applier(applier, node_id);
1497}
1498
1499/// Unified API for bubbling measure dirty flags from a node to the root (Applier context).
1500///
1501/// Call this when a node's size may have changed (children added/removed, modifier changed).
1502/// This ensures that measure_layout will increment the cache epoch and re-measure the subtree.
1503///
1504/// # Behavior
1505/// 1. Marks the starting node as needing measure
1506/// 2. Walks up the parent chain, marking each ancestor
1507/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1508/// 4. Stops at the root (node with no parent)
1509pub fn bubble_measure_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1510    bubble_measure_dirty_applier(applier, node_id);
1511}
1512
1513/// Unified API for bubbling semantics dirty flags from a node to the root (Applier context).
1514///
1515/// This mirrors [`bubble_layout_dirty`] but toggles semantics-specific dirty
1516/// flags instead of layout ones, allowing semantics updates to propagate during
1517/// the apply phase without forcing layout work.
1518pub fn bubble_semantics_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1519    bubble_semantics_dirty_applier(applier, node_id);
1520}
1521
1522/// Schedules semantics bubbling for a node using the active composer if present.
1523///
1524/// This defers the work to the apply phase where we can safely mutate the
1525/// applier tree without re-entrantly borrowing the composer during composition.
1526pub fn queue_semantics_invalidation(node_id: NodeId) {
1527    let _ = composer_context::try_with_composer(|composer| {
1528        composer.enqueue_semantics_invalidation(node_id);
1529    });
1530}
1531
1532/// Unified API for bubbling layout dirty flags from a node to the root (Composer context).
1533///
1534/// This is the canonical function for dirty bubbling during composition (property changes).
1535/// Call this after property changes that happen during composition via with_node_mut.
1536///
1537/// # Behavior
1538/// 1. Marks the starting node as needing layout
1539/// 2. Walks up the parent chain, marking each ancestor
1540/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1541/// 4. Stops at the root (node with no parent)
1542///
1543/// # Performance
1544/// This function is O(height) in the worst case, but typically O(1) due to early exit
1545/// when encountering an already-dirty ancestor.
1546///
1547/// # Type Requirements
1548/// The node type N must implement Node (which includes mark_needs_layout, parent, etc.).
1549/// Typically this will be LayoutNode or similar layout-aware node types.
1550///
1551/// # Usage
1552/// - Call from property setters during composition (e.g., set_modifier, set_measure_policy)
1553/// - Call from widget composition when layout-affecting state changes
1554pub fn bubble_layout_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1555    bubble_layout_dirty_composer::<N>(node_id);
1556}
1557
1558/// Unified API for bubbling measure dirty flags from a node to the root during composition.
1559///
1560/// This queues a dirty-bubble command on the active composer so measure invalidation
1561/// runs during the apply phase, avoiding re-entrant applier borrows while widgets are
1562/// mutating nodes via `with_node_mut`.
1563pub fn bubble_measure_dirty_in_composer(node_id: NodeId) {
1564    with_current_composer(|composer| {
1565        composer.commands_mut().push(Command::BubbleDirty {
1566            node_id,
1567            bubble: DirtyBubble {
1568                layout: false,
1569                measure: true,
1570                semantics: false,
1571            },
1572        });
1573    });
1574}
1575
1576/// Unified API for bubbling semantics dirty flags from a node to the root (Composer context).
1577///
1578/// This mirrors [`bubble_layout_dirty_in_composer`] but routes through the semantics
1579/// dirty flag instead of the layout one. Modifier nodes can request semantics
1580/// invalidations without triggering measure/layout work, and the runtime can
1581/// query the root to determine whether the semantics tree needs rebuilding.
1582pub fn bubble_semantics_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1583    bubble_semantics_dirty_composer::<N>(node_id);
1584}
1585
1586fn bubble_layout_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1587    if let Ok(node) = applier.get_mut(node_id) {
1588        node.mark_needs_layout();
1589    }
1590
1591    loop {
1592        let parent_id = match applier.get_mut(node_id) {
1593            Ok(node) => node.parent(),
1594            Err(_) => None,
1595        };
1596
1597        match parent_id {
1598            Some(pid) => {
1599                if let Ok(parent) = applier.get_mut(pid) {
1600                    let parent_already_dirty = parent.needs_layout();
1601                    if !parent_already_dirty {
1602                        parent.mark_needs_layout();
1603                    }
1604                    node_id = pid;
1605                } else {
1606                    break;
1607                }
1608            }
1609            None => break,
1610        }
1611    }
1612}
1613
1614fn bubble_measure_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1615    if let Ok(node) = applier.get_mut(node_id) {
1616        node.mark_needs_measure();
1617    }
1618
1619    loop {
1620        let parent_id = match applier.get_mut(node_id) {
1621            Ok(node) => node.parent(),
1622            Err(_) => None,
1623        };
1624
1625        match parent_id {
1626            Some(pid) => {
1627                if let Ok(parent) = applier.get_mut(pid) {
1628                    if !parent.needs_measure() {
1629                        parent.mark_needs_measure();
1630                    }
1631                    node_id = pid;
1632                } else {
1633                    break;
1634                }
1635            }
1636            None => {
1637                break;
1638            }
1639        }
1640    }
1641}
1642
1643fn bubble_semantics_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1644    if let Ok(node) = applier.get_mut(node_id) {
1645        node.mark_needs_semantics();
1646    }
1647
1648    loop {
1649        let parent_id = match applier.get_mut(node_id) {
1650            Ok(node) => node.parent(),
1651            Err(_) => None,
1652        };
1653
1654        match parent_id {
1655            Some(pid) => {
1656                if let Ok(parent) = applier.get_mut(pid) {
1657                    if !parent.needs_semantics() {
1658                        parent.mark_needs_semantics();
1659                    }
1660                    node_id = pid;
1661                } else {
1662                    break;
1663                }
1664            }
1665            None => break,
1666        }
1667    }
1668}
1669
1670fn bubble_layout_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1671    let _ = with_node_mut(node_id, |node: &mut N| {
1672        node.mark_needs_layout();
1673    });
1674
1675    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1676        let parent_id = pid;
1677
1678        let advanced = with_node_mut(parent_id, |node: &mut N| {
1679            if !node.needs_layout() {
1680                node.mark_needs_layout();
1681            }
1682            true
1683        })
1684        .unwrap_or(false);
1685
1686        if advanced {
1687            node_id = parent_id;
1688        } else {
1689            break;
1690        }
1691    }
1692}
1693
1694fn bubble_semantics_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1695    let _ = with_node_mut(node_id, |node: &mut N| {
1696        node.mark_needs_semantics();
1697    });
1698
1699    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1700        let parent_id = pid;
1701
1702        let advanced = with_node_mut(parent_id, |node: &mut N| {
1703            if !node.needs_semantics() {
1704                node.mark_needs_semantics();
1705            }
1706            true
1707        })
1708        .unwrap_or(false);
1709
1710        if advanced {
1711            node_id = parent_id;
1712        } else {
1713            break;
1714        }
1715    }
1716}
1717
1718impl dyn Node {
1719    pub fn as_any_mut(&mut self) -> &mut dyn Any {
1720        self
1721    }
1722}
1723
1724pub struct RecycledNode {
1725    stable_id: NodeId,
1726    node: Box<dyn Node>,
1727    warm_origin: bool,
1728}
1729
1730impl RecycledNode {
1731    fn new(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1732        let node = node.rehouse_for_recycle().unwrap_or(node);
1733        Self {
1734            stable_id,
1735            node,
1736            warm_origin,
1737        }
1738    }
1739
1740    fn from_shell(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1741        Self {
1742            stable_id,
1743            node,
1744            warm_origin,
1745        }
1746    }
1747
1748    pub fn stable_id(&self) -> NodeId {
1749        self.stable_id
1750    }
1751
1752    fn warm_origin(&self) -> bool {
1753        self.warm_origin
1754    }
1755
1756    fn set_warm_origin(&mut self, warm_origin: bool) {
1757        self.warm_origin = warm_origin;
1758    }
1759
1760    pub fn node_mut(&mut self) -> &mut dyn Node {
1761        self.node.as_mut()
1762    }
1763
1764    pub fn into_parts(self) -> (NodeId, Box<dyn Node>, bool) {
1765        (self.stable_id, self.node, self.warm_origin)
1766    }
1767}
1768
1769#[derive(Debug, Clone, PartialEq, Eq)]
1770pub struct RecycledNodeInsertion {
1771    pub id: NodeId,
1772    pub stable_id_reused: bool,
1773    pub fallback_error: Option<NodeError>,
1774}
1775
1776impl RecycledNodeInsertion {
1777    fn reused(stable_id: NodeId) -> Self {
1778        Self {
1779            id: stable_id,
1780            stable_id_reused: true,
1781            fallback_error: None,
1782        }
1783    }
1784
1785    fn fresh(id: NodeId, fallback_error: Option<NodeError>) -> Self {
1786        Self {
1787            id,
1788            stable_id_reused: false,
1789            fallback_error,
1790        }
1791    }
1792}
1793
1794pub trait Applier: Any {
1795    fn create(&mut self, node: Box<dyn Node>) -> NodeId;
1796    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError>;
1797    fn remove(&mut self, id: NodeId) -> Result<(), NodeError>;
1798
1799    /// Records that `parent_id`'s child list changed structurally this frame
1800    /// (insert, remove, move, or reparent). Incremental scene consumers drain
1801    /// the recorded parents and re-patch those subtrees so removed nodes are
1802    /// evicted from a persistent render graph even when the frame's scene
1803    /// update is otherwise scoped to unrelated dirty nodes.
1804    fn record_structural_change(&mut self, _parent_id: NodeId) {}
1805
1806    /// Returns the current generation for a node index.
1807    /// Generation is incremented when an index is reused from the freelist,
1808    /// preventing stale slot entries from matching recycled nodes.
1809    fn node_generation(&self, id: NodeId) -> u32;
1810
1811    /// Inserts a node with a pre-assigned ID.
1812    ///
1813    /// This is used for virtual nodes whose IDs are allocated separately
1814    /// (e.g., via allocate_virtual_node_id()). Unlike `create()` which assigns
1815    /// a new ID, this method uses the provided ID.
1816    ///
1817    /// Returns Ok(()) if successful, or an error if the ID is already in use.
1818    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError>;
1819
1820    /// Reinserts a recycled node at its retained stable ID, or creates a fresh ID if that
1821    /// retained ID is no longer available.
1822    fn insert_recycled_node_or_create(
1823        &mut self,
1824        stable_id: NodeId,
1825        node: Box<dyn Node>,
1826    ) -> RecycledNodeInsertion {
1827        let id = self.create(node);
1828        RecycledNodeInsertion::fresh(id, Some(NodeError::AlreadyExists { id: stable_id }))
1829    }
1830
1831    fn as_any(&self) -> &dyn Any
1832    where
1833        Self: Sized,
1834    {
1835        self
1836    }
1837
1838    fn as_any_mut(&mut self) -> &mut dyn Any
1839    where
1840        Self: Sized,
1841    {
1842        self
1843    }
1844
1845    /// Trim trailing tombstones/unused capacity after structural changes.
1846    fn compact(&mut self) {}
1847
1848    /// Returns a previously recycled node shell and its stable ID for the requested concrete type.
1849    fn take_recycled_node(&mut self, _key: TypeId) -> Option<RecycledNode> {
1850        None
1851    }
1852
1853    /// Marks whether a reinserted recycled node originated from the warm recycle path.
1854    fn set_recycled_node_origin(&mut self, _id: NodeId, _warm_origin: bool) {}
1855
1856    /// Seeds a warm recyclable shell for future reuse without requiring a prior removal.
1857    fn seed_recycled_node_shell(
1858        &mut self,
1859        _key: TypeId,
1860        _recycle_pool_limit: Option<usize>,
1861        _shell: Box<dyn Node>,
1862    ) {
1863    }
1864
1865    /// Records that the current apply pass had to allocate a fresh recyclable shell for this type.
1866    fn record_fresh_recyclable_creation(&mut self, _key: TypeId) {}
1867
1868    /// Drops any recyclable shells that should not survive beyond the current apply pass.
1869    fn clear_recycled_nodes(&mut self) {}
1870}
1871
1872type TypedNodeUpdate = fn(&mut dyn Node, NodeId) -> Result<(), NodeError>;
1873type CommandCallback = Box<dyn FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static>;
1874
1875#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1876pub(crate) struct DirtyBubble {
1877    layout: bool,
1878    measure: bool,
1879    semantics: bool,
1880}
1881
1882impl DirtyBubble {
1883    pub(crate) const LAYOUT_AND_MEASURE: Self = Self {
1884        layout: true,
1885        measure: true,
1886        semantics: false,
1887    };
1888
1889    pub(crate) const SEMANTICS: Self = Self {
1890        layout: false,
1891        measure: false,
1892        semantics: true,
1893    };
1894
1895    fn apply(self, applier: &mut dyn Applier, node_id: NodeId) {
1896        if self.layout {
1897            bubble_layout_dirty(applier, node_id);
1898        }
1899        if self.measure {
1900            bubble_measure_dirty(applier, node_id);
1901        }
1902        if self.semantics {
1903            bubble_semantics_dirty(applier, node_id);
1904        }
1905    }
1906}
1907
1908pub(crate) enum Command {
1909    BubbleDirty {
1910        node_id: NodeId,
1911        bubble: DirtyBubble,
1912    },
1913    UpdateTypedNode {
1914        id: NodeId,
1915        updater: TypedNodeUpdate,
1916    },
1917    RemoveNode {
1918        id: NodeId,
1919    },
1920    MountNode {
1921        id: NodeId,
1922    },
1923    AttachChild {
1924        parent_id: NodeId,
1925        child_id: NodeId,
1926        insert_index: Option<usize>,
1927        bubble: DirtyBubble,
1928    },
1929    InsertChild {
1930        parent_id: NodeId,
1931        child_id: NodeId,
1932        appended_index: usize,
1933        insert_index: usize,
1934        bubble: DirtyBubble,
1935    },
1936    MoveChild {
1937        parent_id: NodeId,
1938        from_index: usize,
1939        to_index: usize,
1940        bubble: DirtyBubble,
1941    },
1942    RemoveChild {
1943        parent_id: NodeId,
1944        child_id: NodeId,
1945    },
1946    DetachChild {
1947        parent_id: NodeId,
1948        child_id: NodeId,
1949    },
1950    SyncChildren {
1951        parent_id: NodeId,
1952        expected_children: ChildList,
1953    },
1954    Callback(CommandCallback),
1955}
1956
1957#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1958struct DeferredChildCleanup {
1959    child_id: NodeId,
1960    generation: u32,
1961    removed_from_parent: bool,
1962}
1963
1964#[derive(Default)]
1965struct DeferredChildCleanupQueue {
1966    pending: Vec<DeferredChildCleanup>,
1967    preserved: Vec<(NodeId, u32)>,
1968}
1969
1970impl DeferredChildCleanupQueue {
1971    fn push(&mut self, child_id: NodeId, generation: u32, removed_from_parent: bool) {
1972        if self
1973            .preserved
1974            .iter()
1975            .any(|&(preserved_id, preserved_generation)| {
1976                preserved_id == child_id && preserved_generation == generation
1977            })
1978        {
1979            return;
1980        }
1981        self.pending.push(DeferredChildCleanup {
1982            child_id,
1983            generation,
1984            removed_from_parent,
1985        });
1986    }
1987
1988    fn preserve(&mut self, child_id: NodeId, generation: u32) {
1989        if !self
1990            .preserved
1991            .iter()
1992            .any(|&(preserved_id, preserved_generation)| {
1993                preserved_id == child_id && preserved_generation == generation
1994            })
1995        {
1996            self.preserved.push((child_id, generation));
1997        }
1998        self.pending
1999            .retain(|cleanup| cleanup.child_id != child_id || cleanup.generation != generation);
2000    }
2001
2002    fn flush(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2003        for cleanup in self.pending {
2004            cleanup_detached_child(applier, cleanup)?;
2005        }
2006        Ok(())
2007    }
2008}
2009
2010impl Command {
2011    pub(crate) fn update_node<N: Node + 'static>(id: NodeId) -> Self {
2012        Self::UpdateTypedNode {
2013            id,
2014            updater: update_typed_node::<N>,
2015        }
2016    }
2017
2018    pub(crate) fn callback(
2019        callback: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
2020    ) -> Self {
2021        Self::Callback(Box::new(callback))
2022    }
2023
2024    pub(crate) fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2025        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2026        self.apply_with_cleanup(applier, &mut deferred_cleanup)?;
2027        deferred_cleanup.flush(applier)
2028    }
2029
2030    fn apply_with_cleanup(
2031        self,
2032        applier: &mut dyn Applier,
2033        deferred_cleanup: &mut DeferredChildCleanupQueue,
2034    ) -> Result<(), NodeError> {
2035        match self {
2036            Self::BubbleDirty { node_id, bubble } => {
2037                bubble.apply(applier, node_id);
2038                Ok(())
2039            }
2040            Self::UpdateTypedNode { id, updater } => {
2041                let node = match applier.get_mut(id) {
2042                    Ok(node) => node,
2043                    Err(NodeError::Missing { .. }) => return Ok(()),
2044                    Err(err) => return Err(err),
2045                };
2046                updater(node, id)
2047            }
2048            Self::RemoveNode { id } => {
2049                if let Ok(node) = applier.get_mut(id) {
2050                    node.unmount();
2051                }
2052                match applier.remove(id) {
2053                    Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
2054                    Err(err) => Err(err),
2055                }
2056            }
2057            Self::MountNode { id } => {
2058                let node = match applier.get_mut(id) {
2059                    Ok(node) => node,
2060                    Err(NodeError::Missing { .. }) => return Ok(()),
2061                    Err(err) => return Err(err),
2062                };
2063                node.set_node_id(id);
2064                node.mount();
2065                Ok(())
2066            }
2067            Self::AttachChild {
2068                parent_id,
2069                child_id,
2070                insert_index,
2071                bubble,
2072            } => {
2073                attach_child_at(applier, parent_id, child_id, insert_index, bubble);
2074                Ok(())
2075            }
2076            Self::InsertChild {
2077                parent_id,
2078                child_id,
2079                appended_index,
2080                insert_index,
2081                bubble,
2082            } => {
2083                insert_child_with_reparenting(applier, parent_id, child_id);
2084                bubble.apply(applier, parent_id);
2085                if insert_index != appended_index
2086                    && let Ok(parent_node) = applier.get_mut(parent_id)
2087                {
2088                    parent_node.move_child(appended_index, insert_index);
2089                }
2090                Ok(())
2091            }
2092            Self::MoveChild {
2093                parent_id,
2094                from_index,
2095                to_index,
2096                bubble,
2097            } => {
2098                if let Ok(parent_node) = applier.get_mut(parent_id) {
2099                    parent_node.move_child(from_index, to_index);
2100                }
2101                bubble.apply(applier, parent_id);
2102                note_structural_move(parent_id, from_index, to_index);
2103                applier.record_structural_change(parent_id);
2104                Ok(())
2105            }
2106            Self::RemoveChild {
2107                parent_id,
2108                child_id,
2109            } => apply_remove_child(applier, parent_id, child_id, deferred_cleanup),
2110            Self::DetachChild {
2111                parent_id,
2112                child_id,
2113            } => {
2114                let generation = applier.node_generation(child_id);
2115                detach_child_from_parent(applier, parent_id, child_id)?;
2116                deferred_cleanup.preserve(child_id, generation);
2117                Ok(())
2118            }
2119            Self::SyncChildren {
2120                parent_id,
2121                expected_children,
2122            } => sync_children(applier, parent_id, &expected_children, deferred_cleanup),
2123            Self::Callback(callback) => callback(applier),
2124        }
2125    }
2126}
2127
2128const COMMAND_CHUNK_CAPACITY: usize = 1024;
2129const COMMAND_FLUSH_THRESHOLD: usize = COMMAND_CHUNK_CAPACITY * 4;
2130type ChildList = SmallVec<[NodeId; 4]>;
2131const SMALL_CHILD_SYNC_LINEAR_THRESHOLD: usize = 8;
2132
2133#[derive(Copy, Clone)]
2134enum CommandTag {
2135    BubbleDirty,
2136    UpdateTypedNode,
2137    RemoveNode,
2138    MountNode,
2139    AttachChild,
2140    InsertChild,
2141    MoveChild,
2142    RemoveChild,
2143    DetachChild,
2144    SyncChildren,
2145    Callback,
2146}
2147
2148impl CommandTag {
2149    fn label(self) -> &'static str {
2150        match self {
2151            Self::BubbleDirty => "BubbleDirty",
2152            Self::UpdateTypedNode => "UpdateTypedNode",
2153            Self::RemoveNode => "RemoveNode",
2154            Self::MountNode => "MountNode",
2155            Self::AttachChild => "AttachChild",
2156            Self::InsertChild => "InsertChild",
2157            Self::MoveChild => "MoveChild",
2158            Self::RemoveChild => "RemoveChild",
2159            Self::DetachChild => "DetachChild",
2160            Self::SyncChildren => "SyncChildren",
2161            Self::Callback => "Callback",
2162        }
2163    }
2164}
2165
2166#[derive(Copy, Clone)]
2167struct BubbleDirtyCommand {
2168    node_id: NodeId,
2169    bubble: DirtyBubble,
2170}
2171
2172#[derive(Copy, Clone)]
2173struct UpdateTypedNodeCommand {
2174    id: NodeId,
2175    updater: TypedNodeUpdate,
2176}
2177
2178#[derive(Copy, Clone)]
2179struct AttachChildCommand {
2180    parent_id: NodeId,
2181    child_id: NodeId,
2182    insert_index: Option<usize>,
2183    bubble: DirtyBubble,
2184}
2185
2186#[derive(Copy, Clone)]
2187struct InsertChildCommand {
2188    parent_id: NodeId,
2189    child_id: NodeId,
2190    appended_index: usize,
2191    insert_index: usize,
2192    bubble: DirtyBubble,
2193}
2194
2195#[derive(Copy, Clone)]
2196struct MoveChildCommand {
2197    parent_id: NodeId,
2198    from_index: usize,
2199    to_index: usize,
2200    bubble: DirtyBubble,
2201}
2202
2203#[derive(Copy, Clone)]
2204struct RemoveChildCommand {
2205    parent_id: NodeId,
2206    child_id: NodeId,
2207}
2208
2209#[derive(Copy, Clone)]
2210struct DetachChildCommand {
2211    parent_id: NodeId,
2212    child_id: NodeId,
2213}
2214
2215struct SyncChildrenCommand {
2216    parent_id: NodeId,
2217    child_start: usize,
2218    child_len: usize,
2219}
2220
2221#[derive(Default)]
2222struct CommandQueue {
2223    chunks: Vec<Vec<CommandTag>>,
2224    len: usize,
2225    bubble_dirty: Vec<BubbleDirtyCommand>,
2226    update_typed_nodes: Vec<UpdateTypedNodeCommand>,
2227    remove_nodes: Vec<NodeId>,
2228    mount_nodes: Vec<NodeId>,
2229    attach_children: Vec<AttachChildCommand>,
2230    insert_children: Vec<InsertChildCommand>,
2231    move_children: Vec<MoveChildCommand>,
2232    remove_children: Vec<RemoveChildCommand>,
2233    detach_children: Vec<DetachChildCommand>,
2234    sync_children: Vec<SyncChildrenCommand>,
2235    sync_child_ids: Vec<NodeId>,
2236    callbacks: Vec<CommandCallback>,
2237}
2238
2239impl CommandQueue {
2240    fn push_tag(&mut self, tag: CommandTag) {
2241        let needs_chunk = self
2242            .chunks
2243            .last()
2244            .map(|chunk| chunk.len() == chunk.capacity())
2245            .unwrap_or(true);
2246        if needs_chunk {
2247            self.chunks.push(Vec::with_capacity(COMMAND_CHUNK_CAPACITY));
2248        }
2249        if let Some(chunk) = self.chunks.last_mut() {
2250            chunk.push(tag);
2251            self.len += 1;
2252        }
2253    }
2254
2255    fn push(&mut self, command: Command) {
2256        match command {
2257            Command::BubbleDirty { node_id, bubble } => {
2258                self.bubble_dirty
2259                    .push(BubbleDirtyCommand { node_id, bubble });
2260                self.push_tag(CommandTag::BubbleDirty);
2261            }
2262            Command::UpdateTypedNode { id, updater } => {
2263                self.update_typed_nodes
2264                    .push(UpdateTypedNodeCommand { id, updater });
2265                self.push_tag(CommandTag::UpdateTypedNode);
2266            }
2267            Command::RemoveNode { id } => {
2268                self.remove_nodes.push(id);
2269                self.push_tag(CommandTag::RemoveNode);
2270            }
2271            Command::MountNode { id } => {
2272                self.mount_nodes.push(id);
2273                self.push_tag(CommandTag::MountNode);
2274            }
2275            Command::AttachChild {
2276                parent_id,
2277                child_id,
2278                insert_index,
2279                bubble,
2280            } => {
2281                self.attach_children.push(AttachChildCommand {
2282                    parent_id,
2283                    child_id,
2284                    insert_index,
2285                    bubble,
2286                });
2287                self.push_tag(CommandTag::AttachChild);
2288            }
2289            Command::InsertChild {
2290                parent_id,
2291                child_id,
2292                appended_index,
2293                insert_index,
2294                bubble,
2295            } => {
2296                self.insert_children.push(InsertChildCommand {
2297                    parent_id,
2298                    child_id,
2299                    appended_index,
2300                    insert_index,
2301                    bubble,
2302                });
2303                self.push_tag(CommandTag::InsertChild);
2304            }
2305            Command::MoveChild {
2306                parent_id,
2307                from_index,
2308                to_index,
2309                bubble,
2310            } => {
2311                self.move_children.push(MoveChildCommand {
2312                    parent_id,
2313                    from_index,
2314                    to_index,
2315                    bubble,
2316                });
2317                self.push_tag(CommandTag::MoveChild);
2318            }
2319            Command::RemoveChild {
2320                parent_id,
2321                child_id,
2322            } => {
2323                self.remove_children.push(RemoveChildCommand {
2324                    parent_id,
2325                    child_id,
2326                });
2327                self.push_tag(CommandTag::RemoveChild);
2328            }
2329            Command::DetachChild {
2330                parent_id,
2331                child_id,
2332            } => {
2333                self.detach_children.push(DetachChildCommand {
2334                    parent_id,
2335                    child_id,
2336                });
2337                self.push_tag(CommandTag::DetachChild);
2338            }
2339            Command::SyncChildren {
2340                parent_id,
2341                expected_children,
2342            } => {
2343                let child_start = self.sync_child_ids.len();
2344                let child_len = expected_children.len();
2345                self.sync_child_ids.extend(expected_children);
2346                self.sync_children.push(SyncChildrenCommand {
2347                    parent_id,
2348                    child_start,
2349                    child_len,
2350                });
2351                self.push_tag(CommandTag::SyncChildren);
2352            }
2353            Command::Callback(callback) => {
2354                self.callbacks.push(callback);
2355                self.push_tag(CommandTag::Callback);
2356            }
2357        }
2358    }
2359
2360    fn len(&self) -> usize {
2361        self.len
2362    }
2363
2364    fn capacity(&self) -> usize {
2365        self.chunks.iter().map(Vec::capacity).sum()
2366    }
2367
2368    fn payload_len_bytes(&self) -> usize {
2369        self.bubble_dirty
2370            .len()
2371            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2372            .saturating_add(
2373                self.update_typed_nodes
2374                    .len()
2375                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2376            )
2377            .saturating_add(
2378                self.remove_nodes
2379                    .len()
2380                    .saturating_mul(std::mem::size_of::<NodeId>()),
2381            )
2382            .saturating_add(
2383                self.mount_nodes
2384                    .len()
2385                    .saturating_mul(std::mem::size_of::<NodeId>()),
2386            )
2387            .saturating_add(
2388                self.attach_children
2389                    .len()
2390                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2391            )
2392            .saturating_add(
2393                self.insert_children
2394                    .len()
2395                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2396            )
2397            .saturating_add(
2398                self.move_children
2399                    .len()
2400                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2401            )
2402            .saturating_add(
2403                self.remove_children
2404                    .len()
2405                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2406            )
2407            .saturating_add(
2408                self.detach_children
2409                    .len()
2410                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2411            )
2412            .saturating_add(
2413                self.sync_children
2414                    .len()
2415                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2416            )
2417            .saturating_add(
2418                self.sync_child_ids
2419                    .len()
2420                    .saturating_mul(std::mem::size_of::<NodeId>()),
2421            )
2422            .saturating_add(
2423                self.callbacks
2424                    .len()
2425                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2426            )
2427    }
2428
2429    fn payload_capacity_bytes(&self) -> usize {
2430        self.bubble_dirty
2431            .capacity()
2432            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2433            .saturating_add(
2434                self.update_typed_nodes
2435                    .capacity()
2436                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2437            )
2438            .saturating_add(
2439                self.remove_nodes
2440                    .capacity()
2441                    .saturating_mul(std::mem::size_of::<NodeId>()),
2442            )
2443            .saturating_add(
2444                self.mount_nodes
2445                    .capacity()
2446                    .saturating_mul(std::mem::size_of::<NodeId>()),
2447            )
2448            .saturating_add(
2449                self.attach_children
2450                    .capacity()
2451                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2452            )
2453            .saturating_add(
2454                self.insert_children
2455                    .capacity()
2456                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2457            )
2458            .saturating_add(
2459                self.move_children
2460                    .capacity()
2461                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2462            )
2463            .saturating_add(
2464                self.remove_children
2465                    .capacity()
2466                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2467            )
2468            .saturating_add(
2469                self.detach_children
2470                    .capacity()
2471                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2472            )
2473            .saturating_add(
2474                self.sync_children
2475                    .capacity()
2476                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2477            )
2478            .saturating_add(
2479                self.sync_child_ids
2480                    .capacity()
2481                    .saturating_mul(std::mem::size_of::<NodeId>()),
2482            )
2483            .saturating_add(
2484                self.callbacks
2485                    .capacity()
2486                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2487            )
2488    }
2489
2490    fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2491        let mut bubble_dirty = self.bubble_dirty.into_iter();
2492        let mut update_typed_nodes = self.update_typed_nodes.into_iter();
2493        let mut remove_nodes = self.remove_nodes.into_iter();
2494        let mut mount_nodes = self.mount_nodes.into_iter();
2495        let mut attach_children = self.attach_children.into_iter();
2496        let mut insert_children = self.insert_children.into_iter();
2497        let mut move_children = self.move_children.into_iter();
2498        let mut remove_children = self.remove_children.into_iter();
2499        let mut detach_children = self.detach_children.into_iter();
2500        let mut sync_children_commands = self.sync_children.into_iter();
2501        let sync_child_ids = self.sync_child_ids;
2502        let mut callbacks = self.callbacks.into_iter();
2503        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2504
2505        for chunk in self.chunks {
2506            for tag in chunk {
2507                match tag {
2508                    CommandTag::BubbleDirty => {
2509                        let BubbleDirtyCommand { node_id, bubble } =
2510                            next_command_payload(&mut bubble_dirty, tag)?;
2511                        Command::BubbleDirty { node_id, bubble }
2512                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2513                    }
2514                    CommandTag::UpdateTypedNode => {
2515                        let UpdateTypedNodeCommand { id, updater } =
2516                            next_command_payload(&mut update_typed_nodes, tag)?;
2517                        Command::UpdateTypedNode { id, updater }
2518                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2519                    }
2520                    CommandTag::RemoveNode => {
2521                        let id = next_command_payload(&mut remove_nodes, tag)?;
2522                        Command::RemoveNode { id }
2523                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2524                    }
2525                    CommandTag::MountNode => {
2526                        let id = next_command_payload(&mut mount_nodes, tag)?;
2527                        Command::MountNode { id }
2528                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2529                    }
2530                    CommandTag::AttachChild => {
2531                        let AttachChildCommand {
2532                            parent_id,
2533                            child_id,
2534                            insert_index,
2535                            bubble,
2536                        } = next_command_payload(&mut attach_children, tag)?;
2537                        Command::AttachChild {
2538                            parent_id,
2539                            child_id,
2540                            insert_index,
2541                            bubble,
2542                        }
2543                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2544                    }
2545                    CommandTag::InsertChild => {
2546                        let InsertChildCommand {
2547                            parent_id,
2548                            child_id,
2549                            appended_index,
2550                            insert_index,
2551                            bubble,
2552                        } = next_command_payload(&mut insert_children, tag)?;
2553                        Command::InsertChild {
2554                            parent_id,
2555                            child_id,
2556                            appended_index,
2557                            insert_index,
2558                            bubble,
2559                        }
2560                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2561                    }
2562                    CommandTag::MoveChild => {
2563                        let MoveChildCommand {
2564                            parent_id,
2565                            from_index,
2566                            to_index,
2567                            bubble,
2568                        } = next_command_payload(&mut move_children, tag)?;
2569                        Command::MoveChild {
2570                            parent_id,
2571                            from_index,
2572                            to_index,
2573                            bubble,
2574                        }
2575                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2576                    }
2577                    CommandTag::RemoveChild => {
2578                        let RemoveChildCommand {
2579                            parent_id,
2580                            child_id,
2581                        } = next_command_payload(&mut remove_children, tag)?;
2582                        Command::RemoveChild {
2583                            parent_id,
2584                            child_id,
2585                        }
2586                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2587                    }
2588                    CommandTag::DetachChild => {
2589                        let DetachChildCommand {
2590                            parent_id,
2591                            child_id,
2592                        } = next_command_payload(&mut detach_children, tag)?;
2593                        Command::DetachChild {
2594                            parent_id,
2595                            child_id,
2596                        }
2597                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2598                    }
2599                    CommandTag::SyncChildren => {
2600                        let SyncChildrenCommand {
2601                            parent_id,
2602                            child_start,
2603                            child_len,
2604                        } = next_command_payload(&mut sync_children_commands, tag)?;
2605                        let child_end = child_start
2606                            .checked_add(child_len)
2607                            .ok_or_else(|| command_payload_error(tag))?;
2608                        let expected_children = sync_child_ids
2609                            .get(child_start..child_end)
2610                            .ok_or_else(|| command_payload_error(tag))?;
2611                        sync_children(
2612                            applier,
2613                            parent_id,
2614                            expected_children,
2615                            &mut deferred_cleanup,
2616                        )?;
2617                    }
2618                    CommandTag::Callback => {
2619                        let callback = next_command_payload(&mut callbacks, tag)?;
2620                        Command::Callback(callback)
2621                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2622                    }
2623                }
2624            }
2625        }
2626
2627        debug_assert!(bubble_dirty.next().is_none());
2628        debug_assert!(update_typed_nodes.next().is_none());
2629        debug_assert!(remove_nodes.next().is_none());
2630        debug_assert!(mount_nodes.next().is_none());
2631        debug_assert!(attach_children.next().is_none());
2632        debug_assert!(insert_children.next().is_none());
2633        debug_assert!(move_children.next().is_none());
2634        debug_assert!(remove_children.next().is_none());
2635        debug_assert!(detach_children.next().is_none());
2636        debug_assert!(sync_children_commands.next().is_none());
2637        debug_assert!(callbacks.next().is_none());
2638        deferred_cleanup.flush(applier)
2639    }
2640}
2641
2642fn command_payload_error(tag: CommandTag) -> NodeError {
2643    NodeError::MalformedCommandPayload { tag: tag.label() }
2644}
2645
2646fn next_command_payload<T>(
2647    payloads: &mut impl Iterator<Item = T>,
2648    tag: CommandTag,
2649) -> Result<T, NodeError> {
2650    payloads.next().ok_or_else(|| command_payload_error(tag))
2651}
2652
2653fn update_typed_node<N: Node + 'static>(node: &mut dyn Node, id: NodeId) -> Result<(), NodeError> {
2654    let typed = node
2655        .as_any_mut()
2656        .downcast_mut::<N>()
2657        .ok_or(NodeError::TypeMismatch {
2658            id,
2659            expected: std::any::type_name::<N>(),
2660        })?;
2661    typed.update();
2662    Ok(())
2663}
2664
2665fn attach_child_at(
2666    applier: &mut dyn Applier,
2667    parent_id: NodeId,
2668    child_id: NodeId,
2669    insert_index: Option<usize>,
2670    bubble: DirtyBubble,
2671) {
2672    if insert_child_with_reparenting(applier, parent_id, child_id) {
2673        if let Some(target) = insert_index {
2674            move_appended_child_to(applier, parent_id, target);
2675        }
2676        bubble.apply(applier, parent_id);
2677    } else if let Ok(child) = applier.get_mut(child_id) {
2678        let dirty_bubble = DirtyBubble {
2679            layout: child.needs_layout(),
2680            measure: child.needs_measure(),
2681            semantics: false,
2682        };
2683        dirty_bubble.apply(applier, parent_id);
2684    }
2685}
2686
2687fn move_appended_child_to(applier: &mut dyn Applier, parent_id: NodeId, target: usize) {
2688    let Ok(parent_node) = applier.get_mut(parent_id) else {
2689        return;
2690    };
2691    let mut owned: SmallVec<[NodeId; 8]> = SmallVec::new();
2692    parent_node.collect_owned_children_into(&mut owned);
2693    let appended_index = owned.len().saturating_sub(1);
2694    if target < appended_index {
2695        parent_node.move_child(appended_index, target);
2696        note_structural_move(parent_id, appended_index, target);
2697    }
2698}
2699
2700fn insert_child_with_reparenting(
2701    applier: &mut dyn Applier,
2702    parent_id: NodeId,
2703    child_id: NodeId,
2704) -> bool {
2705    if parent_id == child_id {
2706        debug_assert_ne!(
2707            parent_id, child_id,
2708            "a node cannot be attached as its own child"
2709        );
2710        return false;
2711    }
2712
2713    let old_parent = applier
2714        .get_mut(child_id)
2715        .ok()
2716        .and_then(|node| node.parent());
2717    if let Some(old_parent_id) = old_parent
2718        && old_parent_id != parent_id
2719    {
2720        let removed = applier
2721            .get_mut(old_parent_id)
2722            .is_ok_and(|old_parent_node| old_parent_node.remove_child(child_id));
2723        if let Ok(child_node) = applier.get_mut(child_id) {
2724            child_node.on_removed_from_parent();
2725        }
2726        if removed {
2727            bubble_layout_dirty(applier, old_parent_id);
2728            bubble_measure_dirty(applier, old_parent_id);
2729            note_structural("reparent-detach", old_parent_id, child_id);
2730            applier.record_structural_change(old_parent_id);
2731        }
2732    }
2733
2734    let inserted = applier
2735        .get_mut(parent_id)
2736        .is_ok_and(|parent_node| parent_node.insert_child(child_id));
2737    if inserted {
2738        note_structural("attach", parent_id, child_id);
2739        applier.record_structural_change(parent_id);
2740    }
2741    if let Ok(child_node) = applier.get_mut(child_id) {
2742        child_node.on_attached_to_parent(parent_id);
2743    }
2744    inserted
2745}
2746
2747fn apply_remove_child(
2748    applier: &mut dyn Applier,
2749    parent_id: NodeId,
2750    child_id: NodeId,
2751    deferred_cleanup: &mut DeferredChildCleanupQueue,
2752) -> Result<(), NodeError> {
2753    detach_child_from_parent(applier, parent_id, child_id)?;
2754
2755    let generation = applier.node_generation(child_id);
2756    let removed_from_parent = if let Ok(node) = applier.get_mut(child_id) {
2757        node.parent().is_none()
2758    } else {
2759        return Ok(());
2760    };
2761    deferred_cleanup.push(child_id, generation, removed_from_parent);
2762    Ok(())
2763}
2764
2765fn detach_child_from_parent(
2766    applier: &mut dyn Applier,
2767    parent_id: NodeId,
2768    child_id: NodeId,
2769) -> Result<(), NodeError> {
2770    let removed = applier
2771        .get_mut(parent_id)
2772        .is_ok_and(|parent_node| parent_node.remove_child(child_id));
2773    if removed {
2774        bubble_layout_dirty(applier, parent_id);
2775        bubble_measure_dirty(applier, parent_id);
2776        note_structural("detach", parent_id, child_id);
2777        applier.record_structural_change(parent_id);
2778    }
2779
2780    if let Ok(node) = applier.get_mut(child_id) {
2781        match node.parent() {
2782            Some(existing_parent_id) if existing_parent_id == parent_id => {
2783                node.on_removed_from_parent();
2784            }
2785            None => {}
2786            Some(_) => return Ok(()),
2787        }
2788    } else {
2789        return Ok(());
2790    }
2791
2792    Ok(())
2793}
2794
2795fn cleanup_detached_child(
2796    applier: &mut dyn Applier,
2797    cleanup: DeferredChildCleanup,
2798) -> Result<(), NodeError> {
2799    if applier.node_generation(cleanup.child_id) != cleanup.generation {
2800        return Ok(());
2801    }
2802
2803    let parent_id = match applier.get_mut(cleanup.child_id) {
2804        Ok(node) => node.parent(),
2805        Err(NodeError::Missing { .. }) => return Ok(()),
2806        Err(err) => return Err(err),
2807    };
2808    if parent_id.is_some() {
2809        return Ok(());
2810    }
2811
2812    if let Ok(node) = applier.get_mut(cleanup.child_id) {
2813        if !cleanup.removed_from_parent {
2814            node.on_removed_from_parent();
2815        }
2816        node.unmount();
2817    }
2818    match applier.remove(cleanup.child_id) {
2819        Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
2820        Err(err) => Err(err),
2821    }
2822}
2823
2824fn remove_child_and_cleanup_now(
2825    applier: &mut dyn Applier,
2826    parent_id: NodeId,
2827    child_id: NodeId,
2828) -> Result<(), NodeError> {
2829    let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2830    apply_remove_child(applier, parent_id, child_id, &mut deferred_cleanup)?;
2831    deferred_cleanup.flush(applier)
2832}
2833
2834fn collect_current_children(applier: &mut dyn Applier, parent_id: NodeId) -> ChildList {
2835    let mut scratch = SmallVec::<[NodeId; 8]>::new();
2836    if let Ok(node) = applier.get_mut(parent_id) {
2837        node.collect_children_into(&mut scratch);
2838    }
2839    let mut current = ChildList::new();
2840    current.extend(scratch);
2841    current
2842}
2843
2844fn sync_children(
2845    applier: &mut dyn Applier,
2846    parent_id: NodeId,
2847    expected_children: &[NodeId],
2848    deferred_cleanup: &mut DeferredChildCleanupQueue,
2849) -> Result<(), NodeError> {
2850    let mut current = collect_current_children(applier, parent_id);
2851    let children_changed = current.as_slice() != expected_children;
2852
2853    if children_changed {
2854        if current.len().max(expected_children.len()) <= SMALL_CHILD_SYNC_LINEAR_THRESHOLD {
2855            sync_children_small(
2856                applier,
2857                parent_id,
2858                &mut current,
2859                expected_children,
2860                deferred_cleanup,
2861            )?;
2862        } else {
2863            let mut target_positions: HashMap<NodeId, usize> = HashMap::default();
2864            target_positions.reserve(expected_children.len());
2865            for (index, &child) in expected_children.iter().enumerate() {
2866                target_positions.insert(child, index);
2867            }
2868
2869            for index in (0..current.len()).rev() {
2870                let child = current[index];
2871                if !target_positions.contains_key(&child) {
2872                    current.remove(index);
2873                    apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2874                }
2875            }
2876
2877            let mut current_positions = build_child_positions(&current);
2878            for (target_index, &child) in expected_children.iter().enumerate() {
2879                if let Some(current_index) = current_positions.get(&child).copied() {
2880                    if current_index != target_index {
2881                        let from_index = current_index;
2882                        let to_index = move_child_in_diff_state(
2883                            &mut current,
2884                            &mut current_positions,
2885                            from_index,
2886                            target_index,
2887                        );
2888                        Command::MoveChild {
2889                            parent_id,
2890                            from_index,
2891                            to_index,
2892                            bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2893                        }
2894                        .apply(applier)?;
2895                    }
2896                } else {
2897                    let insert_index = target_index.min(current.len());
2898                    let appended_index = current.len();
2899                    insert_child_into_diff_state(
2900                        &mut current,
2901                        &mut current_positions,
2902                        insert_index,
2903                        child,
2904                    );
2905                    Command::InsertChild {
2906                        parent_id,
2907                        child_id: child,
2908                        appended_index,
2909                        insert_index,
2910                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2911                    }
2912                    .apply(applier)?;
2913                }
2914            }
2915        }
2916    }
2917
2918    reconcile_children(applier, parent_id, expected_children, !children_changed)
2919}
2920
2921fn sync_children_small(
2922    applier: &mut dyn Applier,
2923    parent_id: NodeId,
2924    current: &mut ChildList,
2925    expected_children: &[NodeId],
2926    deferred_cleanup: &mut DeferredChildCleanupQueue,
2927) -> Result<(), NodeError> {
2928    for index in (0..current.len()).rev() {
2929        let child = current[index];
2930        if !expected_children.contains(&child) {
2931            current.remove(index);
2932            apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2933        }
2934    }
2935
2936    for (target_index, &child) in expected_children.iter().enumerate() {
2937        if let Some(current_index) = current
2938            .iter()
2939            .position(|&current_child| current_child == child)
2940        {
2941            if current_index != target_index {
2942                let child = current.remove(current_index);
2943                let to_index = target_index.min(current.len());
2944                current.insert(to_index, child);
2945                Command::MoveChild {
2946                    parent_id,
2947                    from_index: current_index,
2948                    to_index,
2949                    bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2950                }
2951                .apply(applier)?;
2952            }
2953        } else {
2954            let insert_index = target_index.min(current.len());
2955            let appended_index = current.len();
2956            current.insert(insert_index, child);
2957            Command::InsertChild {
2958                parent_id,
2959                child_id: child,
2960                appended_index,
2961                insert_index,
2962                bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2963            }
2964            .apply(applier)?;
2965        }
2966    }
2967
2968    Ok(())
2969}
2970
2971fn reconcile_children(
2972    applier: &mut dyn Applier,
2973    parent_id: NodeId,
2974    expected_children: &[NodeId],
2975    needs_dirty_check: bool,
2976) -> Result<(), NodeError> {
2977    let mut repaired = false;
2978    for &child_id in expected_children {
2979        let needs_attach = if let Ok(node) = applier.get_mut(child_id) {
2980            node.parent() != Some(parent_id)
2981        } else {
2982            false
2983        };
2984
2985        if needs_attach {
2986            insert_child_with_reparenting(applier, parent_id, child_id);
2987            repaired = true;
2988        }
2989    }
2990
2991    let is_dirty = if needs_dirty_check {
2992        if let Ok(node) = applier.get_mut(parent_id) {
2993            node.needs_layout()
2994        } else {
2995            false
2996        }
2997    } else {
2998        false
2999    };
3000
3001    if repaired {
3002        bubble_layout_dirty(applier, parent_id);
3003        bubble_measure_dirty(applier, parent_id);
3004    } else if is_dirty {
3005        bubble_layout_dirty(applier, parent_id);
3006    }
3007
3008    Ok(())
3009}
3010
3011#[derive(Default)]
3012pub struct MemoryApplier {
3013    nodes: Vec<Option<Box<dyn Node>>>,
3014    physical_stable_ids: Vec<u32>,
3015    physical_warm_recycled_origins: Vec<bool>,
3016    stable_to_physical: HashMap<NodeId, usize>,
3017    stable_generations: HashMap<NodeId, u32>,
3018    free_ids: BinaryHeap<Reverse<usize>>,
3019    high_id_nodes: HashMap<NodeId, Box<dyn Node>>,
3020    high_id_warm_recycled_origins: HashMap<NodeId, bool>,
3021    high_id_generations: HashMap<NodeId, u32>,
3022    next_stable_id: NodeId,
3023    layout_runtime: Option<RuntimeHandle>,
3024    slots: SlotTable,
3025    recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
3026    returning_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
3027    cold_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
3028    recycled_node_limits: HashMap<TypeId, usize>,
3029    warm_recycled_node_targets: HashMap<TypeId, usize>,
3030    fresh_recyclable_creations: HashMap<TypeId, usize>,
3031    recycled_node_prototypes: HashMap<TypeId, Box<dyn Node>>,
3032    structural_change_parents: Vec<NodeId>,
3033    virtual_node_ids: HashSet<NodeId>,
3034}
3035
3036struct RemovalFrame {
3037    node_id: NodeId,
3038    children: SmallVec<[NodeId; 8]>,
3039    next_child: usize,
3040}
3041
3042#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3043pub struct MemoryApplierDebugStats {
3044    pub next_stable_id: NodeId,
3045    pub nodes_len: usize,
3046    pub nodes_cap: usize,
3047    pub physical_stable_ids_len: usize,
3048    pub physical_stable_ids_cap: usize,
3049    pub stable_to_physical_len: usize,
3050    pub stable_to_physical_cap: usize,
3051    pub stable_generations_len: usize,
3052    pub stable_generations_cap: usize,
3053    pub free_ids_len: usize,
3054    pub free_ids_cap: usize,
3055    pub high_id_nodes_len: usize,
3056    pub high_id_nodes_cap: usize,
3057    pub high_id_generations_len: usize,
3058    pub high_id_generations_cap: usize,
3059    pub recycled_type_count: usize,
3060    pub recycled_type_cap: usize,
3061    pub recycled_node_count: usize,
3062    pub recycled_node_capacity: usize,
3063    pub warm_recycled_node_id_count: usize,
3064    pub warm_recycled_node_id_capacity: usize,
3065}
3066
3067impl MemoryApplier {
3068    const EAGER_COMPACT_NODE_LEN: usize = 1_024;
3069    const HIGH_ID_THRESHOLD: NodeId = 1_000_000_000;
3070    const INVALID_STABLE_ID: u32 = u32::MAX;
3071    const INITIAL_DENSE_NODE_CAP: usize = 32;
3072    const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
3073    const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
3074
3075    fn pack_stable_id(stable_id: NodeId) -> u32 {
3076        u32::try_from(stable_id).expect("stable id overflow")
3077    }
3078
3079    fn unpack_stable_id(stable_id: u32) -> NodeId {
3080        stable_id as NodeId
3081    }
3082
3083    fn next_dense_node_target_len(old_len: usize) -> usize {
3084        if old_len < Self::INITIAL_DENSE_NODE_CAP {
3085            return Self::INITIAL_DENSE_NODE_CAP;
3086        }
3087        if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
3088            return old_len.saturating_mul(2);
3089        }
3090
3091        let incremental_growth =
3092            (old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
3093        old_len.saturating_add(incremental_growth)
3094    }
3095
3096    fn ensure_dense_node_storage_capacity(&mut self) {
3097        let len = self
3098            .nodes
3099            .len()
3100            .max(self.physical_stable_ids.len())
3101            .max(self.physical_warm_recycled_origins.len());
3102        if len < self.nodes.capacity()
3103            && len < self.physical_stable_ids.capacity()
3104            && len < self.physical_warm_recycled_origins.capacity()
3105        {
3106            return;
3107        }
3108
3109        let target = Self::next_dense_node_target_len(len);
3110        if self.nodes.capacity() < target {
3111            self.nodes
3112                .reserve_exact(target.saturating_sub(self.nodes.len()));
3113        }
3114        if self.physical_stable_ids.capacity() < target {
3115            self.physical_stable_ids
3116                .reserve_exact(target.saturating_sub(self.physical_stable_ids.len()));
3117        }
3118        if self.physical_warm_recycled_origins.capacity() < target {
3119            self.physical_warm_recycled_origins
3120                .reserve_exact(target.saturating_sub(self.physical_warm_recycled_origins.len()));
3121        }
3122    }
3123
3124    fn ensure_stable_index_capacity(&mut self) {
3125        let len = self
3126            .stable_to_physical
3127            .len()
3128            .max(self.stable_generations.len());
3129        if len < self.stable_to_physical.capacity() && len < self.stable_generations.capacity() {
3130            return;
3131        }
3132
3133        let target = Self::next_dense_node_target_len(len);
3134        let additional = target.saturating_sub(len);
3135        if self.stable_to_physical.capacity() < target {
3136            self.stable_to_physical.reserve(additional);
3137        }
3138        if self.stable_generations.capacity() < target {
3139            self.stable_generations.reserve(additional);
3140        }
3141    }
3142
3143    pub fn new() -> Self {
3144        Self {
3145            nodes: Vec::new(),
3146            physical_stable_ids: Vec::new(),
3147            physical_warm_recycled_origins: Vec::new(),
3148            stable_to_physical: HashMap::default(),
3149            stable_generations: HashMap::default(),
3150            free_ids: BinaryHeap::new(),
3151            high_id_nodes: HashMap::default(),
3152            high_id_warm_recycled_origins: HashMap::default(),
3153            high_id_generations: HashMap::default(),
3154            next_stable_id: 0,
3155            layout_runtime: None,
3156            slots: SlotTable::default(),
3157            recycled_nodes: HashMap::default(),
3158            returning_recycled_nodes: HashMap::default(),
3159            cold_recycled_nodes: HashMap::default(),
3160            recycled_node_limits: HashMap::default(),
3161            warm_recycled_node_targets: HashMap::default(),
3162            fresh_recyclable_creations: HashMap::default(),
3163            recycled_node_prototypes: HashMap::default(),
3164            structural_change_parents: Vec::new(),
3165            virtual_node_ids: HashSet::default(),
3166        }
3167    }
3168
3169    pub fn slots(&mut self) -> &mut SlotTable {
3170        &mut self.slots
3171    }
3172
3173    /// Drains the parents recorded via [`Applier::record_structural_change`],
3174    /// keeping only nodes still attached to `root` (a parent that was itself
3175    /// removed is covered by its own surviving ancestor's record). A virtual
3176    /// parent — a subcompose slot wrapper the render graph never contains —
3177    /// is reported as its nearest non-virtual ancestor: that is the node
3178    /// whose graph child set the change altered, and an id the graph cannot
3179    /// resolve would force the scoped scene update to give up and rebuild.
3180    /// Resolves a scene-scope candidate the way structural records are
3181    /// resolved: to its nearest non-virtual ancestor, and only while still
3182    /// attached to `root`. A node detached after recording must not reach the
3183    /// scoped scene update — an id the graph cannot resolve forces it to give
3184    /// up and rebuild the whole scene.
3185    pub fn scene_node_attached_to(&mut self, node_id: NodeId, root: NodeId) -> Option<NodeId> {
3186        let resolved = self.first_non_virtual_ancestor(node_id)?;
3187        self.is_attached_to(resolved, root).then_some(resolved)
3188    }
3189
3190    pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
3191        let recorded = std::mem::take(&mut self.structural_change_parents);
3192        let mut attached = Vec::with_capacity(recorded.len());
3193        for parent_id in recorded {
3194            let Some(parent_id) = self.first_non_virtual_ancestor(parent_id) else {
3195                continue;
3196            };
3197            if self.is_attached_to(parent_id, root) && !attached.contains(&parent_id) {
3198                attached.push(parent_id);
3199            }
3200        }
3201        attached
3202    }
3203
3204    fn first_non_virtual_ancestor(&mut self, node_id: NodeId) -> Option<NodeId> {
3205        let mut current = node_id;
3206        for _ in 0..100_000 {
3207            if !self.virtual_node_ids.contains(&current) {
3208                return Some(current);
3209            }
3210            match self.get_mut(current) {
3211                Ok(node) => current = node.parent()?,
3212                Err(_) => return None,
3213            }
3214        }
3215        None
3216    }
3217
3218    fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
3219        let mut current = node_id;
3220        for _ in 0..100_000 {
3221            if current == root {
3222                return true;
3223            }
3224            match self.get_mut(current) {
3225                Ok(node) => match node.parent() {
3226                    Some(parent) => current = parent,
3227                    None => return false,
3228                },
3229                Err(_) => return false,
3230            }
3231        }
3232        false
3233    }
3234
3235    pub fn with_node<N: Node + 'static, R>(
3236        &mut self,
3237        id: NodeId,
3238        f: impl FnOnce(&mut N) -> R,
3239    ) -> Result<R, NodeError> {
3240        let physical_id = self
3241            .resolve_node_index(id)
3242            .ok_or(NodeError::Missing { id })?;
3243        let slot = self
3244            .nodes
3245            .get_mut(physical_id)
3246            .ok_or(NodeError::Missing { id })?
3247            .as_deref_mut()
3248            .ok_or(NodeError::Missing { id })?;
3249        let typed = slot
3250            .as_any_mut()
3251            .downcast_mut::<N>()
3252            .ok_or(NodeError::TypeMismatch {
3253                id,
3254                expected: std::any::type_name::<N>(),
3255            })?;
3256        Ok(f(typed))
3257    }
3258
3259    pub fn len(&self) -> usize {
3260        self.nodes.iter().filter(|n| n.is_some()).count()
3261    }
3262
3263    pub fn capacity(&self) -> usize {
3264        self.nodes.len()
3265    }
3266
3267    pub fn tombstone_count(&self) -> usize {
3268        self.nodes.iter().filter(|n| n.is_none()).count()
3269    }
3270
3271    pub fn freelist_len(&self) -> usize {
3272        self.free_ids.len()
3273    }
3274
3275    pub fn debug_recycled_node_count(&self) -> usize {
3276        self.total_recycled_node_count()
3277    }
3278
3279    pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
3280        let key = TypeId::of::<N>();
3281        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3282            + self
3283                .returning_recycled_nodes
3284                .get(&key)
3285                .map(Vec::len)
3286                .unwrap_or(0)
3287            + self
3288                .cold_recycled_nodes
3289                .get(&key)
3290                .map(Vec::len)
3291                .unwrap_or(0)
3292    }
3293
3294    pub fn debug_stats(&self) -> MemoryApplierDebugStats {
3295        let mut recycled_keys: HashSet<TypeId> = HashSet::default();
3296        recycled_keys.extend(self.recycled_nodes.keys().copied());
3297        recycled_keys.extend(self.returning_recycled_nodes.keys().copied());
3298        recycled_keys.extend(self.cold_recycled_nodes.keys().copied());
3299
3300        MemoryApplierDebugStats {
3301            next_stable_id: self.next_stable_id,
3302            nodes_len: self.len(),
3303            nodes_cap: self.nodes.len(),
3304            physical_stable_ids_len: self.physical_stable_ids.len(),
3305            physical_stable_ids_cap: self.physical_stable_ids.capacity(),
3306            stable_to_physical_len: self.stable_to_physical.len(),
3307            stable_to_physical_cap: self.stable_to_physical.capacity(),
3308            stable_generations_len: self.stable_generations.len(),
3309            stable_generations_cap: self.stable_generations.capacity(),
3310            free_ids_len: self.free_ids.len(),
3311            free_ids_cap: self.free_ids.capacity(),
3312            high_id_nodes_len: self.high_id_nodes.len(),
3313            high_id_nodes_cap: self.high_id_nodes.capacity(),
3314            high_id_generations_len: self.high_id_generations.len(),
3315            high_id_generations_cap: self.high_id_generations.capacity(),
3316            recycled_type_count: recycled_keys.len(),
3317            recycled_type_cap: self.recycled_nodes.capacity()
3318                + self.returning_recycled_nodes.capacity()
3319                + self.cold_recycled_nodes.capacity(),
3320            recycled_node_count: self.total_recycled_node_count(),
3321            recycled_node_capacity: self.total_recycled_node_capacity(),
3322            warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
3323            warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
3324        }
3325    }
3326
3327    pub fn is_empty(&self) -> bool {
3328        self.len() == 0
3329    }
3330
3331    pub fn debug_live_node_heap_bytes(&self) -> usize {
3332        let dense_nodes = self
3333            .nodes
3334            .iter()
3335            .flatten()
3336            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3337            .sum::<usize>();
3338        let high_id_nodes = self
3339            .high_id_nodes
3340            .values()
3341            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3342            .sum::<usize>();
3343        dense_nodes + high_id_nodes
3344    }
3345
3346    pub fn debug_recycled_node_heap_bytes(&self) -> usize {
3347        let pool_bytes = |pools: &HashMap<TypeId, Vec<RecycledNode>>| {
3348            pools
3349                .values()
3350                .flat_map(|nodes| nodes.iter())
3351                .map(|node| std::mem::size_of_val(&*node.node) + node.node.debug_heap_bytes())
3352                .sum::<usize>()
3353        };
3354
3355        pool_bytes(&self.recycled_nodes)
3356            + pool_bytes(&self.returning_recycled_nodes)
3357            + pool_bytes(&self.cold_recycled_nodes)
3358    }
3359
3360    pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
3361        self.layout_runtime = Some(handle);
3362    }
3363
3364    pub fn clear_runtime_handle(&mut self) {
3365        self.layout_runtime = None;
3366    }
3367
3368    pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
3369        self.layout_runtime.clone()
3370    }
3371
3372    fn pool_node_count(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3373        pools.values().map(Vec::len).sum()
3374    }
3375
3376    fn pool_node_capacity(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3377        pools.values().map(Vec::capacity).sum()
3378    }
3379
3380    fn total_recycled_node_count(&self) -> usize {
3381        Self::pool_node_count(&self.recycled_nodes)
3382            + Self::pool_node_count(&self.returning_recycled_nodes)
3383            + Self::pool_node_count(&self.cold_recycled_nodes)
3384    }
3385
3386    fn total_recycled_node_capacity(&self) -> usize {
3387        Self::pool_node_capacity(&self.recycled_nodes)
3388            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3389            + Self::pool_node_capacity(&self.cold_recycled_nodes)
3390    }
3391
3392    fn total_warm_recycled_node_id_count(&self) -> usize {
3393        self.live_warm_recycled_origin_count()
3394            + Self::pool_node_count(&self.recycled_nodes)
3395            + Self::pool_node_count(&self.returning_recycled_nodes)
3396    }
3397
3398    fn total_warm_recycled_node_id_capacity(&self) -> usize {
3399        self.live_warm_recycled_origin_capacity()
3400            + Self::pool_node_capacity(&self.recycled_nodes)
3401            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3402    }
3403
3404    fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
3405        if let Some(limit) = recycle_pool_limit {
3406            self.recycled_node_limits.insert(key, limit);
3407        } else {
3408            self.recycled_node_limits.remove(&key);
3409        }
3410    }
3411
3412    fn recycle_pool_limit_for(&self, key: TypeId) -> Option<usize> {
3413        self.recycled_node_limits.get(&key).copied()
3414    }
3415
3416    fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
3417        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3418    }
3419
3420    fn warm_recycled_node_target(&self, key: TypeId) -> usize {
3421        self.warm_recycled_node_targets
3422            .get(&key)
3423            .copied()
3424            .unwrap_or(0)
3425    }
3426
3427    fn warm_recycled_node_target_limit(&self, key: TypeId) -> usize {
3428        let Some(limit) = self.recycle_pool_limit_for(key) else {
3429            return usize::MAX;
3430        };
3431        if limit <= 8 { limit } else { limit / 4 }
3432    }
3433
3434    fn update_warm_recycled_node_target(&mut self, key: TypeId, observed_demand: usize) -> usize {
3435        let target_limit = self.warm_recycled_node_target_limit(key);
3436        let existing = self.warm_recycled_node_target(key).min(target_limit);
3437        if observed_demand == 0 {
3438            return existing;
3439        }
3440
3441        let target = match self.recycle_pool_limit_for(key) {
3442            Some(limit) if limit > 8 => target_limit,
3443            Some(_) => observed_demand.min(target_limit),
3444            None => observed_demand,
3445        };
3446        self.warm_recycled_node_targets.insert(key, target);
3447        target
3448    }
3449
3450    fn remember_recycled_node_prototype(&mut self, key: TypeId, shell: &dyn Node) {
3451        if self.recycled_node_prototypes.contains_key(&key) {
3452            return;
3453        }
3454        if let Some(prototype) = shell.rehouse_for_recycle() {
3455            self.recycled_node_prototypes.insert(key, prototype);
3456        }
3457    }
3458
3459    fn live_warm_recycled_origin_count(&self) -> usize {
3460        self.physical_warm_recycled_origins
3461            .iter()
3462            .zip(self.nodes.iter())
3463            .filter(|(warm_origin, node)| **warm_origin && node.is_some())
3464            .count()
3465            + self
3466                .high_id_warm_recycled_origins
3467                .values()
3468                .filter(|warm_origin| **warm_origin)
3469                .count()
3470    }
3471
3472    fn live_warm_recycled_origin_capacity(&self) -> usize {
3473        self.physical_warm_recycled_origins.capacity()
3474            + self.high_id_warm_recycled_origins.capacity()
3475    }
3476
3477    fn push_recycled_node(
3478        &mut self,
3479        key: TypeId,
3480        recycle_pool_limit: Option<usize>,
3481        recycled: RecycledNode,
3482    ) {
3483        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3484        self.remember_recycled_node_prototype(key, recycled.node.as_ref());
3485
3486        let warm_origin = recycled.warm_origin();
3487        let pool = if warm_origin {
3488            self.returning_recycled_nodes.entry(key).or_default()
3489        } else {
3490            self.cold_recycled_nodes.entry(key).or_default()
3491        };
3492        pool.push(recycled);
3493        if let Some(limit) = recycle_pool_limit
3494            && pool.len() > limit
3495        {
3496            let excess = pool.len() - limit;
3497            let dropped: Vec<_> = pool.drain(0..excess).collect();
3498            drop(dropped);
3499        }
3500    }
3501
3502    fn push_warm_recycled_node(
3503        &mut self,
3504        key: TypeId,
3505        recycle_pool_limit: Option<usize>,
3506        mut recycled: RecycledNode,
3507    ) {
3508        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3509
3510        recycled.set_warm_origin(true);
3511        let mut dropped = Vec::new();
3512        let mut remove_pool_entry = false;
3513        {
3514            let pool = self.recycled_nodes.entry(key).or_default();
3515            pool.push(recycled);
3516            if let Some(limit) = recycle_pool_limit
3517                && pool.len() > limit
3518            {
3519                let excess = pool.len() - limit;
3520                dropped = pool.drain(0..excess).collect();
3521                remove_pool_entry = pool.is_empty();
3522            }
3523        }
3524        if remove_pool_entry {
3525            self.recycled_nodes.remove(&key);
3526        }
3527        drop(dropped);
3528    }
3529
3530    fn seed_recycled_node_shell_impl(
3531        &mut self,
3532        key: TypeId,
3533        recycle_pool_limit: Option<usize>,
3534        shell: Box<dyn Node>,
3535    ) {
3536        let limit = recycle_pool_limit.unwrap_or(usize::MAX);
3537        if self.warm_recycled_pool_len(key) >= limit {
3538            return;
3539        }
3540
3541        self.remember_recycled_node_prototype(key, shell.as_ref());
3542        let stable_id = self.next_stable_id;
3543        self.next_stable_id = self.next_stable_id.saturating_add(1);
3544        self.push_warm_recycled_node(
3545            key,
3546            recycle_pool_limit,
3547            RecycledNode::from_shell(stable_id, shell, true),
3548        );
3549    }
3550
3551    fn take_recycled_node_from_pool(
3552        pools: &mut HashMap<TypeId, Vec<RecycledNode>>,
3553        key: TypeId,
3554    ) -> Option<RecycledNode> {
3555        let pool = pools.get_mut(&key)?;
3556        let node = pool.pop();
3557        if pool.is_empty() {
3558            pools.remove(&key);
3559        }
3560        node
3561    }
3562
3563    fn compact_idle_warm_pool(&mut self, key: TypeId) {
3564        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3565            return;
3566        };
3567        if pool.capacity() <= pool.len().saturating_mul(4).max(64) {
3568            return;
3569        }
3570
3571        let retained = pool.len();
3572        let mut compacted = Vec::with_capacity(retained);
3573        compacted.append(pool);
3574        let remove_pool_entry = compacted.is_empty();
3575        *pool = compacted;
3576        let _ = pool;
3577
3578        if remove_pool_entry {
3579            self.recycled_nodes.remove(&key);
3580        }
3581    }
3582
3583    fn trim_idle_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3584        let pool_len = self.warm_recycled_pool_len(key);
3585        if pool_len <= target {
3586            return;
3587        }
3588
3589        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3590            return;
3591        };
3592        let removable = (pool_len - target).min(pool.len());
3593        let dropped: Vec<_> = pool.drain(0..removable).collect();
3594        let remove_pool_entry = pool.is_empty();
3595        let _ = pool;
3596
3597        if remove_pool_entry {
3598            self.recycled_nodes.remove(&key);
3599        }
3600        drop(dropped);
3601    }
3602
3603    fn replenish_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3604        let missing = target.saturating_sub(self.warm_recycled_pool_len(key));
3605        if missing == 0 {
3606            return;
3607        }
3608
3609        let recycle_pool_limit = self.recycle_pool_limit_for(key);
3610        let mut shells = Vec::with_capacity(missing);
3611        if let Some(prototype) = self.recycled_node_prototypes.get(&key) {
3612            for _ in 0..missing {
3613                let Some(shell) = prototype.rehouse_for_recycle() else {
3614                    break;
3615                };
3616                shells.push(shell);
3617            }
3618        }
3619
3620        for shell in shells {
3621            self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3622        }
3623    }
3624
3625    fn prune_stable_generations(&mut self) {
3626        let retained_len = self.stable_to_physical.len() + self.total_recycled_node_count();
3627        if retained_len == self.stable_generations.len() {
3628            return;
3629        }
3630
3631        let mut retained = HashMap::default();
3632        retained.reserve(retained_len);
3633        for stable_id in self.stable_to_physical.keys().copied() {
3634            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3635                retained.insert(stable_id, generation);
3636            }
3637        }
3638        for stable_id in self
3639            .recycled_nodes
3640            .values()
3641            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3642        {
3643            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3644                retained.insert(stable_id, generation);
3645            }
3646        }
3647        for stable_id in self
3648            .returning_recycled_nodes
3649            .values()
3650            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3651        {
3652            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3653                retained.insert(stable_id, generation);
3654            }
3655        }
3656        for stable_id in self
3657            .cold_recycled_nodes
3658            .values()
3659            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3660        {
3661            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3662                retained.insert(stable_id, generation);
3663            }
3664        }
3665        self.stable_generations = retained;
3666    }
3667
3668    pub fn dump_tree(&self, root: Option<NodeId>) -> String {
3669        let mut output = String::new();
3670        if let Some(root_id) = root {
3671            self.dump_node(&mut output, root_id, 0);
3672        } else {
3673            output.push_str("(no root)\n");
3674        }
3675        output
3676    }
3677
3678    fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
3679        let indent = "  ".repeat(depth);
3680        if let Some(physical_id) = self.resolve_node_index(id) {
3681            if let Some(node) = self.nodes.get(physical_id).and_then(Option::as_ref) {
3682                let type_name = std::any::type_name_of_val(&**node);
3683                output.push_str(&format!("{}[{}] {}\n", indent, id, type_name));
3684
3685                let children = node.children();
3686                for child_id in children {
3687                    self.dump_node(output, child_id, depth + 1);
3688                }
3689            } else {
3690                output.push_str(&format!(
3691                    "{}[{}] (missing physical node {})\n",
3692                    indent, id, physical_id
3693                ));
3694            }
3695        } else {
3696            output.push_str(&format!("{}[{}] (missing)\n", indent, id));
3697        }
3698    }
3699
3700    fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
3701        self.stable_to_physical.get(&id).copied()
3702    }
3703
3704    fn contains_node_id(&self, id: NodeId) -> bool {
3705        self.resolve_node_index(id).is_some() || self.high_id_nodes.contains_key(&id)
3706    }
3707
3708    fn insert_high_id_node(&mut self, stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) {
3709        self.high_id_nodes.insert(stable_id, node);
3710        self.high_id_warm_recycled_origins
3711            .insert(stable_id, warm_origin);
3712        self.high_id_generations.entry(stable_id).or_insert(0);
3713    }
3714
3715    fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
3716        if stable_id >= Self::HIGH_ID_THRESHOLD {
3717            self.insert_high_id_node(stable_id, node, false);
3718            return;
3719        }
3720
3721        let physical_id = if let Some(Reverse(free_physical_id)) = self.free_ids.pop() {
3722            self.nodes[free_physical_id] = Some(node);
3723            self.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
3724            self.physical_warm_recycled_origins[free_physical_id] = false;
3725            free_physical_id
3726        } else {
3727            self.ensure_dense_node_storage_capacity();
3728            let physical_id = self.nodes.len();
3729            self.nodes.push(Some(node));
3730            self.physical_stable_ids
3731                .push(Self::pack_stable_id(stable_id));
3732            self.physical_warm_recycled_origins.push(false);
3733            physical_id
3734        };
3735
3736        self.next_stable_id = self.next_stable_id.max(stable_id.saturating_add(1));
3737        self.ensure_stable_index_capacity();
3738        self.stable_generations.entry(stable_id).or_insert(0);
3739        self.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
3740        self.stable_to_physical.insert(stable_id, physical_id);
3741    }
3742
3743    fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
3744        if let Some(physical_id) = self.resolve_node_index(id) {
3745            let slot = self
3746                .nodes
3747                .get(physical_id)
3748                .ok_or(NodeError::Missing { id })?
3749                .as_deref()
3750                .ok_or(NodeError::Missing { id })?;
3751            return Ok(slot);
3752        }
3753
3754        self.high_id_nodes
3755            .get(&id)
3756            .map(|node| node.as_ref())
3757            .ok_or(NodeError::Missing { id })
3758    }
3759
3760    fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
3761        Ok(self.get_ref(id)?.parent())
3762    }
3763
3764    fn collect_owned_children(
3765        &self,
3766        node_id: NodeId,
3767        out: &mut SmallVec<[NodeId; 8]>,
3768    ) -> Result<(), NodeError> {
3769        self.get_ref(node_id)?.collect_owned_children_into(out);
3770        out.retain(|child_id| {
3771            self.node_parent(*child_id)
3772                .map(|parent| parent == Some(node_id))
3773                .unwrap_or(false)
3774        });
3775        Ok(())
3776    }
3777
3778    fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
3779        self.virtual_node_ids.remove(&node_id);
3780        if self.high_id_nodes.contains_key(&node_id) {
3781            if let Some(mut node) = self.high_id_nodes.remove(&node_id)
3782                && let Some(key) = node.recycle_key()
3783            {
3784                let recycle_pool_limit = node.recycle_pool_limit();
3785                let warm_origin = self
3786                    .high_id_warm_recycled_origins
3787                    .remove(&node_id)
3788                    .unwrap_or(false);
3789                node.prepare_for_recycle();
3790                self.push_recycled_node(
3791                    key,
3792                    recycle_pool_limit,
3793                    RecycledNode::new(node_id, node, warm_origin),
3794                );
3795            }
3796            let generation = self.high_id_generations.entry(node_id).or_insert(0);
3797            *generation = generation.wrapping_add(1);
3798            return Ok(());
3799        }
3800
3801        let physical_id = self
3802            .resolve_node_index(node_id)
3803            .ok_or(NodeError::Missing { id: node_id })?;
3804        if let Some(mut node) = self.nodes[physical_id].take()
3805            && let Some(key) = node.recycle_key()
3806        {
3807            let recycle_pool_limit = node.recycle_pool_limit();
3808            let warm_origin = self
3809                .physical_warm_recycled_origins
3810                .get_mut(physical_id)
3811                .map(std::mem::take)
3812                .unwrap_or(false);
3813            node.prepare_for_recycle();
3814            self.push_recycled_node(
3815                key,
3816                recycle_pool_limit,
3817                RecycledNode::new(node_id, node, warm_origin),
3818            );
3819        }
3820        self.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
3821        self.stable_to_physical.remove(&node_id);
3822        if let Some(generation) = self.stable_generations.get_mut(&node_id) {
3823            *generation = generation.wrapping_add(1);
3824        } else {
3825            self.stable_generations.insert(node_id, 1);
3826        }
3827        self.free_ids.push(Reverse(physical_id));
3828        Ok(())
3829    }
3830
3831    fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
3832        self.get_ref(id)?;
3833
3834        let mut root_children = SmallVec::<[NodeId; 8]>::new();
3835        self.collect_owned_children(id, &mut root_children)?;
3836
3837        let mut stack = Vec::new();
3838        stack.push(RemovalFrame {
3839            node_id: id,
3840            children: root_children,
3841            next_child: 0,
3842        });
3843        let mut max_depth = stack.len();
3844
3845        while let Some(frame) = stack.last_mut() {
3846            if frame.next_child < frame.children.len() {
3847                let child_id = frame.children[frame.next_child];
3848                frame.next_child += 1;
3849
3850                if let Ok(child) = self.get_mut(child_id) {
3851                    child.on_removed_from_parent();
3852                    child.unmount();
3853                }
3854
3855                let mut child_children = SmallVec::<[NodeId; 8]>::new();
3856                self.collect_owned_children(child_id, &mut child_children)?;
3857                stack.push(RemovalFrame {
3858                    node_id: child_id,
3859                    children: child_children,
3860                    next_child: 0,
3861                });
3862                max_depth = max_depth.max(stack.len());
3863                continue;
3864            }
3865
3866            let node_id = frame.node_id;
3867            stack.pop();
3868            self.remove_node_storage(node_id)?;
3869        }
3870
3871        Ok(max_depth)
3872    }
3873
3874    #[cfg(test)]
3875    fn debug_remove_max_traversal_depth(&mut self, id: NodeId) -> Result<usize, NodeError> {
3876        self.remove_subtree_postorder(id)
3877    }
3878}
3879
3880impl Applier for MemoryApplier {
3881    fn record_structural_change(&mut self, parent_id: NodeId) {
3882        if self.structural_change_parents.last() != Some(&parent_id) {
3883            self.structural_change_parents.push(parent_id);
3884        }
3885    }
3886
3887    fn create(&mut self, node: Box<dyn Node>) -> NodeId {
3888        let stable_id = self.next_stable_id;
3889        self.next_stable_id = self.next_stable_id.saturating_add(1);
3890        if stable_id >= Self::HIGH_ID_THRESHOLD {
3891            self.insert_high_id_node(stable_id, node, false);
3892            return stable_id;
3893        }
3894
3895        self.ensure_stable_index_capacity();
3896        self.stable_generations.insert(stable_id, 0);
3897
3898        let physical_id = if let Some(Reverse(id)) = self.free_ids.pop() {
3899            debug_assert!(self.nodes[id].is_none(), "freelist entry {id} is not None");
3900            self.nodes[id] = Some(node);
3901            self.physical_stable_ids[id] = Self::pack_stable_id(stable_id);
3902            self.physical_warm_recycled_origins[id] = false;
3903            id
3904        } else {
3905            self.ensure_dense_node_storage_capacity();
3906            let id = self.nodes.len();
3907            self.nodes.push(Some(node));
3908            self.physical_stable_ids
3909                .push(Self::pack_stable_id(stable_id));
3910            self.physical_warm_recycled_origins.push(false);
3911            id
3912        };
3913        self.stable_to_physical.insert(stable_id, physical_id);
3914        stable_id
3915    }
3916
3917    fn node_generation(&self, id: NodeId) -> u32 {
3918        self.high_id_generations
3919            .get(&id)
3920            .copied()
3921            .or_else(|| self.stable_generations.get(&id).copied())
3922            .unwrap_or(0)
3923    }
3924
3925    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
3926        if let Some(physical_id) = self.resolve_node_index(id) {
3927            let slot = self.nodes[physical_id]
3928                .as_deref_mut()
3929                .ok_or(NodeError::Missing { id })?;
3930            return Ok(slot);
3931        }
3932        self.high_id_nodes
3933            .get_mut(&id)
3934            .map(|n| n.as_mut())
3935            .ok_or(NodeError::Missing { id })
3936    }
3937
3938    fn remove(&mut self, id: NodeId) -> Result<(), NodeError> {
3939        self.remove_subtree_postorder(id).map(|_| ())
3940    }
3941
3942    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError> {
3943        if self.contains_node_id(id) {
3944            return Err(NodeError::AlreadyExists { id });
3945        }
3946        self.insert_available_with_id(id, node);
3947        self.virtual_node_ids.insert(id);
3948        Ok(())
3949    }
3950
3951    fn insert_recycled_node_or_create(
3952        &mut self,
3953        stable_id: NodeId,
3954        node: Box<dyn Node>,
3955    ) -> RecycledNodeInsertion {
3956        if self.contains_node_id(stable_id) {
3957            let id = self.create(node);
3958            return RecycledNodeInsertion::fresh(
3959                id,
3960                Some(NodeError::AlreadyExists { id: stable_id }),
3961            );
3962        }
3963
3964        self.insert_available_with_id(stable_id, node);
3965        RecycledNodeInsertion::reused(stable_id)
3966    }
3967
3968    fn compact(&mut self) {
3969        let live_count = self.nodes.iter().filter(|slot| slot.is_some()).count();
3970        let tombstone_count = self.nodes.len().saturating_sub(live_count);
3971        if tombstone_count == 0 {
3972            return;
3973        }
3974        if self.nodes.len() > Self::EAGER_COMPACT_NODE_LEN && tombstone_count < live_count {
3975            return;
3976        }
3977        let rehouse_live_nodes = tombstone_count >= live_count;
3978        let mut packed_nodes = Vec::with_capacity(live_count);
3979        let mut packed_physical_stable_ids = Vec::with_capacity(live_count);
3980        let mut packed_warm_recycled_origins = Vec::with_capacity(live_count);
3981        let mut stable_to_physical = HashMap::default();
3982        stable_to_physical.reserve(live_count);
3983
3984        for physical_id in 0..self.nodes.len() {
3985            let Some(mut node) = self.nodes[physical_id].take() else {
3986                continue;
3987            };
3988            if rehouse_live_nodes && let Some(rehoused) = node.rehouse_for_live_compaction() {
3989                node = rehoused;
3990            }
3991            let stable_id = std::mem::replace(
3992                &mut self.physical_stable_ids[physical_id],
3993                Self::INVALID_STABLE_ID,
3994            );
3995            debug_assert_ne!(
3996                stable_id,
3997                Self::INVALID_STABLE_ID,
3998                "live physical slot must have a stable id",
3999            );
4000            let stable_id = Self::unpack_stable_id(stable_id);
4001            packed_nodes.push(Some(node));
4002            packed_physical_stable_ids.push(Self::pack_stable_id(stable_id));
4003            packed_warm_recycled_origins.push(self.physical_warm_recycled_origins[physical_id]);
4004            stable_to_physical.insert(stable_id, packed_nodes.len() - 1);
4005        }
4006
4007        self.nodes = packed_nodes;
4008        self.physical_stable_ids = packed_physical_stable_ids;
4009        self.physical_warm_recycled_origins = packed_warm_recycled_origins;
4010        self.free_ids = BinaryHeap::new();
4011        self.stable_to_physical = stable_to_physical;
4012        self.prune_stable_generations();
4013    }
4014
4015    fn take_recycled_node(&mut self, key: TypeId) -> Option<RecycledNode> {
4016        Self::take_recycled_node_from_pool(&mut self.returning_recycled_nodes, key)
4017            .or_else(|| Self::take_recycled_node_from_pool(&mut self.recycled_nodes, key))
4018    }
4019
4020    fn set_recycled_node_origin(&mut self, id: NodeId, warm_origin: bool) {
4021        if let Some(physical_id) = self.resolve_node_index(id) {
4022            self.physical_warm_recycled_origins[physical_id] = warm_origin;
4023        } else if self.high_id_nodes.contains_key(&id) {
4024            self.high_id_warm_recycled_origins.insert(id, warm_origin);
4025        }
4026    }
4027
4028    fn seed_recycled_node_shell(
4029        &mut self,
4030        key: TypeId,
4031        recycle_pool_limit: Option<usize>,
4032        shell: Box<dyn Node>,
4033    ) {
4034        self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
4035    }
4036
4037    fn record_fresh_recyclable_creation(&mut self, key: TypeId) {
4038        *self.fresh_recyclable_creations.entry(key).or_insert(0) += 1;
4039    }
4040
4041    fn clear_recycled_nodes(&mut self) {
4042        let returning = std::mem::take(&mut self.returning_recycled_nodes);
4043        for (key, mut nodes) in returning {
4044            let pool = self.recycled_nodes.entry(key).or_default();
4045            pool.append(&mut nodes);
4046        }
4047
4048        let fresh_recyclable_creations = std::mem::take(&mut self.fresh_recyclable_creations);
4049        let cold = std::mem::take(&mut self.cold_recycled_nodes);
4050        for (key, mut nodes) in cold {
4051            let needed = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
4052            if needed > 0 {
4053                let remaining_limit = self
4054                    .recycle_pool_limit_for(key)
4055                    .unwrap_or(usize::MAX)
4056                    .saturating_sub(self.warm_recycled_pool_len(key));
4057                let promote = nodes.len().min(needed).min(remaining_limit);
4058                let split_at = nodes.len().saturating_sub(promote);
4059                let promoted = nodes.split_off(split_at);
4060                for mut recycled in promoted {
4061                    recycled.set_warm_origin(true);
4062                    self.recycled_nodes.entry(key).or_default().push(recycled);
4063                }
4064            }
4065        }
4066
4067        let mut keys: HashSet<TypeId> = HashSet::default();
4068        keys.extend(self.recycled_nodes.keys().copied());
4069        keys.extend(self.recycled_node_limits.keys().copied());
4070        keys.extend(self.warm_recycled_node_targets.keys().copied());
4071        keys.extend(self.recycled_node_prototypes.keys().copied());
4072        for key in keys {
4073            let observed_demand = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
4074            let target = self.update_warm_recycled_node_target(key, observed_demand);
4075            self.replenish_warm_pool_to_target(key, target);
4076            self.trim_idle_warm_pool_to_target(key, target);
4077            self.compact_idle_warm_pool(key);
4078        }
4079        self.prune_stable_generations();
4080        self.compact();
4081    }
4082}
4083
4084pub trait ApplierHost {
4085    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier>;
4086    /// Compact internal storage after commands have been applied.
4087    fn compact(&self) {}
4088}
4089
4090pub struct ConcreteApplierHost<A: Applier + 'static> {
4091    inner: RefCell<A>,
4092}
4093
4094impl<A: Applier + 'static> ConcreteApplierHost<A> {
4095    pub fn new(applier: A) -> Self {
4096        Self {
4097            inner: RefCell::new(applier),
4098        }
4099    }
4100
4101    pub fn borrow_typed(&self) -> RefMut<'_, A> {
4102        self.inner.borrow_mut()
4103    }
4104
4105    pub fn try_borrow_typed(&self) -> Result<RefMut<'_, A>, std::cell::BorrowMutError> {
4106        self.inner.try_borrow_mut()
4107    }
4108
4109    pub fn into_inner(self) -> A {
4110        self.inner.into_inner()
4111    }
4112}
4113
4114impl<A: Applier + 'static> ApplierHost for ConcreteApplierHost<A> {
4115    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier> {
4116        RefMut::map(self.inner.borrow_mut(), |applier| {
4117            applier as &mut dyn Applier
4118        })
4119    }
4120
4121    fn compact(&self) {
4122        self.inner.borrow_mut().compact();
4123    }
4124}
4125
4126pub struct ApplierGuard<'a, A: Applier + 'static> {
4127    inner: RefMut<'a, A>,
4128}
4129
4130impl<'a, A: Applier + 'static> ApplierGuard<'a, A> {
4131    fn new(inner: RefMut<'a, A>) -> Self {
4132        Self { inner }
4133    }
4134}
4135
4136impl<'a, A: Applier + 'static> Deref for ApplierGuard<'a, A> {
4137    type Target = A;
4138
4139    fn deref(&self) -> &Self::Target {
4140        &self.inner
4141    }
4142}
4143
4144impl<'a, A: Applier + 'static> DerefMut for ApplierGuard<'a, A> {
4145    fn deref_mut(&mut self) -> &mut Self::Target {
4146        &mut self.inner
4147    }
4148}
4149
4150pub struct SlotsHost {
4151    storage_key: Cell<usize>,
4152    inner: RefCell<SlotsHostInner>,
4153}
4154
4155#[derive(Debug, Default)]
4156pub(crate) struct SlotPassOutcome {
4157    pub(crate) compacted: bool,
4158    pub(crate) compact_anchor_registry_storage: bool,
4159    pub(crate) compact_payload_storage: bool,
4160}
4161
4162#[derive(Default)]
4163pub(crate) struct FinishedSlotPass {
4164    pub(crate) outcome: SlotPassOutcome,
4165    pub(crate) detached_root_children: Vec<slot::DetachedSubtree>,
4166}
4167
4168struct ActivePassState {
4169    state: slot::SlotWriteSessionState,
4170}
4171
4172struct SlotsHostInner {
4173    table: SlotTable,
4174    nested_hosts: Vec<std::rc::Weak<SlotsHost>>,
4175    lifecycle: slot::SlotLifecycleCoordinator,
4176    runtime_state: Option<Rc<crate::composer::ComposerRuntimeState>>,
4177    active_pass: Option<ActivePassState>,
4178}
4179
4180impl Drop for SlotsHost {
4181    fn drop(&mut self) {
4182        let storage_key = self.storage_key.get();
4183        let inner = self.inner.get_mut();
4184        if let Some(state) = inner.runtime_state.clone() {
4185            if let Err(err) = state.dispose_retained_subtrees_for_host(
4186                storage_key,
4187                &mut inner.table,
4188                &mut inner.lifecycle,
4189            ) {
4190                log::error!(
4191                    "retained subtree disposal failed while dropping SlotsHost {storage_key}: {err}"
4192                );
4193                state.abandon_retained_subtrees_for_host(
4194                    storage_key,
4195                    &mut inner.table,
4196                    &mut inner.lifecycle,
4197                );
4198            } else {
4199                state.clear_host_storage_key(storage_key);
4200            }
4201        }
4202        inner.lifecycle.dispose_slot_table(&mut inner.table);
4203    }
4204}
4205
4206impl SlotsHost {
4207    pub fn storage_key(&self) -> usize {
4208        self.storage_key.get()
4209    }
4210
4211    pub fn new(storage: SlotTable) -> Self {
4212        let storage_key = storage.storage_id();
4213        Self {
4214            storage_key: Cell::new(storage_key),
4215            inner: RefCell::new(SlotsHostInner {
4216                table: storage,
4217                nested_hosts: Vec::new(),
4218                lifecycle: slot::SlotLifecycleCoordinator::default(),
4219                runtime_state: None,
4220                active_pass: None,
4221            }),
4222        }
4223    }
4224
4225    pub fn note_nested_host(&self, nested: &Rc<SlotsHost>) {
4226        let Ok(mut inner) = self.inner.try_borrow_mut() else {
4227            return;
4228        };
4229        inner.nested_hosts.retain(|held| held.upgrade().is_some());
4230        if inner
4231            .nested_hosts
4232            .iter()
4233            .any(|held| held.upgrade().is_some_and(|host| Rc::ptr_eq(&host, nested)))
4234        {
4235            return;
4236        }
4237        inner.nested_hosts.push(Rc::downgrade(nested));
4238    }
4239
4240    pub(crate) fn forget_effects(&self) -> bool {
4241        let (forgotten, nested, runtime_state) = {
4242            let Ok(mut inner) = self.inner.try_borrow_mut() else {
4243                return false;
4244            };
4245            if inner.active_pass.is_some() {
4246                return false;
4247            }
4248            let drops = inner.table.take_effect_drops();
4249            inner.nested_hosts.retain(|held| held.upgrade().is_some());
4250            let nested: Vec<Rc<SlotsHost>> = inner
4251                .nested_hosts
4252                .iter()
4253                .filter_map(std::rc::Weak::upgrade)
4254                .collect();
4255            (drops, nested, inner.runtime_state.clone())
4256        };
4257        let mut any = !forgotten.is_empty();
4258        drop(forgotten);
4259        for host in nested {
4260            any |= host.forget_effects();
4261        }
4262        if any && let Some(runtime_state) = runtime_state {
4263            runtime_state.force_recompose_host_scopes(self.storage_key());
4264        }
4265        any
4266    }
4267
4268    pub(crate) fn bind_runtime_state(&self, state: &Rc<crate::composer::ComposerRuntimeState>) {
4269        let mut inner = self.inner.borrow_mut();
4270        inner.runtime_state = Some(Rc::clone(state));
4271    }
4272
4273    pub(crate) fn rebind_orphaned_runtime_state(
4274        &self,
4275        state: &Rc<crate::composer::ComposerRuntimeState>,
4276    ) -> bool {
4277        let inner = self.inner.borrow();
4278        if inner.active_pass.is_some() {
4279            log::error!("cannot rebind SlotsHost during an active pass");
4280            return false;
4281        }
4282        let Some(bound_state) = inner.runtime_state.as_ref() else {
4283            drop(inner);
4284            self.bind_runtime_state(state);
4285            return true;
4286        };
4287        if Rc::ptr_eq(bound_state, state) {
4288            return true;
4289        }
4290        if bound_state.has_live_applier_host() {
4291            return false;
4292        }
4293        drop(inner);
4294
4295        let mut inner = self.inner.borrow_mut();
4296        let Some(bound_state) = inner.runtime_state.as_ref() else {
4297            inner.runtime_state = Some(Rc::clone(state));
4298            return true;
4299        };
4300        if Rc::ptr_eq(bound_state, state) {
4301            return true;
4302        }
4303        if bound_state.has_live_applier_host() {
4304            return false;
4305        }
4306
4307        let previous_state = Rc::clone(bound_state);
4308        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4309        lifecycle.flush_pending_drops();
4310        let host_key = self.storage_key();
4311        if previous_state
4312            .dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
4313            .is_err()
4314        {
4315            inner.lifecycle = lifecycle;
4316            return false;
4317        }
4318        previous_state.clear_host(self);
4319        lifecycle.flush_pending_drops();
4320        inner.runtime_state = Some(Rc::clone(state));
4321        inner.lifecycle = lifecycle;
4322        true
4323    }
4324
4325    pub(crate) fn runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
4326        self.inner.borrow().runtime_state.clone()
4327    }
4328
4329    pub(crate) fn borrow(&self) -> Ref<'_, SlotTable> {
4330        Ref::map(self.inner.borrow(), |inner| &inner.table)
4331    }
4332
4333    pub(crate) fn borrow_mut(&self) -> RefMut<'_, SlotTable> {
4334        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.table)
4335    }
4336
4337    pub fn into_table(self: Rc<Self>) -> Result<SlotTable, NodeError> {
4338        if Rc::strong_count(&self) != 1 {
4339            return Err(NodeError::SlotHostUnavailable {
4340                operation: "SlotsHost::into_table",
4341                reason: "other host references are alive",
4342            });
4343        }
4344        self.take_table_for_transfer()
4345    }
4346
4347    fn take_table_for_transfer(&self) -> Result<SlotTable, NodeError> {
4348        let inner = self.inner.borrow();
4349        if inner.active_pass.is_some() {
4350            return Err(NodeError::SlotHostUnavailable {
4351                operation: "SlotsHost::into_table",
4352                reason: "slot pass is active",
4353            });
4354        }
4355        drop(inner);
4356        let mut inner = self.inner.borrow_mut();
4357        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4358        lifecycle.flush_pending_drops();
4359        if let Some(state) = inner.runtime_state.clone() {
4360            let host_key = self.storage_key();
4361            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4362            state.clear_host(self);
4363            lifecycle.flush_pending_drops();
4364        }
4365        let taken = std::mem::take(&mut inner.table);
4366        self.storage_key.set(inner.table.storage_id());
4367        inner.runtime_state = None;
4368        inner.lifecycle = lifecycle;
4369        Ok(taken)
4370    }
4371
4372    pub fn reset(&self) -> Result<(), NodeError> {
4373        let inner = self.inner.borrow();
4374        if inner.active_pass.is_some() {
4375            return Err(NodeError::SlotHostUnavailable {
4376                operation: "SlotsHost::reset",
4377                reason: "slot pass is active",
4378            });
4379        }
4380        let runtime_state = inner.runtime_state.clone();
4381        drop(inner);
4382        let mut inner = self.inner.borrow_mut();
4383        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4384        if let Some(state) = runtime_state {
4385            let host_key = self.storage_key();
4386            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4387            state.clear_host(self);
4388        }
4389        lifecycle.dispose_slot_table(&mut inner.table);
4390        inner.table = SlotTable::default();
4391        self.storage_key.set(inner.table.storage_id());
4392        inner.runtime_state = None;
4393        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4394        Ok(())
4395    }
4396
4397    pub(crate) fn abandon_after_apply_failure(&self) {
4398        let inner = self.inner.borrow();
4399        if inner.active_pass.is_some() {
4400            log::error!("cannot abandon SlotsHost during an active pass");
4401            return;
4402        }
4403        let runtime_state = inner.runtime_state.clone();
4404        drop(inner);
4405        let mut inner = self.inner.borrow_mut();
4406        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4407        if let Some(state) = runtime_state {
4408            let host_key = self.storage_key();
4409            state.abandon_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle);
4410        }
4411        lifecycle.dispose_slot_table(&mut inner.table);
4412        inner.table = SlotTable::default();
4413        self.storage_key.set(inner.table.storage_id());
4414        inner.runtime_state = None;
4415        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4416    }
4417
4418    pub(crate) fn debug_stats(&self) -> SlotTableDebugStats {
4419        let inner = self.inner.borrow();
4420        let local = inner.table.debug_stats();
4421        let lifecycle = inner.lifecycle.debug_stats();
4422        let retention = inner
4423            .runtime_state
4424            .clone()
4425            .map(|state| state.slot_retention_debug_stats(self))
4426            .unwrap_or_default();
4427        SlotTableDebugStats::from_parts(local, lifecycle, retention)
4428    }
4429
4430    pub(crate) fn debug_snapshot(&self) -> slot::SlotDebugSnapshot {
4431        let inner = self.inner.borrow();
4432        let mut snapshot = inner.table.debug_snapshot();
4433        if let Some(state) = inner.runtime_state.clone() {
4434            state.fill_slot_debug_snapshot(self, &mut snapshot);
4435        }
4436        snapshot
4437    }
4438
4439    pub(crate) fn begin_pass(&self, mode: slot::SlotPassMode) {
4440        let mut inner = self.inner.borrow_mut();
4441        if inner.active_pass.is_some() {
4442            log::error!("slot pass already active for host");
4443            return;
4444        }
4445        let mut state = slot::SlotWriteSessionState::default();
4446        state.reset_for_pass(mode);
4447        inner.active_pass = Some(ActivePassState { state });
4448    }
4449
4450    pub(crate) fn has_active_pass(&self) -> bool {
4451        self.inner.borrow().active_pass.is_some()
4452    }
4453
4454    pub(crate) fn try_push_branch_fold(&self, key: Key) -> Option<usize> {
4455        let mut inner = self.inner.try_borrow_mut().ok()?;
4456        let pass = inner.active_pass.as_mut()?;
4457        Some(pass.state.push_branch_fold(key))
4458    }
4459
4460    pub(crate) fn try_close_branch_fold(&self, token: usize) -> bool {
4461        let Ok(mut inner) = self.inner.try_borrow_mut() else {
4462            return false;
4463        };
4464        let Some(pass) = inner.active_pass.as_mut() else {
4465            return false;
4466        };
4467        pass.state.close_branch_fold(token);
4468        true
4469    }
4470
4471    pub(crate) fn abandon_active_pass(&self) {
4472        self.inner.borrow_mut().active_pass = None;
4473    }
4474
4475    pub(crate) fn with_write_session<R>(
4476        &self,
4477        f: impl FnOnce(&mut slot::SlotWriteSession<'_>) -> R,
4478    ) -> R {
4479        let mut inner = self.inner.borrow_mut();
4480        let SlotsHostInner {
4481            table,
4482            lifecycle,
4483            active_pass,
4484            ..
4485        } = &mut *inner;
4486        let active_pass = active_pass
4487            .as_mut()
4488            .expect("slot write session requires an active pass");
4489        let mut session = table.write_session(lifecycle, &mut active_pass.state);
4490        f(&mut session)
4491    }
4492
4493    pub(crate) fn with_table_and_lifecycle_mut<R>(
4494        &self,
4495        f: impl FnOnce(&mut SlotTable, &mut slot::SlotLifecycleCoordinator) -> R,
4496    ) -> R {
4497        let mut inner = self.inner.borrow_mut();
4498        let SlotsHostInner {
4499            table, lifecycle, ..
4500        } = &mut *inner;
4501        f(table, lifecycle)
4502    }
4503
4504    pub(crate) fn finish_pass(
4505        &self,
4506        applier: &mut dyn Applier,
4507    ) -> Result<FinishedSlotPass, NodeError> {
4508        let mut inner = self.inner.borrow_mut();
4509        let SlotsHostInner {
4510            table,
4511            lifecycle,
4512            active_pass: active_pass_slot,
4513            ..
4514        } = &mut *inner;
4515        let Some(mut active_pass) = active_pass_slot.take() else {
4516            return Ok(FinishedSlotPass::default());
4517        };
4518
4519        active_pass.state.flush_payload_location_refreshes(table);
4520
4521        #[cfg(debug_assertions)]
4522        if let Err(err) = active_pass.state.validate(table) {
4523            log::error!("slot writer invariant violation before finalize_pass: {err:?}");
4524            return Err(NodeError::SlotHostUnavailable {
4525                operation: "SlotsHost::finish_pass",
4526                reason: "slot writer invariant violation",
4527            });
4528        }
4529
4530        let detached_root_children = {
4531            let mut session = table.write_session(lifecycle, &mut active_pass.state);
4532            session.finalize_pass(applier)?
4533        };
4534
4535        Ok(FinishedSlotPass {
4536            outcome: SlotPassOutcome {
4537                compacted: active_pass.state.request_compaction,
4538                compact_anchor_registry_storage: active_pass
4539                    .state
4540                    .request_anchor_storage_compaction,
4541                compact_payload_storage: active_pass.state.request_payload_storage_compaction,
4542            },
4543            detached_root_children,
4544        })
4545    }
4546
4547    pub(crate) fn flush_pending_drops(&self) {
4548        self.inner.borrow_mut().lifecycle.flush_pending_drops();
4549    }
4550
4551    pub(crate) fn complete_pass_cleanup(&self, outcome: &SlotPassOutcome) {
4552        let mut inner = self.inner.borrow_mut();
4553        let SlotsHostInner {
4554            table,
4555            lifecycle,
4556            runtime_state,
4557            ..
4558        } = &mut *inner;
4559        lifecycle.flush_pending_drops();
4560        if outcome.compacted {
4561            table.compact_storage();
4562            lifecycle.compact_storage();
4563        }
4564        if let Some(state) = runtime_state.clone() {
4565            state.compact_table_identity_storage_for_host(
4566                self,
4567                table,
4568                outcome.compact_anchor_registry_storage,
4569                outcome.compact_payload_storage,
4570            );
4571        } else {
4572            if outcome.compact_anchor_registry_storage {
4573                table.compact_anchor_registry_storage(None);
4574            }
4575            if outcome.compact_payload_storage {
4576                table.compact_payload_anchor_registry_storage(None);
4577            }
4578        }
4579        table.assert_fast_integrity("slot pass cleanup");
4580        #[cfg(any(test, debug_assertions))]
4581        {
4582            table.debug_verify();
4583            if let Some(state) = runtime_state.clone() {
4584                state.debug_verify_host(self, table);
4585            }
4586        }
4587    }
4588}
4589
4590fn build_child_positions(children: &[NodeId]) -> HashMap<NodeId, usize> {
4591    let mut positions = HashMap::default();
4592    positions.reserve(children.len());
4593    for (index, &child) in children.iter().enumerate() {
4594        positions.insert(child, index);
4595    }
4596    positions
4597}
4598
4599fn refresh_child_positions(
4600    current: &[NodeId],
4601    positions: &mut HashMap<NodeId, usize>,
4602    start: usize,
4603    end: usize,
4604) {
4605    if current.is_empty() || start >= current.len() {
4606        return;
4607    }
4608    let end = end.min(current.len() - 1);
4609    for (offset, &child) in current[start..=end].iter().enumerate() {
4610        positions.insert(child, start + offset);
4611    }
4612}
4613
4614fn insert_child_into_diff_state(
4615    current: &mut ChildList,
4616    positions: &mut HashMap<NodeId, usize>,
4617    index: usize,
4618    child: NodeId,
4619) {
4620    let index = index.min(current.len());
4621    current.insert(index, child);
4622    refresh_child_positions(current, positions, index, current.len() - 1);
4623}
4624
4625fn move_child_in_diff_state(
4626    current: &mut ChildList,
4627    positions: &mut HashMap<NodeId, usize>,
4628    from_index: usize,
4629    target_index: usize,
4630) -> usize {
4631    let child = current.remove(from_index);
4632    let to_index = target_index.min(current.len());
4633    current.insert(to_index, child);
4634    refresh_child_positions(
4635        current,
4636        positions,
4637        from_index.min(to_index),
4638        from_index.max(to_index),
4639    );
4640    to_index
4641}
4642
4643pub(crate) use state::MutableStateInner;
4644pub use state::{
4645    MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
4646    StateSubscriptionHold,
4647};
4648
4649fn hash_key<K: Hash>(key: &K) -> Key {
4650    let mut hasher = hash::default::new();
4651    key.hash(&mut hasher);
4652    hasher.finish()
4653}
4654
4655pub(crate) fn explicit_group_key_seed<K: Hash>(
4656    key: &K,
4657    caller: &'static std::panic::Location<'static>,
4658) -> slot::GroupKeySeed {
4659    let source_key = location_key(caller.file(), caller.line(), caller.column());
4660    let explicit_key = hash_key(key);
4661    slot::GroupKeySeed::keyed(source_key, explicit_key)
4662}
4663
4664#[cfg(test)]
4665#[path = "tests/mod.rs"]
4666mod tests;
4667
4668#[cfg(test)]
4669#[path = "tests/recursive_decrease_increase_test.rs"]
4670mod recursive_decrease_increase_test;
4671
4672pub mod collections;
4673pub mod hash;
4674
4675/// Where a test writes real files. Behind `test-helpers` so only a test build
4676/// of the workspace carries it.
4677#[cfg(any(test, feature = "test-helpers"))]
4678pub mod test_scratch;
4679#[cfg(any(test, feature = "test-helpers"))]
4680pub use test_scratch::test_scratch_dir;
4681
4682pub(crate) fn note_structural(reason: &str, parent_id: NodeId, child_id: NodeId) {
4683    if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
4684        eprintln!("[structural] {reason} parent={parent_id} child={child_id}");
4685    }
4686}
4687
4688pub(crate) fn note_structural_move(parent_id: NodeId, from_index: usize, to_index: usize) {
4689    if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
4690        eprintln!("[structural] move parent={parent_id} from={from_index} to={to_index}");
4691    }
4692}