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