Skip to main content

cranpose_core/
lib.rs

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