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, LaunchedEffect,
69    LaunchedEffectAsync, LaunchedEffectScope, 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
1051fn key_scoped<K: Hash, R>(
1052    key: &K,
1053    caller: &'static std::panic::Location<'static>,
1054    content: impl FnOnce() -> R,
1055) -> R {
1056    let seed = explicit_group_key_seed(key, caller);
1057    with_current_composer(|composer| composer.with_group_seed(seed, |_| content()))
1058}
1059
1060#[track_caller]
1061pub fn with_key<K: Hash>(key: &K, content: impl FnOnce()) {
1062    key_scoped(key, std::panic::Location::caller(), content);
1063}
1064
1065/// Scopes composition identity to `keys` for the duration of `content`.
1066///
1067/// Mirrors Jetpack Compose's `key(vararg keys) { content }`. A call keeps its
1068/// identity by call-site position alone by default, so a slot survives a
1069/// recomposition even when what a caller passes in changes; wrapping the
1070/// call in `key(keys, || ...)` folds `keys` into that identity instead, so
1071/// changing `keys` discards the previous state and starts fresh, and two
1072/// `key` calls at the same call site with different `keys` never share
1073/// state. Pass a tuple to key on more than one value, e.g.
1074/// `key((a, b), || ...)`.
1075#[track_caller]
1076pub fn key<K: Hash, R>(keys: K, content: impl FnOnce() -> R) -> R {
1077    key_scoped(&keys, std::panic::Location::caller(), content)
1078}
1079
1080#[derive(Default)]
1081struct DisposableEffectState {
1082    key: Option<effect_key::EffectKey>,
1083    cleanup: Option<Box<dyn FnOnce()>>,
1084}
1085
1086impl DisposableEffectState {
1087    fn should_run(&self, key: &effect_key::EffectKey) -> bool {
1088        match &self.key {
1089            Some(current) => key.differs_from(current),
1090            None => true,
1091        }
1092    }
1093
1094    fn set_key(&mut self, key: effect_key::EffectKey) {
1095        self.key = Some(key);
1096    }
1097
1098    fn set_cleanup(&mut self, cleanup: Option<Box<dyn FnOnce()>>) {
1099        self.cleanup = cleanup;
1100    }
1101
1102    fn run_cleanup(&mut self) {
1103        if let Some(cleanup) = self.cleanup.take() {
1104            cleanup();
1105        }
1106    }
1107}
1108
1109impl Drop for DisposableEffectState {
1110    fn drop(&mut self) {
1111        self.run_cleanup();
1112    }
1113}
1114
1115#[derive(Clone, Copy, Debug, Default)]
1116pub struct DisposableEffectScope;
1117
1118#[derive(Default)]
1119pub struct DisposableEffectResult {
1120    cleanup: Option<Box<dyn FnOnce()>>,
1121}
1122
1123impl DisposableEffectScope {
1124    pub fn on_dispose(&self, cleanup: impl FnOnce() + 'static) -> DisposableEffectResult {
1125        DisposableEffectResult::new(cleanup)
1126    }
1127}
1128
1129impl DisposableEffectResult {
1130    pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
1131        Self {
1132            cleanup: Some(Box::new(cleanup)),
1133        }
1134    }
1135
1136    fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
1137        self.cleanup
1138    }
1139}
1140
1141#[allow(non_snake_case)]
1142pub fn SideEffect(effect: impl FnOnce() + 'static) {
1143    with_current_composer(|composer| composer.register_side_effect(effect));
1144}
1145
1146pub fn __disposable_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
1147where
1148    K: PartialEq + 'static,
1149    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1150{
1151    with_current_composer(|composer| {
1152        composer.with_group(group_key, |composer| {
1153            let key = effect_key::EffectKey::new(keys);
1154            let state = composer.remember_effect::<DisposableEffectState>();
1155            if state.with(|state| state.should_run(&key)) {
1156                state.update(|state| {
1157                    state.run_cleanup();
1158                    state.set_key(key);
1159                });
1160                let state_for_effect = state.clone();
1161                let mut effect_opt = Some(effect);
1162                composer.register_side_effect(move || {
1163                    if let Some(effect) = effect_opt.take() {
1164                        let result = effect(DisposableEffectScope);
1165                        state_for_effect.update(|state| state.set_cleanup(result.into_cleanup()));
1166                    }
1167                });
1168            }
1169        });
1170    });
1171}
1172
1173/// Runs `effect` when this call site first enters composition, and again
1174/// whenever `keys` no longer equals the value it ran with last time; the
1175/// previous run's [`DisposableEffectScope::on_dispose`] cleanup, if any,
1176/// runs first, and also when the call site leaves composition entirely.
1177///
1178/// `keys` may be a tuple to depend on more than one value, matching Jetpack
1179/// Compose's `DisposableEffect(vararg keys)`.
1180#[allow(non_snake_case)]
1181#[track_caller]
1182pub fn DisposableEffect<K, F>(keys: K, effect: F)
1183where
1184    K: PartialEq + 'static,
1185    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1186{
1187    __disposable_effect_impl(crate::caller_location_key(), keys, effect);
1188}
1189
1190#[macro_export]
1191macro_rules! clone_captures {
1192    ($($alias:ident $(= $value:expr)?),+ $(,)?; $body:expr) => {{
1193        $(let $alias = $crate::clone_captures!(@clone $alias $(= $value)?);)+
1194        $body
1195    }};
1196    (@clone $alias:ident = $value:expr) => {
1197        ($value).clone()
1198    };
1199    (@clone $alias:ident) => {
1200        $alias.clone()
1201    };
1202}
1203
1204pub fn with_node_mut<N: Node + 'static, R>(
1205    id: NodeId,
1206    f: impl FnOnce(&mut N) -> R,
1207) -> Result<R, NodeError> {
1208    with_current_composer(|composer| composer.with_node_mut(id, f))
1209}
1210
1211pub fn push_parent(id: NodeId) {
1212    with_current_composer(|composer| composer.push_parent(id));
1213}
1214
1215pub fn pop_parent() {
1216    with_current_composer(|composer| composer.pop_parent());
1217}
1218
1219pub trait Node: Any {
1220    fn mount(&mut self) {}
1221    fn update(&mut self) {}
1222    fn unmount(&mut self) {}
1223    /// Adds `child` to this node's child list, returning whether the list
1224    /// actually changed. A node that already holds the child returns `false`:
1225    /// callers record a structural change from this answer, and a structural
1226    /// change re-lowers the whole subtree, so claiming one for a no-op costs a
1227    /// full rebuild per call.
1228    fn insert_child(&mut self, _child: NodeId) -> bool {
1229        false
1230    }
1231    /// Removes `child` from this node's child list, returning whether the list
1232    /// actually changed. Only this node can answer that: a caller inspecting
1233    /// the child's parent pointer gets it wrong when the child was reparented
1234    /// while still listed here.
1235    fn remove_child(&mut self, _child: NodeId) -> bool {
1236        false
1237    }
1238    fn move_child(&mut self, _from: usize, _to: usize) {}
1239    fn update_children(&mut self, _children: &[NodeId]) {}
1240    fn children(&self) -> Vec<NodeId> {
1241        Vec::new()
1242    }
1243    /// Copies child IDs into the provided scratch buffer without allocating a
1244    /// fresh container for every traversal.
1245    fn collect_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1246        out.clear();
1247        out.extend(self.children());
1248    }
1249    fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1250        self.collect_children_into(out);
1251    }
1252    /// Called after the node is created to record its own ID.
1253    /// Useful for nodes that need to store their ID for later operations.
1254    fn set_node_id(&mut self, _id: NodeId) {}
1255    /// Called when this node is attached to a parent.
1256    /// Nodes with parent tracking should set their parent reference here.
1257    fn on_attached_to_parent(&mut self, _parent: NodeId) {}
1258    /// Called when this node is removed from its parent.
1259    /// Nodes with parent tracking should clear their parent reference here.
1260    fn on_removed_from_parent(&mut self) {}
1261    /// Get this node's parent ID (for nodes that track parents).
1262    /// Returns None if node has no parent or doesn't track parents.
1263    fn parent(&self) -> Option<NodeId> {
1264        None
1265    }
1266    /// Mark this node as needing layout (for nodes with dirty flags).
1267    /// Called during bubbling to propagate dirtiness up the tree.
1268    fn mark_needs_layout(&self) {}
1269    /// Check if this node needs layout (for nodes with dirty flags).
1270    fn needs_layout(&self) -> bool {
1271        false
1272    }
1273    /// Mark this node as needing measure (size may have changed).
1274    /// Called during bubbling when children are added/removed.
1275    fn mark_needs_measure(&self) {}
1276    /// Check if this node needs measure (for nodes with dirty flags).
1277    fn needs_measure(&self) -> bool {
1278        false
1279    }
1280    /// Mark this node as needing semantics recomputation.
1281    fn mark_needs_semantics(&self) {}
1282    /// Check if this node needs semantics recomputation.
1283    fn needs_semantics(&self) -> bool {
1284        false
1285    }
1286    /// Set parent reference for dirty flag bubbling ONLY.
1287    /// This is a minimal version of on_attached_to_parent that doesn't trigger
1288    /// registry updates or other side effects. Used during measurement when we
1289    /// need to establish parent connections for bubble_measure_dirty without
1290    /// causing the full attachment lifecycle.
1291    ///
1292    /// Default implementation uses the normal parent-attachment hook.
1293    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1294        self.on_attached_to_parent(parent);
1295    }
1296
1297    /// Returns a recycle pool key when this node supports shell reuse.
1298    fn recycle_key(&self) -> Option<TypeId> {
1299        None
1300    }
1301
1302    /// Bounds how many recyclable shells of this node type should be retained.
1303    fn recycle_pool_limit(&self) -> Option<usize> {
1304        None
1305    }
1306
1307    /// Clears live attachments before the node shell enters a recycle pool.
1308    fn prepare_for_recycle(&mut self) {}
1309
1310    /// Optionally provides a compact replacement box for this recycled shell.
1311    ///
1312    /// Returning `Some` lets nodes move pooled survivors onto fresh compact
1313    /// storage so the recycle pool does not pin large spike-era allocations.
1314    fn rehouse_for_recycle(&self) -> Option<Box<dyn Node>> {
1315        None
1316    }
1317
1318    /// Optionally moves a live node onto a fresh box during applier compaction.
1319    ///
1320    /// This is used after large-majority teardowns where a small surviving live
1321    /// tree can otherwise pin allocator pages from a much larger spike-era node
1322    /// population. Implementations must preserve the node's live state.
1323    fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn Node>> {
1324        None
1325    }
1326
1327    /// Returns the node-owned heap retained beyond the node's own box allocation.
1328    fn debug_heap_bytes(&self) -> usize {
1329        0
1330    }
1331}
1332
1333/// Unified API for bubbling layout dirty flags from a node to the root (Applier context).
1334///
1335/// This is the canonical function for dirty bubbling during the apply phase (structural changes).
1336/// Call this after mutations like insert/remove/move that happen during apply.
1337///
1338/// # Behavior
1339/// 1. Marks the starting node as needing layout
1340/// 2. Walks up the parent chain, marking each ancestor
1341/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1342/// 4. Stops at the root (node with no parent)
1343///
1344/// # Performance
1345/// This function is O(height) in the worst case, but typically O(1) due to early exit
1346/// when encountering an already-dirty ancestor.
1347///
1348/// # Usage
1349/// - Call from composer mutations (insert/remove/move) during apply phase
1350/// - Call from applier-level operations that modify the tree structure
1351pub fn bubble_layout_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1352    bubble_layout_dirty_applier(applier, node_id);
1353}
1354
1355/// Unified API for bubbling measure dirty flags from a node to the root (Applier context).
1356///
1357/// Call this when a node's size may have changed (children added/removed, modifier changed).
1358/// This ensures that measure_layout will increment the cache epoch and re-measure the subtree.
1359///
1360/// # Behavior
1361/// 1. Marks the starting node as needing measure
1362/// 2. Walks up the parent chain, marking each ancestor
1363/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1364/// 4. Stops at the root (node with no parent)
1365pub fn bubble_measure_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1366    bubble_measure_dirty_applier(applier, node_id);
1367}
1368
1369/// Unified API for bubbling semantics dirty flags from a node to the root (Applier context).
1370///
1371/// This mirrors [`bubble_layout_dirty`] but toggles semantics-specific dirty
1372/// flags instead of layout ones, allowing semantics updates to propagate during
1373/// the apply phase without forcing layout work.
1374pub fn bubble_semantics_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1375    bubble_semantics_dirty_applier(applier, node_id);
1376}
1377
1378/// Schedules semantics bubbling for a node using the active composer if present.
1379///
1380/// This defers the work to the apply phase where we can safely mutate the
1381/// applier tree without re-entrantly borrowing the composer during composition.
1382pub fn queue_semantics_invalidation(node_id: NodeId) {
1383    let _ = composer_context::try_with_composer(|composer| {
1384        composer.enqueue_semantics_invalidation(node_id);
1385    });
1386}
1387
1388/// Unified API for bubbling layout dirty flags from a node to the root (Composer context).
1389///
1390/// This is the canonical function for dirty bubbling during composition (property changes).
1391/// Call this after property changes that happen during composition via with_node_mut.
1392///
1393/// # Behavior
1394/// 1. Marks the starting node as needing layout
1395/// 2. Walks up the parent chain, marking each ancestor
1396/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1397/// 4. Stops at the root (node with no parent)
1398///
1399/// # Performance
1400/// This function is O(height) in the worst case, but typically O(1) due to early exit
1401/// when encountering an already-dirty ancestor.
1402///
1403/// # Type Requirements
1404/// The node type N must implement Node (which includes mark_needs_layout, parent, etc.).
1405/// Typically this will be LayoutNode or similar layout-aware node types.
1406///
1407/// # Usage
1408/// - Call from property setters during composition (e.g., set_modifier, set_measure_policy)
1409/// - Call from widget composition when layout-affecting state changes
1410pub fn bubble_layout_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1411    bubble_layout_dirty_composer::<N>(node_id);
1412}
1413
1414/// Unified API for bubbling measure dirty flags from a node to the root during composition.
1415///
1416/// This queues a dirty-bubble command on the active composer so measure invalidation
1417/// runs during the apply phase, avoiding re-entrant applier borrows while widgets are
1418/// mutating nodes via `with_node_mut`.
1419pub fn bubble_measure_dirty_in_composer(node_id: NodeId) {
1420    with_current_composer(|composer| {
1421        composer.commands_mut().push(Command::BubbleDirty {
1422            node_id,
1423            bubble: DirtyBubble {
1424                layout: false,
1425                measure: true,
1426                semantics: false,
1427            },
1428        });
1429    });
1430}
1431
1432/// Unified API for bubbling semantics dirty flags from a node to the root (Composer context).
1433///
1434/// This mirrors [`bubble_layout_dirty_in_composer`] but routes through the semantics
1435/// dirty flag instead of the layout one. Modifier nodes can request semantics
1436/// invalidations without triggering measure/layout work, and the runtime can
1437/// query the root to determine whether the semantics tree needs rebuilding.
1438pub fn bubble_semantics_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1439    bubble_semantics_dirty_composer::<N>(node_id);
1440}
1441
1442fn bubble_layout_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1443    if let Ok(node) = applier.get_mut(node_id) {
1444        node.mark_needs_layout();
1445    }
1446
1447    loop {
1448        let parent_id = match applier.get_mut(node_id) {
1449            Ok(node) => node.parent(),
1450            Err(_) => None,
1451        };
1452
1453        match parent_id {
1454            Some(pid) => {
1455                if let Ok(parent) = applier.get_mut(pid) {
1456                    let parent_already_dirty = parent.needs_layout();
1457                    if !parent_already_dirty {
1458                        parent.mark_needs_layout();
1459                    }
1460                    node_id = pid;
1461                } else {
1462                    break;
1463                }
1464            }
1465            None => break,
1466        }
1467    }
1468}
1469
1470fn bubble_measure_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1471    if let Ok(node) = applier.get_mut(node_id) {
1472        node.mark_needs_measure();
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_measure() {
1485                        parent.mark_needs_measure();
1486                    }
1487                    node_id = pid;
1488                } else {
1489                    break;
1490                }
1491            }
1492            None => {
1493                break;
1494            }
1495        }
1496    }
1497}
1498
1499fn bubble_semantics_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1500    if let Ok(node) = applier.get_mut(node_id) {
1501        node.mark_needs_semantics();
1502    }
1503
1504    loop {
1505        let parent_id = match applier.get_mut(node_id) {
1506            Ok(node) => node.parent(),
1507            Err(_) => None,
1508        };
1509
1510        match parent_id {
1511            Some(pid) => {
1512                if let Ok(parent) = applier.get_mut(pid) {
1513                    if !parent.needs_semantics() {
1514                        parent.mark_needs_semantics();
1515                    }
1516                    node_id = pid;
1517                } else {
1518                    break;
1519                }
1520            }
1521            None => break,
1522        }
1523    }
1524}
1525
1526fn bubble_layout_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1527    let _ = with_node_mut(node_id, |node: &mut N| {
1528        node.mark_needs_layout();
1529    });
1530
1531    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1532        let parent_id = pid;
1533
1534        let advanced = with_node_mut(parent_id, |node: &mut N| {
1535            if !node.needs_layout() {
1536                node.mark_needs_layout();
1537            }
1538            true
1539        })
1540        .unwrap_or(false);
1541
1542        if advanced {
1543            node_id = parent_id;
1544        } else {
1545            break;
1546        }
1547    }
1548}
1549
1550fn bubble_semantics_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1551    let _ = with_node_mut(node_id, |node: &mut N| {
1552        node.mark_needs_semantics();
1553    });
1554
1555    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1556        let parent_id = pid;
1557
1558        let advanced = with_node_mut(parent_id, |node: &mut N| {
1559            if !node.needs_semantics() {
1560                node.mark_needs_semantics();
1561            }
1562            true
1563        })
1564        .unwrap_or(false);
1565
1566        if advanced {
1567            node_id = parent_id;
1568        } else {
1569            break;
1570        }
1571    }
1572}
1573
1574impl dyn Node {
1575    pub fn as_any_mut(&mut self) -> &mut dyn Any {
1576        self
1577    }
1578}
1579
1580pub struct RecycledNode {
1581    stable_id: NodeId,
1582    node: Box<dyn Node>,
1583    warm_origin: bool,
1584}
1585
1586impl RecycledNode {
1587    fn new(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1588        let node = node.rehouse_for_recycle().unwrap_or(node);
1589        Self {
1590            stable_id,
1591            node,
1592            warm_origin,
1593        }
1594    }
1595
1596    fn from_shell(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1597        Self {
1598            stable_id,
1599            node,
1600            warm_origin,
1601        }
1602    }
1603
1604    pub fn stable_id(&self) -> NodeId {
1605        self.stable_id
1606    }
1607
1608    fn warm_origin(&self) -> bool {
1609        self.warm_origin
1610    }
1611
1612    fn set_warm_origin(&mut self, warm_origin: bool) {
1613        self.warm_origin = warm_origin;
1614    }
1615
1616    pub fn node_mut(&mut self) -> &mut dyn Node {
1617        self.node.as_mut()
1618    }
1619
1620    pub fn into_parts(self) -> (NodeId, Box<dyn Node>, bool) {
1621        (self.stable_id, self.node, self.warm_origin)
1622    }
1623}
1624
1625#[derive(Debug, Clone, PartialEq, Eq)]
1626pub struct RecycledNodeInsertion {
1627    pub id: NodeId,
1628    pub stable_id_reused: bool,
1629    pub fallback_error: Option<NodeError>,
1630}
1631
1632impl RecycledNodeInsertion {
1633    fn reused(stable_id: NodeId) -> Self {
1634        Self {
1635            id: stable_id,
1636            stable_id_reused: true,
1637            fallback_error: None,
1638        }
1639    }
1640
1641    fn fresh(id: NodeId, fallback_error: Option<NodeError>) -> Self {
1642        Self {
1643            id,
1644            stable_id_reused: false,
1645            fallback_error,
1646        }
1647    }
1648}
1649
1650pub trait Applier: Any {
1651    fn create(&mut self, node: Box<dyn Node>) -> NodeId;
1652    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError>;
1653    fn remove(&mut self, id: NodeId) -> Result<(), NodeError>;
1654
1655    /// Records that `parent_id`'s child list changed structurally this frame
1656    /// (insert, remove, move, or reparent). Incremental scene consumers drain
1657    /// the recorded parents and re-patch those subtrees so removed nodes are
1658    /// evicted from a persistent render graph even when the frame's scene
1659    /// update is otherwise scoped to unrelated dirty nodes.
1660    fn record_structural_change(&mut self, _parent_id: NodeId) {}
1661
1662    /// Returns the current generation for a node index.
1663    /// Generation is incremented when an index is reused from the freelist,
1664    /// preventing stale slot entries from matching recycled nodes.
1665    fn node_generation(&self, id: NodeId) -> u32;
1666
1667    /// Inserts a node with a pre-assigned ID.
1668    ///
1669    /// This is used for virtual nodes whose IDs are allocated separately
1670    /// (e.g., via allocate_virtual_node_id()). Unlike `create()` which assigns
1671    /// a new ID, this method uses the provided ID.
1672    ///
1673    /// Returns Ok(()) if successful, or an error if the ID is already in use.
1674    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError>;
1675
1676    /// Reinserts a recycled node at its retained stable ID, or creates a fresh ID if that
1677    /// retained ID is no longer available.
1678    fn insert_recycled_node_or_create(
1679        &mut self,
1680        stable_id: NodeId,
1681        node: Box<dyn Node>,
1682    ) -> RecycledNodeInsertion {
1683        let id = self.create(node);
1684        RecycledNodeInsertion::fresh(id, Some(NodeError::AlreadyExists { id: stable_id }))
1685    }
1686
1687    fn as_any(&self) -> &dyn Any
1688    where
1689        Self: Sized,
1690    {
1691        self
1692    }
1693
1694    fn as_any_mut(&mut self) -> &mut dyn Any
1695    where
1696        Self: Sized,
1697    {
1698        self
1699    }
1700
1701    /// Trim trailing tombstones/unused capacity after structural changes.
1702    fn compact(&mut self) {}
1703
1704    /// Returns a previously recycled node shell and its stable ID for the requested concrete type.
1705    fn take_recycled_node(&mut self, _key: TypeId) -> Option<RecycledNode> {
1706        None
1707    }
1708
1709    /// Marks whether a reinserted recycled node originated from the warm recycle path.
1710    fn set_recycled_node_origin(&mut self, _id: NodeId, _warm_origin: bool) {}
1711
1712    /// Seeds a warm recyclable shell for future reuse without requiring a prior removal.
1713    fn seed_recycled_node_shell(
1714        &mut self,
1715        _key: TypeId,
1716        _recycle_pool_limit: Option<usize>,
1717        _shell: Box<dyn Node>,
1718    ) {
1719    }
1720
1721    /// Records that the current apply pass had to allocate a fresh recyclable shell for this type.
1722    fn record_fresh_recyclable_creation(&mut self, _key: TypeId) {}
1723
1724    /// Drops any recyclable shells that should not survive beyond the current apply pass.
1725    fn clear_recycled_nodes(&mut self) {}
1726}
1727
1728type TypedNodeUpdate = fn(&mut dyn Node, NodeId) -> Result<(), NodeError>;
1729type CommandCallback = Box<dyn FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static>;
1730
1731#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1732pub(crate) struct DirtyBubble {
1733    layout: bool,
1734    measure: bool,
1735    semantics: bool,
1736}
1737
1738impl DirtyBubble {
1739    pub(crate) const LAYOUT_AND_MEASURE: Self = Self {
1740        layout: true,
1741        measure: true,
1742        semantics: false,
1743    };
1744
1745    pub(crate) const SEMANTICS: Self = Self {
1746        layout: false,
1747        measure: false,
1748        semantics: true,
1749    };
1750
1751    fn apply(self, applier: &mut dyn Applier, node_id: NodeId) {
1752        if self.layout {
1753            bubble_layout_dirty(applier, node_id);
1754        }
1755        if self.measure {
1756            bubble_measure_dirty(applier, node_id);
1757        }
1758        if self.semantics {
1759            bubble_semantics_dirty(applier, node_id);
1760        }
1761    }
1762}
1763
1764pub(crate) enum Command {
1765    BubbleDirty {
1766        node_id: NodeId,
1767        bubble: DirtyBubble,
1768    },
1769    UpdateTypedNode {
1770        id: NodeId,
1771        updater: TypedNodeUpdate,
1772    },
1773    RemoveNode {
1774        id: NodeId,
1775    },
1776    MountNode {
1777        id: NodeId,
1778    },
1779    AttachChild {
1780        parent_id: NodeId,
1781        child_id: NodeId,
1782        insert_index: Option<usize>,
1783        bubble: DirtyBubble,
1784    },
1785    InsertChild {
1786        parent_id: NodeId,
1787        child_id: NodeId,
1788        appended_index: usize,
1789        insert_index: usize,
1790        bubble: DirtyBubble,
1791    },
1792    MoveChild {
1793        parent_id: NodeId,
1794        from_index: usize,
1795        to_index: usize,
1796        bubble: DirtyBubble,
1797    },
1798    RemoveChild {
1799        parent_id: NodeId,
1800        child_id: NodeId,
1801    },
1802    DetachChild {
1803        parent_id: NodeId,
1804        child_id: NodeId,
1805    },
1806    SyncChildren {
1807        parent_id: NodeId,
1808        expected_children: ChildList,
1809    },
1810    Callback(CommandCallback),
1811}
1812
1813#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1814struct DeferredChildCleanup {
1815    child_id: NodeId,
1816    generation: u32,
1817    removed_from_parent: bool,
1818}
1819
1820#[derive(Default)]
1821struct DeferredChildCleanupQueue {
1822    pending: Vec<DeferredChildCleanup>,
1823    preserved: Vec<(NodeId, u32)>,
1824}
1825
1826impl DeferredChildCleanupQueue {
1827    fn push(&mut self, child_id: NodeId, generation: u32, removed_from_parent: bool) {
1828        if self
1829            .preserved
1830            .iter()
1831            .any(|&(preserved_id, preserved_generation)| {
1832                preserved_id == child_id && preserved_generation == generation
1833            })
1834        {
1835            return;
1836        }
1837        self.pending.push(DeferredChildCleanup {
1838            child_id,
1839            generation,
1840            removed_from_parent,
1841        });
1842    }
1843
1844    fn preserve(&mut self, child_id: NodeId, generation: u32) {
1845        if !self
1846            .preserved
1847            .iter()
1848            .any(|&(preserved_id, preserved_generation)| {
1849                preserved_id == child_id && preserved_generation == generation
1850            })
1851        {
1852            self.preserved.push((child_id, generation));
1853        }
1854        self.pending
1855            .retain(|cleanup| cleanup.child_id != child_id || cleanup.generation != generation);
1856    }
1857
1858    fn flush(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
1859        for cleanup in self.pending {
1860            cleanup_detached_child(applier, cleanup)?;
1861        }
1862        Ok(())
1863    }
1864}
1865
1866impl Command {
1867    pub(crate) fn update_node<N: Node + 'static>(id: NodeId) -> Self {
1868        Self::UpdateTypedNode {
1869            id,
1870            updater: update_typed_node::<N>,
1871        }
1872    }
1873
1874    pub(crate) fn callback(
1875        callback: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
1876    ) -> Self {
1877        Self::Callback(Box::new(callback))
1878    }
1879
1880    pub(crate) fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
1881        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
1882        self.apply_with_cleanup(applier, &mut deferred_cleanup)?;
1883        deferred_cleanup.flush(applier)
1884    }
1885
1886    fn apply_with_cleanup(
1887        self,
1888        applier: &mut dyn Applier,
1889        deferred_cleanup: &mut DeferredChildCleanupQueue,
1890    ) -> Result<(), NodeError> {
1891        match self {
1892            Self::BubbleDirty { node_id, bubble } => {
1893                bubble.apply(applier, node_id);
1894                Ok(())
1895            }
1896            Self::UpdateTypedNode { id, updater } => {
1897                let node = match applier.get_mut(id) {
1898                    Ok(node) => node,
1899                    Err(NodeError::Missing { .. }) => return Ok(()),
1900                    Err(err) => return Err(err),
1901                };
1902                updater(node, id)
1903            }
1904            Self::RemoveNode { id } => {
1905                if let Ok(node) = applier.get_mut(id) {
1906                    node.unmount();
1907                }
1908                match applier.remove(id) {
1909                    Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
1910                    Err(err) => Err(err),
1911                }
1912            }
1913            Self::MountNode { id } => {
1914                let node = match applier.get_mut(id) {
1915                    Ok(node) => node,
1916                    Err(NodeError::Missing { .. }) => return Ok(()),
1917                    Err(err) => return Err(err),
1918                };
1919                node.set_node_id(id);
1920                node.mount();
1921                Ok(())
1922            }
1923            Self::AttachChild {
1924                parent_id,
1925                child_id,
1926                insert_index,
1927                bubble,
1928            } => {
1929                attach_child_at(applier, parent_id, child_id, insert_index, bubble);
1930                Ok(())
1931            }
1932            Self::InsertChild {
1933                parent_id,
1934                child_id,
1935                appended_index,
1936                insert_index,
1937                bubble,
1938            } => {
1939                insert_child_with_reparenting(applier, parent_id, child_id);
1940                bubble.apply(applier, parent_id);
1941                if insert_index != appended_index
1942                    && let Ok(parent_node) = applier.get_mut(parent_id)
1943                {
1944                    parent_node.move_child(appended_index, insert_index);
1945                }
1946                Ok(())
1947            }
1948            Self::MoveChild {
1949                parent_id,
1950                from_index,
1951                to_index,
1952                bubble,
1953            } => {
1954                if let Ok(parent_node) = applier.get_mut(parent_id) {
1955                    parent_node.move_child(from_index, to_index);
1956                }
1957                bubble.apply(applier, parent_id);
1958                note_structural_move(parent_id, from_index, to_index);
1959                applier.record_structural_change(parent_id);
1960                Ok(())
1961            }
1962            Self::RemoveChild {
1963                parent_id,
1964                child_id,
1965            } => apply_remove_child(applier, parent_id, child_id, deferred_cleanup),
1966            Self::DetachChild {
1967                parent_id,
1968                child_id,
1969            } => {
1970                let generation = applier.node_generation(child_id);
1971                detach_child_from_parent(applier, parent_id, child_id)?;
1972                deferred_cleanup.preserve(child_id, generation);
1973                Ok(())
1974            }
1975            Self::SyncChildren {
1976                parent_id,
1977                expected_children,
1978            } => sync_children(applier, parent_id, &expected_children, deferred_cleanup),
1979            Self::Callback(callback) => callback(applier),
1980        }
1981    }
1982}
1983
1984const COMMAND_CHUNK_CAPACITY: usize = 1024;
1985const COMMAND_FLUSH_THRESHOLD: usize = COMMAND_CHUNK_CAPACITY * 4;
1986type ChildList = SmallVec<[NodeId; 4]>;
1987const SMALL_CHILD_SYNC_LINEAR_THRESHOLD: usize = 8;
1988
1989#[derive(Copy, Clone)]
1990enum CommandTag {
1991    BubbleDirty,
1992    UpdateTypedNode,
1993    RemoveNode,
1994    MountNode,
1995    AttachChild,
1996    InsertChild,
1997    MoveChild,
1998    RemoveChild,
1999    DetachChild,
2000    SyncChildren,
2001    Callback,
2002}
2003
2004impl CommandTag {
2005    fn label(self) -> &'static str {
2006        match self {
2007            Self::BubbleDirty => "BubbleDirty",
2008            Self::UpdateTypedNode => "UpdateTypedNode",
2009            Self::RemoveNode => "RemoveNode",
2010            Self::MountNode => "MountNode",
2011            Self::AttachChild => "AttachChild",
2012            Self::InsertChild => "InsertChild",
2013            Self::MoveChild => "MoveChild",
2014            Self::RemoveChild => "RemoveChild",
2015            Self::DetachChild => "DetachChild",
2016            Self::SyncChildren => "SyncChildren",
2017            Self::Callback => "Callback",
2018        }
2019    }
2020}
2021
2022#[derive(Copy, Clone)]
2023struct BubbleDirtyCommand {
2024    node_id: NodeId,
2025    bubble: DirtyBubble,
2026}
2027
2028#[derive(Copy, Clone)]
2029struct UpdateTypedNodeCommand {
2030    id: NodeId,
2031    updater: TypedNodeUpdate,
2032}
2033
2034#[derive(Copy, Clone)]
2035struct AttachChildCommand {
2036    parent_id: NodeId,
2037    child_id: NodeId,
2038    insert_index: Option<usize>,
2039    bubble: DirtyBubble,
2040}
2041
2042#[derive(Copy, Clone)]
2043struct InsertChildCommand {
2044    parent_id: NodeId,
2045    child_id: NodeId,
2046    appended_index: usize,
2047    insert_index: usize,
2048    bubble: DirtyBubble,
2049}
2050
2051#[derive(Copy, Clone)]
2052struct MoveChildCommand {
2053    parent_id: NodeId,
2054    from_index: usize,
2055    to_index: usize,
2056    bubble: DirtyBubble,
2057}
2058
2059#[derive(Copy, Clone)]
2060struct RemoveChildCommand {
2061    parent_id: NodeId,
2062    child_id: NodeId,
2063}
2064
2065#[derive(Copy, Clone)]
2066struct DetachChildCommand {
2067    parent_id: NodeId,
2068    child_id: NodeId,
2069}
2070
2071struct SyncChildrenCommand {
2072    parent_id: NodeId,
2073    child_start: usize,
2074    child_len: usize,
2075}
2076
2077#[derive(Default)]
2078struct CommandQueue {
2079    chunks: Vec<Vec<CommandTag>>,
2080    len: usize,
2081    bubble_dirty: Vec<BubbleDirtyCommand>,
2082    update_typed_nodes: Vec<UpdateTypedNodeCommand>,
2083    remove_nodes: Vec<NodeId>,
2084    mount_nodes: Vec<NodeId>,
2085    attach_children: Vec<AttachChildCommand>,
2086    insert_children: Vec<InsertChildCommand>,
2087    move_children: Vec<MoveChildCommand>,
2088    remove_children: Vec<RemoveChildCommand>,
2089    detach_children: Vec<DetachChildCommand>,
2090    sync_children: Vec<SyncChildrenCommand>,
2091    sync_child_ids: Vec<NodeId>,
2092    callbacks: Vec<CommandCallback>,
2093}
2094
2095impl CommandQueue {
2096    fn push_tag(&mut self, tag: CommandTag) {
2097        let needs_chunk = self
2098            .chunks
2099            .last()
2100            .map(|chunk| chunk.len() == chunk.capacity())
2101            .unwrap_or(true);
2102        if needs_chunk {
2103            self.chunks.push(Vec::with_capacity(COMMAND_CHUNK_CAPACITY));
2104        }
2105        if let Some(chunk) = self.chunks.last_mut() {
2106            chunk.push(tag);
2107            self.len += 1;
2108        }
2109    }
2110
2111    fn push(&mut self, command: Command) {
2112        match command {
2113            Command::BubbleDirty { node_id, bubble } => {
2114                self.bubble_dirty
2115                    .push(BubbleDirtyCommand { node_id, bubble });
2116                self.push_tag(CommandTag::BubbleDirty);
2117            }
2118            Command::UpdateTypedNode { id, updater } => {
2119                self.update_typed_nodes
2120                    .push(UpdateTypedNodeCommand { id, updater });
2121                self.push_tag(CommandTag::UpdateTypedNode);
2122            }
2123            Command::RemoveNode { id } => {
2124                self.remove_nodes.push(id);
2125                self.push_tag(CommandTag::RemoveNode);
2126            }
2127            Command::MountNode { id } => {
2128                self.mount_nodes.push(id);
2129                self.push_tag(CommandTag::MountNode);
2130            }
2131            Command::AttachChild {
2132                parent_id,
2133                child_id,
2134                insert_index,
2135                bubble,
2136            } => {
2137                self.attach_children.push(AttachChildCommand {
2138                    parent_id,
2139                    child_id,
2140                    insert_index,
2141                    bubble,
2142                });
2143                self.push_tag(CommandTag::AttachChild);
2144            }
2145            Command::InsertChild {
2146                parent_id,
2147                child_id,
2148                appended_index,
2149                insert_index,
2150                bubble,
2151            } => {
2152                self.insert_children.push(InsertChildCommand {
2153                    parent_id,
2154                    child_id,
2155                    appended_index,
2156                    insert_index,
2157                    bubble,
2158                });
2159                self.push_tag(CommandTag::InsertChild);
2160            }
2161            Command::MoveChild {
2162                parent_id,
2163                from_index,
2164                to_index,
2165                bubble,
2166            } => {
2167                self.move_children.push(MoveChildCommand {
2168                    parent_id,
2169                    from_index,
2170                    to_index,
2171                    bubble,
2172                });
2173                self.push_tag(CommandTag::MoveChild);
2174            }
2175            Command::RemoveChild {
2176                parent_id,
2177                child_id,
2178            } => {
2179                self.remove_children.push(RemoveChildCommand {
2180                    parent_id,
2181                    child_id,
2182                });
2183                self.push_tag(CommandTag::RemoveChild);
2184            }
2185            Command::DetachChild {
2186                parent_id,
2187                child_id,
2188            } => {
2189                self.detach_children.push(DetachChildCommand {
2190                    parent_id,
2191                    child_id,
2192                });
2193                self.push_tag(CommandTag::DetachChild);
2194            }
2195            Command::SyncChildren {
2196                parent_id,
2197                expected_children,
2198            } => {
2199                let child_start = self.sync_child_ids.len();
2200                let child_len = expected_children.len();
2201                self.sync_child_ids.extend(expected_children);
2202                self.sync_children.push(SyncChildrenCommand {
2203                    parent_id,
2204                    child_start,
2205                    child_len,
2206                });
2207                self.push_tag(CommandTag::SyncChildren);
2208            }
2209            Command::Callback(callback) => {
2210                self.callbacks.push(callback);
2211                self.push_tag(CommandTag::Callback);
2212            }
2213        }
2214    }
2215
2216    fn len(&self) -> usize {
2217        self.len
2218    }
2219
2220    fn capacity(&self) -> usize {
2221        self.chunks.iter().map(Vec::capacity).sum()
2222    }
2223
2224    fn payload_len_bytes(&self) -> usize {
2225        self.bubble_dirty
2226            .len()
2227            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2228            .saturating_add(
2229                self.update_typed_nodes
2230                    .len()
2231                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2232            )
2233            .saturating_add(
2234                self.remove_nodes
2235                    .len()
2236                    .saturating_mul(std::mem::size_of::<NodeId>()),
2237            )
2238            .saturating_add(
2239                self.mount_nodes
2240                    .len()
2241                    .saturating_mul(std::mem::size_of::<NodeId>()),
2242            )
2243            .saturating_add(
2244                self.attach_children
2245                    .len()
2246                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2247            )
2248            .saturating_add(
2249                self.insert_children
2250                    .len()
2251                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2252            )
2253            .saturating_add(
2254                self.move_children
2255                    .len()
2256                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2257            )
2258            .saturating_add(
2259                self.remove_children
2260                    .len()
2261                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2262            )
2263            .saturating_add(
2264                self.detach_children
2265                    .len()
2266                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2267            )
2268            .saturating_add(
2269                self.sync_children
2270                    .len()
2271                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2272            )
2273            .saturating_add(
2274                self.sync_child_ids
2275                    .len()
2276                    .saturating_mul(std::mem::size_of::<NodeId>()),
2277            )
2278            .saturating_add(
2279                self.callbacks
2280                    .len()
2281                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2282            )
2283    }
2284
2285    fn payload_capacity_bytes(&self) -> usize {
2286        self.bubble_dirty
2287            .capacity()
2288            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2289            .saturating_add(
2290                self.update_typed_nodes
2291                    .capacity()
2292                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2293            )
2294            .saturating_add(
2295                self.remove_nodes
2296                    .capacity()
2297                    .saturating_mul(std::mem::size_of::<NodeId>()),
2298            )
2299            .saturating_add(
2300                self.mount_nodes
2301                    .capacity()
2302                    .saturating_mul(std::mem::size_of::<NodeId>()),
2303            )
2304            .saturating_add(
2305                self.attach_children
2306                    .capacity()
2307                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2308            )
2309            .saturating_add(
2310                self.insert_children
2311                    .capacity()
2312                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2313            )
2314            .saturating_add(
2315                self.move_children
2316                    .capacity()
2317                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2318            )
2319            .saturating_add(
2320                self.remove_children
2321                    .capacity()
2322                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2323            )
2324            .saturating_add(
2325                self.detach_children
2326                    .capacity()
2327                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2328            )
2329            .saturating_add(
2330                self.sync_children
2331                    .capacity()
2332                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2333            )
2334            .saturating_add(
2335                self.sync_child_ids
2336                    .capacity()
2337                    .saturating_mul(std::mem::size_of::<NodeId>()),
2338            )
2339            .saturating_add(
2340                self.callbacks
2341                    .capacity()
2342                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2343            )
2344    }
2345
2346    fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2347        let mut bubble_dirty = self.bubble_dirty.into_iter();
2348        let mut update_typed_nodes = self.update_typed_nodes.into_iter();
2349        let mut remove_nodes = self.remove_nodes.into_iter();
2350        let mut mount_nodes = self.mount_nodes.into_iter();
2351        let mut attach_children = self.attach_children.into_iter();
2352        let mut insert_children = self.insert_children.into_iter();
2353        let mut move_children = self.move_children.into_iter();
2354        let mut remove_children = self.remove_children.into_iter();
2355        let mut detach_children = self.detach_children.into_iter();
2356        let mut sync_children_commands = self.sync_children.into_iter();
2357        let sync_child_ids = self.sync_child_ids;
2358        let mut callbacks = self.callbacks.into_iter();
2359        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2360
2361        for chunk in self.chunks {
2362            for tag in chunk {
2363                match tag {
2364                    CommandTag::BubbleDirty => {
2365                        let BubbleDirtyCommand { node_id, bubble } =
2366                            next_command_payload(&mut bubble_dirty, tag)?;
2367                        Command::BubbleDirty { node_id, bubble }
2368                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2369                    }
2370                    CommandTag::UpdateTypedNode => {
2371                        let UpdateTypedNodeCommand { id, updater } =
2372                            next_command_payload(&mut update_typed_nodes, tag)?;
2373                        Command::UpdateTypedNode { id, updater }
2374                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2375                    }
2376                    CommandTag::RemoveNode => {
2377                        let id = next_command_payload(&mut remove_nodes, tag)?;
2378                        Command::RemoveNode { id }
2379                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2380                    }
2381                    CommandTag::MountNode => {
2382                        let id = next_command_payload(&mut mount_nodes, tag)?;
2383                        Command::MountNode { id }
2384                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2385                    }
2386                    CommandTag::AttachChild => {
2387                        let AttachChildCommand {
2388                            parent_id,
2389                            child_id,
2390                            insert_index,
2391                            bubble,
2392                        } = next_command_payload(&mut attach_children, tag)?;
2393                        Command::AttachChild {
2394                            parent_id,
2395                            child_id,
2396                            insert_index,
2397                            bubble,
2398                        }
2399                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2400                    }
2401                    CommandTag::InsertChild => {
2402                        let InsertChildCommand {
2403                            parent_id,
2404                            child_id,
2405                            appended_index,
2406                            insert_index,
2407                            bubble,
2408                        } = next_command_payload(&mut insert_children, tag)?;
2409                        Command::InsertChild {
2410                            parent_id,
2411                            child_id,
2412                            appended_index,
2413                            insert_index,
2414                            bubble,
2415                        }
2416                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2417                    }
2418                    CommandTag::MoveChild => {
2419                        let MoveChildCommand {
2420                            parent_id,
2421                            from_index,
2422                            to_index,
2423                            bubble,
2424                        } = next_command_payload(&mut move_children, tag)?;
2425                        Command::MoveChild {
2426                            parent_id,
2427                            from_index,
2428                            to_index,
2429                            bubble,
2430                        }
2431                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2432                    }
2433                    CommandTag::RemoveChild => {
2434                        let RemoveChildCommand {
2435                            parent_id,
2436                            child_id,
2437                        } = next_command_payload(&mut remove_children, tag)?;
2438                        Command::RemoveChild {
2439                            parent_id,
2440                            child_id,
2441                        }
2442                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2443                    }
2444                    CommandTag::DetachChild => {
2445                        let DetachChildCommand {
2446                            parent_id,
2447                            child_id,
2448                        } = next_command_payload(&mut detach_children, tag)?;
2449                        Command::DetachChild {
2450                            parent_id,
2451                            child_id,
2452                        }
2453                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2454                    }
2455                    CommandTag::SyncChildren => {
2456                        let SyncChildrenCommand {
2457                            parent_id,
2458                            child_start,
2459                            child_len,
2460                        } = next_command_payload(&mut sync_children_commands, tag)?;
2461                        let child_end = child_start
2462                            .checked_add(child_len)
2463                            .ok_or_else(|| command_payload_error(tag))?;
2464                        let expected_children = sync_child_ids
2465                            .get(child_start..child_end)
2466                            .ok_or_else(|| command_payload_error(tag))?;
2467                        sync_children(
2468                            applier,
2469                            parent_id,
2470                            expected_children,
2471                            &mut deferred_cleanup,
2472                        )?;
2473                    }
2474                    CommandTag::Callback => {
2475                        let callback = next_command_payload(&mut callbacks, tag)?;
2476                        Command::Callback(callback)
2477                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2478                    }
2479                }
2480            }
2481        }
2482
2483        debug_assert!(bubble_dirty.next().is_none());
2484        debug_assert!(update_typed_nodes.next().is_none());
2485        debug_assert!(remove_nodes.next().is_none());
2486        debug_assert!(mount_nodes.next().is_none());
2487        debug_assert!(attach_children.next().is_none());
2488        debug_assert!(insert_children.next().is_none());
2489        debug_assert!(move_children.next().is_none());
2490        debug_assert!(remove_children.next().is_none());
2491        debug_assert!(detach_children.next().is_none());
2492        debug_assert!(sync_children_commands.next().is_none());
2493        debug_assert!(callbacks.next().is_none());
2494        deferred_cleanup.flush(applier)
2495    }
2496}
2497
2498fn command_payload_error(tag: CommandTag) -> NodeError {
2499    NodeError::MalformedCommandPayload { tag: tag.label() }
2500}
2501
2502fn next_command_payload<T>(
2503    payloads: &mut impl Iterator<Item = T>,
2504    tag: CommandTag,
2505) -> Result<T, NodeError> {
2506    payloads.next().ok_or_else(|| command_payload_error(tag))
2507}
2508
2509fn update_typed_node<N: Node + 'static>(node: &mut dyn Node, id: NodeId) -> Result<(), NodeError> {
2510    let typed = node
2511        .as_any_mut()
2512        .downcast_mut::<N>()
2513        .ok_or(NodeError::TypeMismatch {
2514            id,
2515            expected: std::any::type_name::<N>(),
2516        })?;
2517    typed.update();
2518    Ok(())
2519}
2520
2521fn attach_child_at(
2522    applier: &mut dyn Applier,
2523    parent_id: NodeId,
2524    child_id: NodeId,
2525    insert_index: Option<usize>,
2526    bubble: DirtyBubble,
2527) {
2528    if insert_child_with_reparenting(applier, parent_id, child_id) {
2529        if let Some(target) = insert_index {
2530            move_appended_child_to(applier, parent_id, target);
2531        }
2532        bubble.apply(applier, parent_id);
2533    } else if let Ok(child) = applier.get_mut(child_id) {
2534        let dirty_bubble = DirtyBubble {
2535            layout: child.needs_layout(),
2536            measure: child.needs_measure(),
2537            semantics: false,
2538        };
2539        dirty_bubble.apply(applier, parent_id);
2540    }
2541}
2542
2543fn move_appended_child_to(applier: &mut dyn Applier, parent_id: NodeId, target: usize) {
2544    let Ok(parent_node) = applier.get_mut(parent_id) else {
2545        return;
2546    };
2547    let mut owned: SmallVec<[NodeId; 8]> = SmallVec::new();
2548    parent_node.collect_owned_children_into(&mut owned);
2549    let appended_index = owned.len().saturating_sub(1);
2550    if target < appended_index {
2551        parent_node.move_child(appended_index, target);
2552        note_structural_move(parent_id, appended_index, target);
2553    }
2554}
2555
2556fn insert_child_with_reparenting(
2557    applier: &mut dyn Applier,
2558    parent_id: NodeId,
2559    child_id: NodeId,
2560) -> bool {
2561    if parent_id == child_id {
2562        debug_assert_ne!(
2563            parent_id, child_id,
2564            "a node cannot be attached as its own child"
2565        );
2566        return false;
2567    }
2568
2569    let old_parent = applier
2570        .get_mut(child_id)
2571        .ok()
2572        .and_then(|node| node.parent());
2573    if let Some(old_parent_id) = old_parent
2574        && old_parent_id != parent_id
2575    {
2576        let removed = applier
2577            .get_mut(old_parent_id)
2578            .is_ok_and(|old_parent_node| old_parent_node.remove_child(child_id));
2579        if let Ok(child_node) = applier.get_mut(child_id) {
2580            child_node.on_removed_from_parent();
2581        }
2582        if removed {
2583            bubble_layout_dirty(applier, old_parent_id);
2584            bubble_measure_dirty(applier, old_parent_id);
2585            note_structural("reparent-detach", old_parent_id, child_id);
2586            applier.record_structural_change(old_parent_id);
2587        }
2588    }
2589
2590    let inserted = applier
2591        .get_mut(parent_id)
2592        .is_ok_and(|parent_node| parent_node.insert_child(child_id));
2593    if inserted {
2594        note_structural("attach", parent_id, child_id);
2595        applier.record_structural_change(parent_id);
2596    }
2597    if let Ok(child_node) = applier.get_mut(child_id) {
2598        child_node.on_attached_to_parent(parent_id);
2599    }
2600    inserted
2601}
2602
2603fn apply_remove_child(
2604    applier: &mut dyn Applier,
2605    parent_id: NodeId,
2606    child_id: NodeId,
2607    deferred_cleanup: &mut DeferredChildCleanupQueue,
2608) -> Result<(), NodeError> {
2609    detach_child_from_parent(applier, parent_id, child_id)?;
2610
2611    let generation = applier.node_generation(child_id);
2612    let removed_from_parent = if let Ok(node) = applier.get_mut(child_id) {
2613        node.parent().is_none()
2614    } else {
2615        return Ok(());
2616    };
2617    deferred_cleanup.push(child_id, generation, removed_from_parent);
2618    Ok(())
2619}
2620
2621fn detach_child_from_parent(
2622    applier: &mut dyn Applier,
2623    parent_id: NodeId,
2624    child_id: NodeId,
2625) -> Result<(), NodeError> {
2626    let removed = applier
2627        .get_mut(parent_id)
2628        .is_ok_and(|parent_node| parent_node.remove_child(child_id));
2629    if removed {
2630        bubble_layout_dirty(applier, parent_id);
2631        bubble_measure_dirty(applier, parent_id);
2632        note_structural("detach", parent_id, child_id);
2633        applier.record_structural_change(parent_id);
2634    }
2635
2636    if let Ok(node) = applier.get_mut(child_id) {
2637        match node.parent() {
2638            Some(existing_parent_id) if existing_parent_id == parent_id => {
2639                node.on_removed_from_parent();
2640            }
2641            None => {}
2642            Some(_) => return Ok(()),
2643        }
2644    } else {
2645        return Ok(());
2646    }
2647
2648    Ok(())
2649}
2650
2651fn cleanup_detached_child(
2652    applier: &mut dyn Applier,
2653    cleanup: DeferredChildCleanup,
2654) -> Result<(), NodeError> {
2655    if applier.node_generation(cleanup.child_id) != cleanup.generation {
2656        return Ok(());
2657    }
2658
2659    let parent_id = match applier.get_mut(cleanup.child_id) {
2660        Ok(node) => node.parent(),
2661        Err(NodeError::Missing { .. }) => return Ok(()),
2662        Err(err) => return Err(err),
2663    };
2664    if parent_id.is_some() {
2665        return Ok(());
2666    }
2667
2668    if let Ok(node) = applier.get_mut(cleanup.child_id) {
2669        if !cleanup.removed_from_parent {
2670            node.on_removed_from_parent();
2671        }
2672        node.unmount();
2673    }
2674    match applier.remove(cleanup.child_id) {
2675        Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
2676        Err(err) => Err(err),
2677    }
2678}
2679
2680fn remove_child_and_cleanup_now(
2681    applier: &mut dyn Applier,
2682    parent_id: NodeId,
2683    child_id: NodeId,
2684) -> Result<(), NodeError> {
2685    let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2686    apply_remove_child(applier, parent_id, child_id, &mut deferred_cleanup)?;
2687    deferred_cleanup.flush(applier)
2688}
2689
2690fn collect_current_children(applier: &mut dyn Applier, parent_id: NodeId) -> ChildList {
2691    let mut scratch = SmallVec::<[NodeId; 8]>::new();
2692    if let Ok(node) = applier.get_mut(parent_id) {
2693        node.collect_children_into(&mut scratch);
2694    }
2695    let mut current = ChildList::new();
2696    current.extend(scratch);
2697    current
2698}
2699
2700fn sync_children(
2701    applier: &mut dyn Applier,
2702    parent_id: NodeId,
2703    expected_children: &[NodeId],
2704    deferred_cleanup: &mut DeferredChildCleanupQueue,
2705) -> Result<(), NodeError> {
2706    let mut current = collect_current_children(applier, parent_id);
2707    let children_changed = current.as_slice() != expected_children;
2708
2709    if children_changed {
2710        if current.len().max(expected_children.len()) <= SMALL_CHILD_SYNC_LINEAR_THRESHOLD {
2711            sync_children_small(
2712                applier,
2713                parent_id,
2714                &mut current,
2715                expected_children,
2716                deferred_cleanup,
2717            )?;
2718        } else {
2719            let mut target_positions: HashMap<NodeId, usize> = HashMap::default();
2720            target_positions.reserve(expected_children.len());
2721            for (index, &child) in expected_children.iter().enumerate() {
2722                target_positions.insert(child, index);
2723            }
2724
2725            for index in (0..current.len()).rev() {
2726                let child = current[index];
2727                if !target_positions.contains_key(&child) {
2728                    current.remove(index);
2729                    apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2730                }
2731            }
2732
2733            let mut current_positions = build_child_positions(&current);
2734            for (target_index, &child) in expected_children.iter().enumerate() {
2735                if let Some(current_index) = current_positions.get(&child).copied() {
2736                    if current_index != target_index {
2737                        let from_index = current_index;
2738                        let to_index = move_child_in_diff_state(
2739                            &mut current,
2740                            &mut current_positions,
2741                            from_index,
2742                            target_index,
2743                        );
2744                        Command::MoveChild {
2745                            parent_id,
2746                            from_index,
2747                            to_index,
2748                            bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2749                        }
2750                        .apply(applier)?;
2751                    }
2752                } else {
2753                    let insert_index = target_index.min(current.len());
2754                    let appended_index = current.len();
2755                    insert_child_into_diff_state(
2756                        &mut current,
2757                        &mut current_positions,
2758                        insert_index,
2759                        child,
2760                    );
2761                    Command::InsertChild {
2762                        parent_id,
2763                        child_id: child,
2764                        appended_index,
2765                        insert_index,
2766                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2767                    }
2768                    .apply(applier)?;
2769                }
2770            }
2771        }
2772    }
2773
2774    reconcile_children(applier, parent_id, expected_children, !children_changed)
2775}
2776
2777fn sync_children_small(
2778    applier: &mut dyn Applier,
2779    parent_id: NodeId,
2780    current: &mut ChildList,
2781    expected_children: &[NodeId],
2782    deferred_cleanup: &mut DeferredChildCleanupQueue,
2783) -> Result<(), NodeError> {
2784    for index in (0..current.len()).rev() {
2785        let child = current[index];
2786        if !expected_children.contains(&child) {
2787            current.remove(index);
2788            apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2789        }
2790    }
2791
2792    for (target_index, &child) in expected_children.iter().enumerate() {
2793        if let Some(current_index) = current
2794            .iter()
2795            .position(|&current_child| current_child == child)
2796        {
2797            if current_index != target_index {
2798                let child = current.remove(current_index);
2799                let to_index = target_index.min(current.len());
2800                current.insert(to_index, child);
2801                Command::MoveChild {
2802                    parent_id,
2803                    from_index: current_index,
2804                    to_index,
2805                    bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2806                }
2807                .apply(applier)?;
2808            }
2809        } else {
2810            let insert_index = target_index.min(current.len());
2811            let appended_index = current.len();
2812            current.insert(insert_index, child);
2813            Command::InsertChild {
2814                parent_id,
2815                child_id: child,
2816                appended_index,
2817                insert_index,
2818                bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2819            }
2820            .apply(applier)?;
2821        }
2822    }
2823
2824    Ok(())
2825}
2826
2827fn reconcile_children(
2828    applier: &mut dyn Applier,
2829    parent_id: NodeId,
2830    expected_children: &[NodeId],
2831    needs_dirty_check: bool,
2832) -> Result<(), NodeError> {
2833    let mut repaired = false;
2834    for &child_id in expected_children {
2835        let needs_attach = if let Ok(node) = applier.get_mut(child_id) {
2836            node.parent() != Some(parent_id)
2837        } else {
2838            false
2839        };
2840
2841        if needs_attach {
2842            insert_child_with_reparenting(applier, parent_id, child_id);
2843            repaired = true;
2844        }
2845    }
2846
2847    let is_dirty = if needs_dirty_check {
2848        if let Ok(node) = applier.get_mut(parent_id) {
2849            node.needs_layout()
2850        } else {
2851            false
2852        }
2853    } else {
2854        false
2855    };
2856
2857    if repaired {
2858        bubble_layout_dirty(applier, parent_id);
2859        bubble_measure_dirty(applier, parent_id);
2860    } else if is_dirty {
2861        bubble_layout_dirty(applier, parent_id);
2862    }
2863
2864    Ok(())
2865}
2866
2867#[derive(Default)]
2868pub struct MemoryApplier {
2869    nodes: Vec<Option<Box<dyn Node>>>,
2870    physical_stable_ids: Vec<u32>,
2871    physical_warm_recycled_origins: Vec<bool>,
2872    stable_to_physical: HashMap<NodeId, usize>,
2873    stable_generations: HashMap<NodeId, u32>,
2874    free_ids: BinaryHeap<Reverse<usize>>,
2875    high_id_nodes: HashMap<NodeId, Box<dyn Node>>,
2876    high_id_warm_recycled_origins: HashMap<NodeId, bool>,
2877    high_id_generations: HashMap<NodeId, u32>,
2878    next_stable_id: NodeId,
2879    layout_runtime: Option<RuntimeHandle>,
2880    slots: SlotTable,
2881    recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2882    returning_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2883    cold_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2884    recycled_node_limits: HashMap<TypeId, usize>,
2885    warm_recycled_node_targets: HashMap<TypeId, usize>,
2886    fresh_recyclable_creations: HashMap<TypeId, usize>,
2887    recycled_node_prototypes: HashMap<TypeId, Box<dyn Node>>,
2888    structural_change_parents: Vec<NodeId>,
2889    virtual_node_ids: HashSet<NodeId>,
2890}
2891
2892struct RemovalFrame {
2893    node_id: NodeId,
2894    children: SmallVec<[NodeId; 8]>,
2895    next_child: usize,
2896}
2897
2898#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2899pub struct MemoryApplierDebugStats {
2900    pub next_stable_id: NodeId,
2901    pub nodes_len: usize,
2902    pub nodes_cap: usize,
2903    pub physical_stable_ids_len: usize,
2904    pub physical_stable_ids_cap: usize,
2905    pub stable_to_physical_len: usize,
2906    pub stable_to_physical_cap: usize,
2907    pub stable_generations_len: usize,
2908    pub stable_generations_cap: usize,
2909    pub free_ids_len: usize,
2910    pub free_ids_cap: usize,
2911    pub high_id_nodes_len: usize,
2912    pub high_id_nodes_cap: usize,
2913    pub high_id_generations_len: usize,
2914    pub high_id_generations_cap: usize,
2915    pub recycled_type_count: usize,
2916    pub recycled_type_cap: usize,
2917    pub recycled_node_count: usize,
2918    pub recycled_node_capacity: usize,
2919    pub warm_recycled_node_id_count: usize,
2920    pub warm_recycled_node_id_capacity: usize,
2921}
2922
2923impl MemoryApplier {
2924    const EAGER_COMPACT_NODE_LEN: usize = 1_024;
2925    const HIGH_ID_THRESHOLD: NodeId = 1_000_000_000;
2926    const INVALID_STABLE_ID: u32 = u32::MAX;
2927    const INITIAL_DENSE_NODE_CAP: usize = 32;
2928    const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
2929    const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
2930
2931    fn pack_stable_id(stable_id: NodeId) -> u32 {
2932        u32::try_from(stable_id).expect("stable id overflow")
2933    }
2934
2935    fn unpack_stable_id(stable_id: u32) -> NodeId {
2936        stable_id as NodeId
2937    }
2938
2939    fn next_dense_node_target_len(old_len: usize) -> usize {
2940        if old_len < Self::INITIAL_DENSE_NODE_CAP {
2941            return Self::INITIAL_DENSE_NODE_CAP;
2942        }
2943        if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
2944            return old_len.saturating_mul(2);
2945        }
2946
2947        let incremental_growth =
2948            (old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
2949        old_len.saturating_add(incremental_growth)
2950    }
2951
2952    fn ensure_dense_node_storage_capacity(&mut self) {
2953        let len = self
2954            .nodes
2955            .len()
2956            .max(self.physical_stable_ids.len())
2957            .max(self.physical_warm_recycled_origins.len());
2958        if len < self.nodes.capacity()
2959            && len < self.physical_stable_ids.capacity()
2960            && len < self.physical_warm_recycled_origins.capacity()
2961        {
2962            return;
2963        }
2964
2965        let target = Self::next_dense_node_target_len(len);
2966        if self.nodes.capacity() < target {
2967            self.nodes
2968                .reserve_exact(target.saturating_sub(self.nodes.len()));
2969        }
2970        if self.physical_stable_ids.capacity() < target {
2971            self.physical_stable_ids
2972                .reserve_exact(target.saturating_sub(self.physical_stable_ids.len()));
2973        }
2974        if self.physical_warm_recycled_origins.capacity() < target {
2975            self.physical_warm_recycled_origins
2976                .reserve_exact(target.saturating_sub(self.physical_warm_recycled_origins.len()));
2977        }
2978    }
2979
2980    fn ensure_stable_index_capacity(&mut self) {
2981        let len = self
2982            .stable_to_physical
2983            .len()
2984            .max(self.stable_generations.len());
2985        if len < self.stable_to_physical.capacity() && len < self.stable_generations.capacity() {
2986            return;
2987        }
2988
2989        let target = Self::next_dense_node_target_len(len);
2990        let additional = target.saturating_sub(len);
2991        if self.stable_to_physical.capacity() < target {
2992            self.stable_to_physical.reserve(additional);
2993        }
2994        if self.stable_generations.capacity() < target {
2995            self.stable_generations.reserve(additional);
2996        }
2997    }
2998
2999    pub fn new() -> Self {
3000        Self {
3001            nodes: Vec::new(),
3002            physical_stable_ids: Vec::new(),
3003            physical_warm_recycled_origins: Vec::new(),
3004            stable_to_physical: HashMap::default(),
3005            stable_generations: HashMap::default(),
3006            free_ids: BinaryHeap::new(),
3007            high_id_nodes: HashMap::default(),
3008            high_id_warm_recycled_origins: HashMap::default(),
3009            high_id_generations: HashMap::default(),
3010            next_stable_id: 0,
3011            layout_runtime: None,
3012            slots: SlotTable::default(),
3013            recycled_nodes: HashMap::default(),
3014            returning_recycled_nodes: HashMap::default(),
3015            cold_recycled_nodes: HashMap::default(),
3016            recycled_node_limits: HashMap::default(),
3017            warm_recycled_node_targets: HashMap::default(),
3018            fresh_recyclable_creations: HashMap::default(),
3019            recycled_node_prototypes: HashMap::default(),
3020            structural_change_parents: Vec::new(),
3021            virtual_node_ids: HashSet::default(),
3022        }
3023    }
3024
3025    pub fn slots(&mut self) -> &mut SlotTable {
3026        &mut self.slots
3027    }
3028
3029    /// Drains the parents recorded via [`Applier::record_structural_change`],
3030    /// keeping only nodes still attached to `root` (a parent that was itself
3031    /// removed is covered by its own surviving ancestor's record). A virtual
3032    /// parent — a subcompose slot wrapper the render graph never contains —
3033    /// is reported as its nearest non-virtual ancestor: that is the node
3034    /// whose graph child set the change altered, and an id the graph cannot
3035    /// resolve would force the scoped scene update to give up and rebuild.
3036    /// Resolves a scene-scope candidate the way structural records are
3037    /// resolved: to its nearest non-virtual ancestor, and only while still
3038    /// attached to `root`. A node detached after recording must not reach the
3039    /// scoped scene update — an id the graph cannot resolve forces it to give
3040    /// up and rebuild the whole scene.
3041    pub fn scene_node_attached_to(&mut self, node_id: NodeId, root: NodeId) -> Option<NodeId> {
3042        let resolved = self.first_non_virtual_ancestor(node_id)?;
3043        self.is_attached_to(resolved, root).then_some(resolved)
3044    }
3045
3046    pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
3047        let recorded = std::mem::take(&mut self.structural_change_parents);
3048        let mut attached = Vec::with_capacity(recorded.len());
3049        for parent_id in recorded {
3050            let Some(parent_id) = self.first_non_virtual_ancestor(parent_id) else {
3051                continue;
3052            };
3053            if self.is_attached_to(parent_id, root) && !attached.contains(&parent_id) {
3054                attached.push(parent_id);
3055            }
3056        }
3057        attached
3058    }
3059
3060    fn first_non_virtual_ancestor(&mut self, node_id: NodeId) -> Option<NodeId> {
3061        let mut current = node_id;
3062        for _ in 0..100_000 {
3063            if !self.virtual_node_ids.contains(&current) {
3064                return Some(current);
3065            }
3066            match self.get_mut(current) {
3067                Ok(node) => current = node.parent()?,
3068                Err(_) => return None,
3069            }
3070        }
3071        None
3072    }
3073
3074    fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
3075        let mut current = node_id;
3076        for _ in 0..100_000 {
3077            if current == root {
3078                return true;
3079            }
3080            match self.get_mut(current) {
3081                Ok(node) => match node.parent() {
3082                    Some(parent) => current = parent,
3083                    None => return false,
3084                },
3085                Err(_) => return false,
3086            }
3087        }
3088        false
3089    }
3090
3091    pub fn with_node<N: Node + 'static, R>(
3092        &mut self,
3093        id: NodeId,
3094        f: impl FnOnce(&mut N) -> R,
3095    ) -> Result<R, NodeError> {
3096        let physical_id = self
3097            .resolve_node_index(id)
3098            .ok_or(NodeError::Missing { id })?;
3099        let slot = self
3100            .nodes
3101            .get_mut(physical_id)
3102            .ok_or(NodeError::Missing { id })?
3103            .as_deref_mut()
3104            .ok_or(NodeError::Missing { id })?;
3105        let typed = slot
3106            .as_any_mut()
3107            .downcast_mut::<N>()
3108            .ok_or(NodeError::TypeMismatch {
3109                id,
3110                expected: std::any::type_name::<N>(),
3111            })?;
3112        Ok(f(typed))
3113    }
3114
3115    pub fn len(&self) -> usize {
3116        self.nodes.iter().filter(|n| n.is_some()).count()
3117    }
3118
3119    pub fn capacity(&self) -> usize {
3120        self.nodes.len()
3121    }
3122
3123    pub fn tombstone_count(&self) -> usize {
3124        self.nodes.iter().filter(|n| n.is_none()).count()
3125    }
3126
3127    pub fn freelist_len(&self) -> usize {
3128        self.free_ids.len()
3129    }
3130
3131    pub fn debug_recycled_node_count(&self) -> usize {
3132        self.total_recycled_node_count()
3133    }
3134
3135    pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
3136        let key = TypeId::of::<N>();
3137        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3138            + self
3139                .returning_recycled_nodes
3140                .get(&key)
3141                .map(Vec::len)
3142                .unwrap_or(0)
3143            + self
3144                .cold_recycled_nodes
3145                .get(&key)
3146                .map(Vec::len)
3147                .unwrap_or(0)
3148    }
3149
3150    pub fn debug_stats(&self) -> MemoryApplierDebugStats {
3151        let mut recycled_keys: HashSet<TypeId> = HashSet::default();
3152        recycled_keys.extend(self.recycled_nodes.keys().copied());
3153        recycled_keys.extend(self.returning_recycled_nodes.keys().copied());
3154        recycled_keys.extend(self.cold_recycled_nodes.keys().copied());
3155
3156        MemoryApplierDebugStats {
3157            next_stable_id: self.next_stable_id,
3158            nodes_len: self.len(),
3159            nodes_cap: self.nodes.len(),
3160            physical_stable_ids_len: self.physical_stable_ids.len(),
3161            physical_stable_ids_cap: self.physical_stable_ids.capacity(),
3162            stable_to_physical_len: self.stable_to_physical.len(),
3163            stable_to_physical_cap: self.stable_to_physical.capacity(),
3164            stable_generations_len: self.stable_generations.len(),
3165            stable_generations_cap: self.stable_generations.capacity(),
3166            free_ids_len: self.free_ids.len(),
3167            free_ids_cap: self.free_ids.capacity(),
3168            high_id_nodes_len: self.high_id_nodes.len(),
3169            high_id_nodes_cap: self.high_id_nodes.capacity(),
3170            high_id_generations_len: self.high_id_generations.len(),
3171            high_id_generations_cap: self.high_id_generations.capacity(),
3172            recycled_type_count: recycled_keys.len(),
3173            recycled_type_cap: self.recycled_nodes.capacity()
3174                + self.returning_recycled_nodes.capacity()
3175                + self.cold_recycled_nodes.capacity(),
3176            recycled_node_count: self.total_recycled_node_count(),
3177            recycled_node_capacity: self.total_recycled_node_capacity(),
3178            warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
3179            warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
3180        }
3181    }
3182
3183    pub fn is_empty(&self) -> bool {
3184        self.len() == 0
3185    }
3186
3187    pub fn debug_live_node_heap_bytes(&self) -> usize {
3188        let dense_nodes = self
3189            .nodes
3190            .iter()
3191            .flatten()
3192            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3193            .sum::<usize>();
3194        let high_id_nodes = self
3195            .high_id_nodes
3196            .values()
3197            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3198            .sum::<usize>();
3199        dense_nodes + high_id_nodes
3200    }
3201
3202    pub fn debug_recycled_node_heap_bytes(&self) -> usize {
3203        let pool_bytes = |pools: &HashMap<TypeId, Vec<RecycledNode>>| {
3204            pools
3205                .values()
3206                .flat_map(|nodes| nodes.iter())
3207                .map(|node| std::mem::size_of_val(&*node.node) + node.node.debug_heap_bytes())
3208                .sum::<usize>()
3209        };
3210
3211        pool_bytes(&self.recycled_nodes)
3212            + pool_bytes(&self.returning_recycled_nodes)
3213            + pool_bytes(&self.cold_recycled_nodes)
3214    }
3215
3216    pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
3217        self.layout_runtime = Some(handle);
3218    }
3219
3220    pub fn clear_runtime_handle(&mut self) {
3221        self.layout_runtime = None;
3222    }
3223
3224    pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
3225        self.layout_runtime.clone()
3226    }
3227
3228    fn pool_node_count(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3229        pools.values().map(Vec::len).sum()
3230    }
3231
3232    fn pool_node_capacity(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3233        pools.values().map(Vec::capacity).sum()
3234    }
3235
3236    fn total_recycled_node_count(&self) -> usize {
3237        Self::pool_node_count(&self.recycled_nodes)
3238            + Self::pool_node_count(&self.returning_recycled_nodes)
3239            + Self::pool_node_count(&self.cold_recycled_nodes)
3240    }
3241
3242    fn total_recycled_node_capacity(&self) -> usize {
3243        Self::pool_node_capacity(&self.recycled_nodes)
3244            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3245            + Self::pool_node_capacity(&self.cold_recycled_nodes)
3246    }
3247
3248    fn total_warm_recycled_node_id_count(&self) -> usize {
3249        self.live_warm_recycled_origin_count()
3250            + Self::pool_node_count(&self.recycled_nodes)
3251            + Self::pool_node_count(&self.returning_recycled_nodes)
3252    }
3253
3254    fn total_warm_recycled_node_id_capacity(&self) -> usize {
3255        self.live_warm_recycled_origin_capacity()
3256            + Self::pool_node_capacity(&self.recycled_nodes)
3257            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3258    }
3259
3260    fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
3261        if let Some(limit) = recycle_pool_limit {
3262            self.recycled_node_limits.insert(key, limit);
3263        } else {
3264            self.recycled_node_limits.remove(&key);
3265        }
3266    }
3267
3268    fn recycle_pool_limit_for(&self, key: TypeId) -> Option<usize> {
3269        self.recycled_node_limits.get(&key).copied()
3270    }
3271
3272    fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
3273        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3274    }
3275
3276    fn warm_recycled_node_target(&self, key: TypeId) -> usize {
3277        self.warm_recycled_node_targets
3278            .get(&key)
3279            .copied()
3280            .unwrap_or(0)
3281    }
3282
3283    fn warm_recycled_node_target_limit(&self, key: TypeId) -> usize {
3284        let Some(limit) = self.recycle_pool_limit_for(key) else {
3285            return usize::MAX;
3286        };
3287        if limit <= 8 { limit } else { limit / 4 }
3288    }
3289
3290    fn update_warm_recycled_node_target(&mut self, key: TypeId, observed_demand: usize) -> usize {
3291        let target_limit = self.warm_recycled_node_target_limit(key);
3292        let existing = self.warm_recycled_node_target(key).min(target_limit);
3293        if observed_demand == 0 {
3294            return existing;
3295        }
3296
3297        let target = match self.recycle_pool_limit_for(key) {
3298            Some(limit) if limit > 8 => target_limit,
3299            Some(_) => observed_demand.min(target_limit),
3300            None => observed_demand,
3301        };
3302        self.warm_recycled_node_targets.insert(key, target);
3303        target
3304    }
3305
3306    fn remember_recycled_node_prototype(&mut self, key: TypeId, shell: &dyn Node) {
3307        if self.recycled_node_prototypes.contains_key(&key) {
3308            return;
3309        }
3310        if let Some(prototype) = shell.rehouse_for_recycle() {
3311            self.recycled_node_prototypes.insert(key, prototype);
3312        }
3313    }
3314
3315    fn live_warm_recycled_origin_count(&self) -> usize {
3316        self.physical_warm_recycled_origins
3317            .iter()
3318            .zip(self.nodes.iter())
3319            .filter(|(warm_origin, node)| **warm_origin && node.is_some())
3320            .count()
3321            + self
3322                .high_id_warm_recycled_origins
3323                .values()
3324                .filter(|warm_origin| **warm_origin)
3325                .count()
3326    }
3327
3328    fn live_warm_recycled_origin_capacity(&self) -> usize {
3329        self.physical_warm_recycled_origins.capacity()
3330            + self.high_id_warm_recycled_origins.capacity()
3331    }
3332
3333    fn push_recycled_node(
3334        &mut self,
3335        key: TypeId,
3336        recycle_pool_limit: Option<usize>,
3337        recycled: RecycledNode,
3338    ) {
3339        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3340        self.remember_recycled_node_prototype(key, recycled.node.as_ref());
3341
3342        let warm_origin = recycled.warm_origin();
3343        let pool = if warm_origin {
3344            self.returning_recycled_nodes.entry(key).or_default()
3345        } else {
3346            self.cold_recycled_nodes.entry(key).or_default()
3347        };
3348        pool.push(recycled);
3349        if let Some(limit) = recycle_pool_limit
3350            && pool.len() > limit
3351        {
3352            let excess = pool.len() - limit;
3353            let dropped: Vec<_> = pool.drain(0..excess).collect();
3354            drop(dropped);
3355        }
3356    }
3357
3358    fn push_warm_recycled_node(
3359        &mut self,
3360        key: TypeId,
3361        recycle_pool_limit: Option<usize>,
3362        mut recycled: RecycledNode,
3363    ) {
3364        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3365
3366        recycled.set_warm_origin(true);
3367        let mut dropped = Vec::new();
3368        let mut remove_pool_entry = false;
3369        {
3370            let pool = self.recycled_nodes.entry(key).or_default();
3371            pool.push(recycled);
3372            if let Some(limit) = recycle_pool_limit
3373                && pool.len() > limit
3374            {
3375                let excess = pool.len() - limit;
3376                dropped = pool.drain(0..excess).collect();
3377                remove_pool_entry = pool.is_empty();
3378            }
3379        }
3380        if remove_pool_entry {
3381            self.recycled_nodes.remove(&key);
3382        }
3383        drop(dropped);
3384    }
3385
3386    fn seed_recycled_node_shell_impl(
3387        &mut self,
3388        key: TypeId,
3389        recycle_pool_limit: Option<usize>,
3390        shell: Box<dyn Node>,
3391    ) {
3392        let limit = recycle_pool_limit.unwrap_or(usize::MAX);
3393        if self.warm_recycled_pool_len(key) >= limit {
3394            return;
3395        }
3396
3397        self.remember_recycled_node_prototype(key, shell.as_ref());
3398        let stable_id = self.next_stable_id;
3399        self.next_stable_id = self.next_stable_id.saturating_add(1);
3400        self.push_warm_recycled_node(
3401            key,
3402            recycle_pool_limit,
3403            RecycledNode::from_shell(stable_id, shell, true),
3404        );
3405    }
3406
3407    fn take_recycled_node_from_pool(
3408        pools: &mut HashMap<TypeId, Vec<RecycledNode>>,
3409        key: TypeId,
3410    ) -> Option<RecycledNode> {
3411        let pool = pools.get_mut(&key)?;
3412        let node = pool.pop();
3413        if pool.is_empty() {
3414            pools.remove(&key);
3415        }
3416        node
3417    }
3418
3419    fn compact_idle_warm_pool(&mut self, key: TypeId) {
3420        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3421            return;
3422        };
3423        if pool.capacity() <= pool.len().saturating_mul(4).max(64) {
3424            return;
3425        }
3426
3427        let retained = pool.len();
3428        let mut compacted = Vec::with_capacity(retained);
3429        compacted.append(pool);
3430        let remove_pool_entry = compacted.is_empty();
3431        *pool = compacted;
3432        let _ = pool;
3433
3434        if remove_pool_entry {
3435            self.recycled_nodes.remove(&key);
3436        }
3437    }
3438
3439    fn trim_idle_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3440        let pool_len = self.warm_recycled_pool_len(key);
3441        if pool_len <= target {
3442            return;
3443        }
3444
3445        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3446            return;
3447        };
3448        let removable = (pool_len - target).min(pool.len());
3449        let dropped: Vec<_> = pool.drain(0..removable).collect();
3450        let remove_pool_entry = pool.is_empty();
3451        let _ = pool;
3452
3453        if remove_pool_entry {
3454            self.recycled_nodes.remove(&key);
3455        }
3456        drop(dropped);
3457    }
3458
3459    fn replenish_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3460        let missing = target.saturating_sub(self.warm_recycled_pool_len(key));
3461        if missing == 0 {
3462            return;
3463        }
3464
3465        let recycle_pool_limit = self.recycle_pool_limit_for(key);
3466        let mut shells = Vec::with_capacity(missing);
3467        if let Some(prototype) = self.recycled_node_prototypes.get(&key) {
3468            for _ in 0..missing {
3469                let Some(shell) = prototype.rehouse_for_recycle() else {
3470                    break;
3471                };
3472                shells.push(shell);
3473            }
3474        }
3475
3476        for shell in shells {
3477            self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3478        }
3479    }
3480
3481    fn prune_stable_generations(&mut self) {
3482        let retained_len = self.stable_to_physical.len() + self.total_recycled_node_count();
3483        if retained_len == self.stable_generations.len() {
3484            return;
3485        }
3486
3487        let mut retained = HashMap::default();
3488        retained.reserve(retained_len);
3489        for stable_id in self.stable_to_physical.keys().copied() {
3490            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3491                retained.insert(stable_id, generation);
3492            }
3493        }
3494        for stable_id in self
3495            .recycled_nodes
3496            .values()
3497            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3498        {
3499            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3500                retained.insert(stable_id, generation);
3501            }
3502        }
3503        for stable_id in self
3504            .returning_recycled_nodes
3505            .values()
3506            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3507        {
3508            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3509                retained.insert(stable_id, generation);
3510            }
3511        }
3512        for stable_id in self
3513            .cold_recycled_nodes
3514            .values()
3515            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3516        {
3517            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3518                retained.insert(stable_id, generation);
3519            }
3520        }
3521        self.stable_generations = retained;
3522    }
3523
3524    pub fn dump_tree(&self, root: Option<NodeId>) -> String {
3525        let mut output = String::new();
3526        if let Some(root_id) = root {
3527            self.dump_node(&mut output, root_id, 0);
3528        } else {
3529            output.push_str("(no root)\n");
3530        }
3531        output
3532    }
3533
3534    fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
3535        let indent = "  ".repeat(depth);
3536        if let Some(physical_id) = self.resolve_node_index(id) {
3537            if let Some(node) = self.nodes.get(physical_id).and_then(Option::as_ref) {
3538                let type_name = std::any::type_name_of_val(&**node);
3539                output.push_str(&format!("{}[{}] {}\n", indent, id, type_name));
3540
3541                let children = node.children();
3542                for child_id in children {
3543                    self.dump_node(output, child_id, depth + 1);
3544                }
3545            } else {
3546                output.push_str(&format!(
3547                    "{}[{}] (missing physical node {})\n",
3548                    indent, id, physical_id
3549                ));
3550            }
3551        } else {
3552            output.push_str(&format!("{}[{}] (missing)\n", indent, id));
3553        }
3554    }
3555
3556    fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
3557        self.stable_to_physical.get(&id).copied()
3558    }
3559
3560    fn contains_node_id(&self, id: NodeId) -> bool {
3561        self.resolve_node_index(id).is_some() || self.high_id_nodes.contains_key(&id)
3562    }
3563
3564    fn insert_high_id_node(&mut self, stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) {
3565        self.high_id_nodes.insert(stable_id, node);
3566        self.high_id_warm_recycled_origins
3567            .insert(stable_id, warm_origin);
3568        self.high_id_generations.entry(stable_id).or_insert(0);
3569    }
3570
3571    fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
3572        if stable_id >= Self::HIGH_ID_THRESHOLD {
3573            self.insert_high_id_node(stable_id, node, false);
3574            return;
3575        }
3576
3577        let physical_id = if let Some(Reverse(free_physical_id)) = self.free_ids.pop() {
3578            self.nodes[free_physical_id] = Some(node);
3579            self.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
3580            self.physical_warm_recycled_origins[free_physical_id] = false;
3581            free_physical_id
3582        } else {
3583            self.ensure_dense_node_storage_capacity();
3584            let physical_id = self.nodes.len();
3585            self.nodes.push(Some(node));
3586            self.physical_stable_ids
3587                .push(Self::pack_stable_id(stable_id));
3588            self.physical_warm_recycled_origins.push(false);
3589            physical_id
3590        };
3591
3592        self.next_stable_id = self.next_stable_id.max(stable_id.saturating_add(1));
3593        self.ensure_stable_index_capacity();
3594        self.stable_generations.entry(stable_id).or_insert(0);
3595        self.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
3596        self.stable_to_physical.insert(stable_id, physical_id);
3597    }
3598
3599    fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
3600        if let Some(physical_id) = self.resolve_node_index(id) {
3601            let slot = self
3602                .nodes
3603                .get(physical_id)
3604                .ok_or(NodeError::Missing { id })?
3605                .as_deref()
3606                .ok_or(NodeError::Missing { id })?;
3607            return Ok(slot);
3608        }
3609
3610        self.high_id_nodes
3611            .get(&id)
3612            .map(|node| node.as_ref())
3613            .ok_or(NodeError::Missing { id })
3614    }
3615
3616    fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
3617        Ok(self.get_ref(id)?.parent())
3618    }
3619
3620    fn collect_owned_children(
3621        &self,
3622        node_id: NodeId,
3623        out: &mut SmallVec<[NodeId; 8]>,
3624    ) -> Result<(), NodeError> {
3625        self.get_ref(node_id)?.collect_owned_children_into(out);
3626        out.retain(|child_id| {
3627            self.node_parent(*child_id)
3628                .map(|parent| parent == Some(node_id))
3629                .unwrap_or(false)
3630        });
3631        Ok(())
3632    }
3633
3634    fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
3635        self.virtual_node_ids.remove(&node_id);
3636        if self.high_id_nodes.contains_key(&node_id) {
3637            if let Some(mut node) = self.high_id_nodes.remove(&node_id)
3638                && let Some(key) = node.recycle_key()
3639            {
3640                let recycle_pool_limit = node.recycle_pool_limit();
3641                let warm_origin = self
3642                    .high_id_warm_recycled_origins
3643                    .remove(&node_id)
3644                    .unwrap_or(false);
3645                node.prepare_for_recycle();
3646                self.push_recycled_node(
3647                    key,
3648                    recycle_pool_limit,
3649                    RecycledNode::new(node_id, node, warm_origin),
3650                );
3651            }
3652            let generation = self.high_id_generations.entry(node_id).or_insert(0);
3653            *generation = generation.wrapping_add(1);
3654            return Ok(());
3655        }
3656
3657        let physical_id = self
3658            .resolve_node_index(node_id)
3659            .ok_or(NodeError::Missing { id: node_id })?;
3660        if let Some(mut node) = self.nodes[physical_id].take()
3661            && let Some(key) = node.recycle_key()
3662        {
3663            let recycle_pool_limit = node.recycle_pool_limit();
3664            let warm_origin = self
3665                .physical_warm_recycled_origins
3666                .get_mut(physical_id)
3667                .map(std::mem::take)
3668                .unwrap_or(false);
3669            node.prepare_for_recycle();
3670            self.push_recycled_node(
3671                key,
3672                recycle_pool_limit,
3673                RecycledNode::new(node_id, node, warm_origin),
3674            );
3675        }
3676        self.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
3677        self.stable_to_physical.remove(&node_id);
3678        if let Some(generation) = self.stable_generations.get_mut(&node_id) {
3679            *generation = generation.wrapping_add(1);
3680        } else {
3681            self.stable_generations.insert(node_id, 1);
3682        }
3683        self.free_ids.push(Reverse(physical_id));
3684        Ok(())
3685    }
3686
3687    fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
3688        self.get_ref(id)?;
3689
3690        let mut root_children = SmallVec::<[NodeId; 8]>::new();
3691        self.collect_owned_children(id, &mut root_children)?;
3692
3693        let mut stack = Vec::new();
3694        stack.push(RemovalFrame {
3695            node_id: id,
3696            children: root_children,
3697            next_child: 0,
3698        });
3699        let mut max_depth = stack.len();
3700
3701        while let Some(frame) = stack.last_mut() {
3702            if frame.next_child < frame.children.len() {
3703                let child_id = frame.children[frame.next_child];
3704                frame.next_child += 1;
3705
3706                if let Ok(child) = self.get_mut(child_id) {
3707                    child.on_removed_from_parent();
3708                    child.unmount();
3709                }
3710
3711                let mut child_children = SmallVec::<[NodeId; 8]>::new();
3712                self.collect_owned_children(child_id, &mut child_children)?;
3713                stack.push(RemovalFrame {
3714                    node_id: child_id,
3715                    children: child_children,
3716                    next_child: 0,
3717                });
3718                max_depth = max_depth.max(stack.len());
3719                continue;
3720            }
3721
3722            let node_id = frame.node_id;
3723            stack.pop();
3724            self.remove_node_storage(node_id)?;
3725        }
3726
3727        Ok(max_depth)
3728    }
3729
3730    #[cfg(test)]
3731    fn debug_remove_max_traversal_depth(&mut self, id: NodeId) -> Result<usize, NodeError> {
3732        self.remove_subtree_postorder(id)
3733    }
3734}
3735
3736impl Applier for MemoryApplier {
3737    fn record_structural_change(&mut self, parent_id: NodeId) {
3738        if self.structural_change_parents.last() != Some(&parent_id) {
3739            self.structural_change_parents.push(parent_id);
3740        }
3741    }
3742
3743    fn create(&mut self, node: Box<dyn Node>) -> NodeId {
3744        let stable_id = self.next_stable_id;
3745        self.next_stable_id = self.next_stable_id.saturating_add(1);
3746        if stable_id >= Self::HIGH_ID_THRESHOLD {
3747            self.insert_high_id_node(stable_id, node, false);
3748            return stable_id;
3749        }
3750
3751        self.ensure_stable_index_capacity();
3752        self.stable_generations.insert(stable_id, 0);
3753
3754        let physical_id = if let Some(Reverse(id)) = self.free_ids.pop() {
3755            debug_assert!(self.nodes[id].is_none(), "freelist entry {id} is not None");
3756            self.nodes[id] = Some(node);
3757            self.physical_stable_ids[id] = Self::pack_stable_id(stable_id);
3758            self.physical_warm_recycled_origins[id] = false;
3759            id
3760        } else {
3761            self.ensure_dense_node_storage_capacity();
3762            let id = self.nodes.len();
3763            self.nodes.push(Some(node));
3764            self.physical_stable_ids
3765                .push(Self::pack_stable_id(stable_id));
3766            self.physical_warm_recycled_origins.push(false);
3767            id
3768        };
3769        self.stable_to_physical.insert(stable_id, physical_id);
3770        stable_id
3771    }
3772
3773    fn node_generation(&self, id: NodeId) -> u32 {
3774        self.high_id_generations
3775            .get(&id)
3776            .copied()
3777            .or_else(|| self.stable_generations.get(&id).copied())
3778            .unwrap_or(0)
3779    }
3780
3781    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
3782        if let Some(physical_id) = self.resolve_node_index(id) {
3783            let slot = self.nodes[physical_id]
3784                .as_deref_mut()
3785                .ok_or(NodeError::Missing { id })?;
3786            return Ok(slot);
3787        }
3788        self.high_id_nodes
3789            .get_mut(&id)
3790            .map(|n| n.as_mut())
3791            .ok_or(NodeError::Missing { id })
3792    }
3793
3794    fn remove(&mut self, id: NodeId) -> Result<(), NodeError> {
3795        self.remove_subtree_postorder(id).map(|_| ())
3796    }
3797
3798    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError> {
3799        if self.contains_node_id(id) {
3800            return Err(NodeError::AlreadyExists { id });
3801        }
3802        self.insert_available_with_id(id, node);
3803        self.virtual_node_ids.insert(id);
3804        Ok(())
3805    }
3806
3807    fn insert_recycled_node_or_create(
3808        &mut self,
3809        stable_id: NodeId,
3810        node: Box<dyn Node>,
3811    ) -> RecycledNodeInsertion {
3812        if self.contains_node_id(stable_id) {
3813            let id = self.create(node);
3814            return RecycledNodeInsertion::fresh(
3815                id,
3816                Some(NodeError::AlreadyExists { id: stable_id }),
3817            );
3818        }
3819
3820        self.insert_available_with_id(stable_id, node);
3821        RecycledNodeInsertion::reused(stable_id)
3822    }
3823
3824    fn compact(&mut self) {
3825        let live_count = self.nodes.iter().filter(|slot| slot.is_some()).count();
3826        let tombstone_count = self.nodes.len().saturating_sub(live_count);
3827        if tombstone_count == 0 {
3828            return;
3829        }
3830        if self.nodes.len() > Self::EAGER_COMPACT_NODE_LEN && tombstone_count < live_count {
3831            return;
3832        }
3833        let rehouse_live_nodes = tombstone_count >= live_count;
3834        let mut packed_nodes = Vec::with_capacity(live_count);
3835        let mut packed_physical_stable_ids = Vec::with_capacity(live_count);
3836        let mut packed_warm_recycled_origins = Vec::with_capacity(live_count);
3837        let mut stable_to_physical = HashMap::default();
3838        stable_to_physical.reserve(live_count);
3839
3840        for physical_id in 0..self.nodes.len() {
3841            let Some(mut node) = self.nodes[physical_id].take() else {
3842                continue;
3843            };
3844            if rehouse_live_nodes && let Some(rehoused) = node.rehouse_for_live_compaction() {
3845                node = rehoused;
3846            }
3847            let stable_id = std::mem::replace(
3848                &mut self.physical_stable_ids[physical_id],
3849                Self::INVALID_STABLE_ID,
3850            );
3851            debug_assert_ne!(
3852                stable_id,
3853                Self::INVALID_STABLE_ID,
3854                "live physical slot must have a stable id",
3855            );
3856            let stable_id = Self::unpack_stable_id(stable_id);
3857            packed_nodes.push(Some(node));
3858            packed_physical_stable_ids.push(Self::pack_stable_id(stable_id));
3859            packed_warm_recycled_origins.push(self.physical_warm_recycled_origins[physical_id]);
3860            stable_to_physical.insert(stable_id, packed_nodes.len() - 1);
3861        }
3862
3863        self.nodes = packed_nodes;
3864        self.physical_stable_ids = packed_physical_stable_ids;
3865        self.physical_warm_recycled_origins = packed_warm_recycled_origins;
3866        self.free_ids = BinaryHeap::new();
3867        self.stable_to_physical = stable_to_physical;
3868        self.prune_stable_generations();
3869    }
3870
3871    fn take_recycled_node(&mut self, key: TypeId) -> Option<RecycledNode> {
3872        Self::take_recycled_node_from_pool(&mut self.returning_recycled_nodes, key)
3873            .or_else(|| Self::take_recycled_node_from_pool(&mut self.recycled_nodes, key))
3874    }
3875
3876    fn set_recycled_node_origin(&mut self, id: NodeId, warm_origin: bool) {
3877        if let Some(physical_id) = self.resolve_node_index(id) {
3878            self.physical_warm_recycled_origins[physical_id] = warm_origin;
3879        } else if self.high_id_nodes.contains_key(&id) {
3880            self.high_id_warm_recycled_origins.insert(id, warm_origin);
3881        }
3882    }
3883
3884    fn seed_recycled_node_shell(
3885        &mut self,
3886        key: TypeId,
3887        recycle_pool_limit: Option<usize>,
3888        shell: Box<dyn Node>,
3889    ) {
3890        self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3891    }
3892
3893    fn record_fresh_recyclable_creation(&mut self, key: TypeId) {
3894        *self.fresh_recyclable_creations.entry(key).or_insert(0) += 1;
3895    }
3896
3897    fn clear_recycled_nodes(&mut self) {
3898        let returning = std::mem::take(&mut self.returning_recycled_nodes);
3899        for (key, mut nodes) in returning {
3900            let pool = self.recycled_nodes.entry(key).or_default();
3901            pool.append(&mut nodes);
3902        }
3903
3904        let fresh_recyclable_creations = std::mem::take(&mut self.fresh_recyclable_creations);
3905        let cold = std::mem::take(&mut self.cold_recycled_nodes);
3906        for (key, mut nodes) in cold {
3907            let needed = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
3908            if needed > 0 {
3909                let remaining_limit = self
3910                    .recycle_pool_limit_for(key)
3911                    .unwrap_or(usize::MAX)
3912                    .saturating_sub(self.warm_recycled_pool_len(key));
3913                let promote = nodes.len().min(needed).min(remaining_limit);
3914                let split_at = nodes.len().saturating_sub(promote);
3915                let promoted = nodes.split_off(split_at);
3916                for mut recycled in promoted {
3917                    recycled.set_warm_origin(true);
3918                    self.recycled_nodes.entry(key).or_default().push(recycled);
3919                }
3920            }
3921        }
3922
3923        let mut keys: HashSet<TypeId> = HashSet::default();
3924        keys.extend(self.recycled_nodes.keys().copied());
3925        keys.extend(self.recycled_node_limits.keys().copied());
3926        keys.extend(self.warm_recycled_node_targets.keys().copied());
3927        keys.extend(self.recycled_node_prototypes.keys().copied());
3928        for key in keys {
3929            let observed_demand = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
3930            let target = self.update_warm_recycled_node_target(key, observed_demand);
3931            self.replenish_warm_pool_to_target(key, target);
3932            self.trim_idle_warm_pool_to_target(key, target);
3933            self.compact_idle_warm_pool(key);
3934        }
3935        self.prune_stable_generations();
3936        self.compact();
3937    }
3938}
3939
3940pub trait ApplierHost {
3941    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier>;
3942    /// Compact internal storage after commands have been applied.
3943    fn compact(&self) {}
3944}
3945
3946pub struct ConcreteApplierHost<A: Applier + 'static> {
3947    inner: RefCell<A>,
3948}
3949
3950impl<A: Applier + 'static> ConcreteApplierHost<A> {
3951    pub fn new(applier: A) -> Self {
3952        Self {
3953            inner: RefCell::new(applier),
3954        }
3955    }
3956
3957    pub fn borrow_typed(&self) -> RefMut<'_, A> {
3958        self.inner.borrow_mut()
3959    }
3960
3961    pub fn try_borrow_typed(&self) -> Result<RefMut<'_, A>, std::cell::BorrowMutError> {
3962        self.inner.try_borrow_mut()
3963    }
3964
3965    pub fn into_inner(self) -> A {
3966        self.inner.into_inner()
3967    }
3968}
3969
3970impl<A: Applier + 'static> ApplierHost for ConcreteApplierHost<A> {
3971    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier> {
3972        RefMut::map(self.inner.borrow_mut(), |applier| {
3973            applier as &mut dyn Applier
3974        })
3975    }
3976
3977    fn compact(&self) {
3978        self.inner.borrow_mut().compact();
3979    }
3980}
3981
3982pub struct ApplierGuard<'a, A: Applier + 'static> {
3983    inner: RefMut<'a, A>,
3984}
3985
3986impl<'a, A: Applier + 'static> ApplierGuard<'a, A> {
3987    fn new(inner: RefMut<'a, A>) -> Self {
3988        Self { inner }
3989    }
3990}
3991
3992impl<'a, A: Applier + 'static> Deref for ApplierGuard<'a, A> {
3993    type Target = A;
3994
3995    fn deref(&self) -> &Self::Target {
3996        &self.inner
3997    }
3998}
3999
4000impl<'a, A: Applier + 'static> DerefMut for ApplierGuard<'a, A> {
4001    fn deref_mut(&mut self) -> &mut Self::Target {
4002        &mut self.inner
4003    }
4004}
4005
4006pub struct SlotsHost {
4007    storage_key: Cell<usize>,
4008    inner: RefCell<SlotsHostInner>,
4009}
4010
4011#[derive(Debug, Default)]
4012pub(crate) struct SlotPassOutcome {
4013    pub(crate) compacted: bool,
4014    pub(crate) compact_anchor_registry_storage: bool,
4015    pub(crate) compact_payload_storage: bool,
4016}
4017
4018#[derive(Default)]
4019pub(crate) struct FinishedSlotPass {
4020    pub(crate) outcome: SlotPassOutcome,
4021    pub(crate) detached_root_children: Vec<slot::DetachedSubtree>,
4022}
4023
4024struct ActivePassState {
4025    state: slot::SlotWriteSessionState,
4026}
4027
4028struct SlotsHostInner {
4029    table: SlotTable,
4030    nested_hosts: Vec<std::rc::Weak<SlotsHost>>,
4031    lifecycle: slot::SlotLifecycleCoordinator,
4032    runtime_state: Option<Rc<crate::composer::ComposerRuntimeState>>,
4033    active_pass: Option<ActivePassState>,
4034}
4035
4036impl Drop for SlotsHost {
4037    fn drop(&mut self) {
4038        let storage_key = self.storage_key.get();
4039        let inner = self.inner.get_mut();
4040        if let Some(state) = inner.runtime_state.clone() {
4041            if let Err(err) = state.dispose_retained_subtrees_for_host(
4042                storage_key,
4043                &mut inner.table,
4044                &mut inner.lifecycle,
4045            ) {
4046                log::error!(
4047                    "retained subtree disposal failed while dropping SlotsHost {storage_key}: {err}"
4048                );
4049                state.abandon_retained_subtrees_for_host(
4050                    storage_key,
4051                    &mut inner.table,
4052                    &mut inner.lifecycle,
4053                );
4054            } else {
4055                state.clear_host_storage_key(storage_key);
4056            }
4057        }
4058        inner.lifecycle.dispose_slot_table(&mut inner.table);
4059    }
4060}
4061
4062impl SlotsHost {
4063    pub fn storage_key(&self) -> usize {
4064        self.storage_key.get()
4065    }
4066
4067    pub fn new(storage: SlotTable) -> Self {
4068        let storage_key = storage.storage_id();
4069        Self {
4070            storage_key: Cell::new(storage_key),
4071            inner: RefCell::new(SlotsHostInner {
4072                table: storage,
4073                nested_hosts: Vec::new(),
4074                lifecycle: slot::SlotLifecycleCoordinator::default(),
4075                runtime_state: None,
4076                active_pass: None,
4077            }),
4078        }
4079    }
4080
4081    pub fn note_nested_host(&self, nested: &Rc<SlotsHost>) {
4082        let Ok(mut inner) = self.inner.try_borrow_mut() else {
4083            return;
4084        };
4085        inner.nested_hosts.retain(|held| held.upgrade().is_some());
4086        if inner
4087            .nested_hosts
4088            .iter()
4089            .any(|held| held.upgrade().is_some_and(|host| Rc::ptr_eq(&host, nested)))
4090        {
4091            return;
4092        }
4093        inner.nested_hosts.push(Rc::downgrade(nested));
4094    }
4095
4096    pub(crate) fn forget_effects(&self) -> bool {
4097        let (forgotten, nested, runtime_state) = {
4098            let Ok(mut inner) = self.inner.try_borrow_mut() else {
4099                return false;
4100            };
4101            if inner.active_pass.is_some() {
4102                return false;
4103            }
4104            let drops = inner.table.take_effect_drops();
4105            inner.nested_hosts.retain(|held| held.upgrade().is_some());
4106            let nested: Vec<Rc<SlotsHost>> = inner
4107                .nested_hosts
4108                .iter()
4109                .filter_map(std::rc::Weak::upgrade)
4110                .collect();
4111            (drops, nested, inner.runtime_state.clone())
4112        };
4113        let mut any = !forgotten.is_empty();
4114        drop(forgotten);
4115        for host in nested {
4116            any |= host.forget_effects();
4117        }
4118        if any && let Some(runtime_state) = runtime_state {
4119            runtime_state.force_recompose_host_scopes(self.storage_key());
4120        }
4121        any
4122    }
4123
4124    pub(crate) fn bind_runtime_state(&self, state: &Rc<crate::composer::ComposerRuntimeState>) {
4125        let mut inner = self.inner.borrow_mut();
4126        inner.runtime_state = Some(Rc::clone(state));
4127    }
4128
4129    pub(crate) fn rebind_orphaned_runtime_state(
4130        &self,
4131        state: &Rc<crate::composer::ComposerRuntimeState>,
4132    ) -> bool {
4133        let inner = self.inner.borrow();
4134        if inner.active_pass.is_some() {
4135            log::error!("cannot rebind SlotsHost during an active pass");
4136            return false;
4137        }
4138        let Some(bound_state) = inner.runtime_state.as_ref() else {
4139            drop(inner);
4140            self.bind_runtime_state(state);
4141            return true;
4142        };
4143        if Rc::ptr_eq(bound_state, state) {
4144            return true;
4145        }
4146        if bound_state.has_live_applier_host() {
4147            return false;
4148        }
4149        drop(inner);
4150
4151        let mut inner = self.inner.borrow_mut();
4152        let Some(bound_state) = inner.runtime_state.as_ref() else {
4153            inner.runtime_state = Some(Rc::clone(state));
4154            return true;
4155        };
4156        if Rc::ptr_eq(bound_state, state) {
4157            return true;
4158        }
4159        if bound_state.has_live_applier_host() {
4160            return false;
4161        }
4162
4163        let previous_state = Rc::clone(bound_state);
4164        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4165        lifecycle.flush_pending_drops();
4166        let host_key = self.storage_key();
4167        if previous_state
4168            .dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
4169            .is_err()
4170        {
4171            inner.lifecycle = lifecycle;
4172            return false;
4173        }
4174        previous_state.clear_host(self);
4175        lifecycle.flush_pending_drops();
4176        inner.runtime_state = Some(Rc::clone(state));
4177        inner.lifecycle = lifecycle;
4178        true
4179    }
4180
4181    pub(crate) fn runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
4182        self.inner.borrow().runtime_state.clone()
4183    }
4184
4185    pub(crate) fn borrow(&self) -> Ref<'_, SlotTable> {
4186        Ref::map(self.inner.borrow(), |inner| &inner.table)
4187    }
4188
4189    pub(crate) fn borrow_mut(&self) -> RefMut<'_, SlotTable> {
4190        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.table)
4191    }
4192
4193    pub fn into_table(self: Rc<Self>) -> Result<SlotTable, NodeError> {
4194        if Rc::strong_count(&self) != 1 {
4195            return Err(NodeError::SlotHostUnavailable {
4196                operation: "SlotsHost::into_table",
4197                reason: "other host references are alive",
4198            });
4199        }
4200        self.take_table_for_transfer()
4201    }
4202
4203    fn take_table_for_transfer(&self) -> Result<SlotTable, NodeError> {
4204        let inner = self.inner.borrow();
4205        if inner.active_pass.is_some() {
4206            return Err(NodeError::SlotHostUnavailable {
4207                operation: "SlotsHost::into_table",
4208                reason: "slot pass is active",
4209            });
4210        }
4211        drop(inner);
4212        let mut inner = self.inner.borrow_mut();
4213        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4214        lifecycle.flush_pending_drops();
4215        if let Some(state) = inner.runtime_state.clone() {
4216            let host_key = self.storage_key();
4217            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4218            state.clear_host(self);
4219            lifecycle.flush_pending_drops();
4220        }
4221        let taken = std::mem::take(&mut inner.table);
4222        self.storage_key.set(inner.table.storage_id());
4223        inner.runtime_state = None;
4224        inner.lifecycle = lifecycle;
4225        Ok(taken)
4226    }
4227
4228    pub fn reset(&self) -> Result<(), NodeError> {
4229        let inner = self.inner.borrow();
4230        if inner.active_pass.is_some() {
4231            return Err(NodeError::SlotHostUnavailable {
4232                operation: "SlotsHost::reset",
4233                reason: "slot pass is active",
4234            });
4235        }
4236        let runtime_state = inner.runtime_state.clone();
4237        drop(inner);
4238        let mut inner = self.inner.borrow_mut();
4239        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4240        if let Some(state) = runtime_state {
4241            let host_key = self.storage_key();
4242            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4243            state.clear_host(self);
4244        }
4245        lifecycle.dispose_slot_table(&mut inner.table);
4246        inner.table = SlotTable::default();
4247        self.storage_key.set(inner.table.storage_id());
4248        inner.runtime_state = None;
4249        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4250        Ok(())
4251    }
4252
4253    pub(crate) fn abandon_after_apply_failure(&self) {
4254        let inner = self.inner.borrow();
4255        if inner.active_pass.is_some() {
4256            log::error!("cannot abandon SlotsHost during an active pass");
4257            return;
4258        }
4259        let runtime_state = inner.runtime_state.clone();
4260        drop(inner);
4261        let mut inner = self.inner.borrow_mut();
4262        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4263        if let Some(state) = runtime_state {
4264            let host_key = self.storage_key();
4265            state.abandon_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle);
4266        }
4267        lifecycle.dispose_slot_table(&mut inner.table);
4268        inner.table = SlotTable::default();
4269        self.storage_key.set(inner.table.storage_id());
4270        inner.runtime_state = None;
4271        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4272    }
4273
4274    pub(crate) fn debug_stats(&self) -> SlotTableDebugStats {
4275        let inner = self.inner.borrow();
4276        let local = inner.table.debug_stats();
4277        let lifecycle = inner.lifecycle.debug_stats();
4278        let retention = inner
4279            .runtime_state
4280            .clone()
4281            .map(|state| state.slot_retention_debug_stats(self))
4282            .unwrap_or_default();
4283        SlotTableDebugStats::from_parts(local, lifecycle, retention)
4284    }
4285
4286    pub(crate) fn debug_snapshot(&self) -> slot::SlotDebugSnapshot {
4287        let inner = self.inner.borrow();
4288        let mut snapshot = inner.table.debug_snapshot();
4289        if let Some(state) = inner.runtime_state.clone() {
4290            state.fill_slot_debug_snapshot(self, &mut snapshot);
4291        }
4292        snapshot
4293    }
4294
4295    pub(crate) fn begin_pass(&self, mode: slot::SlotPassMode) {
4296        let mut inner = self.inner.borrow_mut();
4297        if inner.active_pass.is_some() {
4298            log::error!("slot pass already active for host");
4299            return;
4300        }
4301        let mut state = slot::SlotWriteSessionState::default();
4302        state.reset_for_pass(mode);
4303        inner.active_pass = Some(ActivePassState { state });
4304    }
4305
4306    pub(crate) fn has_active_pass(&self) -> bool {
4307        self.inner.borrow().active_pass.is_some()
4308    }
4309
4310    pub(crate) fn try_push_branch_fold(&self, key: Key) -> Option<usize> {
4311        let mut inner = self.inner.try_borrow_mut().ok()?;
4312        let pass = inner.active_pass.as_mut()?;
4313        Some(pass.state.push_branch_fold(key))
4314    }
4315
4316    pub(crate) fn try_close_branch_fold(&self, token: usize) -> bool {
4317        let Ok(mut inner) = self.inner.try_borrow_mut() else {
4318            return false;
4319        };
4320        let Some(pass) = inner.active_pass.as_mut() else {
4321            return false;
4322        };
4323        pass.state.close_branch_fold(token);
4324        true
4325    }
4326
4327    pub(crate) fn abandon_active_pass(&self) {
4328        self.inner.borrow_mut().active_pass = None;
4329    }
4330
4331    pub(crate) fn with_write_session<R>(
4332        &self,
4333        f: impl FnOnce(&mut slot::SlotWriteSession<'_>) -> R,
4334    ) -> R {
4335        let mut inner = self.inner.borrow_mut();
4336        let SlotsHostInner {
4337            table,
4338            lifecycle,
4339            active_pass,
4340            ..
4341        } = &mut *inner;
4342        let active_pass = active_pass
4343            .as_mut()
4344            .expect("slot write session requires an active pass");
4345        let mut session = table.write_session(lifecycle, &mut active_pass.state);
4346        f(&mut session)
4347    }
4348
4349    pub(crate) fn with_table_and_lifecycle_mut<R>(
4350        &self,
4351        f: impl FnOnce(&mut SlotTable, &mut slot::SlotLifecycleCoordinator) -> R,
4352    ) -> R {
4353        let mut inner = self.inner.borrow_mut();
4354        let SlotsHostInner {
4355            table, lifecycle, ..
4356        } = &mut *inner;
4357        f(table, lifecycle)
4358    }
4359
4360    pub(crate) fn finish_pass(
4361        &self,
4362        applier: &mut dyn Applier,
4363    ) -> Result<FinishedSlotPass, NodeError> {
4364        let mut inner = self.inner.borrow_mut();
4365        let SlotsHostInner {
4366            table,
4367            lifecycle,
4368            active_pass: active_pass_slot,
4369            ..
4370        } = &mut *inner;
4371        let Some(mut active_pass) = active_pass_slot.take() else {
4372            return Ok(FinishedSlotPass::default());
4373        };
4374
4375        active_pass.state.flush_payload_location_refreshes(table);
4376
4377        #[cfg(debug_assertions)]
4378        if let Err(err) = active_pass.state.validate(table) {
4379            log::error!("slot writer invariant violation before finalize_pass: {err:?}");
4380            return Err(NodeError::SlotHostUnavailable {
4381                operation: "SlotsHost::finish_pass",
4382                reason: "slot writer invariant violation",
4383            });
4384        }
4385
4386        let detached_root_children = {
4387            let mut session = table.write_session(lifecycle, &mut active_pass.state);
4388            session.finalize_pass(applier)?
4389        };
4390
4391        Ok(FinishedSlotPass {
4392            outcome: SlotPassOutcome {
4393                compacted: active_pass.state.request_compaction,
4394                compact_anchor_registry_storage: active_pass
4395                    .state
4396                    .request_anchor_storage_compaction,
4397                compact_payload_storage: active_pass.state.request_payload_storage_compaction,
4398            },
4399            detached_root_children,
4400        })
4401    }
4402
4403    pub(crate) fn complete_pass_cleanup(&self, outcome: &SlotPassOutcome) {
4404        let mut inner = self.inner.borrow_mut();
4405        let SlotsHostInner {
4406            table,
4407            lifecycle,
4408            runtime_state,
4409            ..
4410        } = &mut *inner;
4411        lifecycle.flush_pending_drops();
4412        if outcome.compacted {
4413            table.compact_storage();
4414            lifecycle.compact_storage();
4415        }
4416        if let Some(state) = runtime_state.clone() {
4417            state.compact_table_identity_storage_for_host(
4418                self,
4419                table,
4420                outcome.compact_anchor_registry_storage,
4421                outcome.compact_payload_storage,
4422            );
4423        } else {
4424            if outcome.compact_anchor_registry_storage {
4425                table.compact_anchor_registry_storage(None);
4426            }
4427            if outcome.compact_payload_storage {
4428                table.compact_payload_anchor_registry_storage(None);
4429            }
4430        }
4431        table.assert_fast_integrity("slot pass cleanup");
4432        #[cfg(any(test, debug_assertions))]
4433        {
4434            table.debug_verify();
4435            if let Some(state) = runtime_state.clone() {
4436                state.debug_verify_host(self, table);
4437            }
4438        }
4439    }
4440}
4441
4442fn build_child_positions(children: &[NodeId]) -> HashMap<NodeId, usize> {
4443    let mut positions = HashMap::default();
4444    positions.reserve(children.len());
4445    for (index, &child) in children.iter().enumerate() {
4446        positions.insert(child, index);
4447    }
4448    positions
4449}
4450
4451fn refresh_child_positions(
4452    current: &[NodeId],
4453    positions: &mut HashMap<NodeId, usize>,
4454    start: usize,
4455    end: usize,
4456) {
4457    if current.is_empty() || start >= current.len() {
4458        return;
4459    }
4460    let end = end.min(current.len() - 1);
4461    for (offset, &child) in current[start..=end].iter().enumerate() {
4462        positions.insert(child, start + offset);
4463    }
4464}
4465
4466fn insert_child_into_diff_state(
4467    current: &mut ChildList,
4468    positions: &mut HashMap<NodeId, usize>,
4469    index: usize,
4470    child: NodeId,
4471) {
4472    let index = index.min(current.len());
4473    current.insert(index, child);
4474    refresh_child_positions(current, positions, index, current.len() - 1);
4475}
4476
4477fn move_child_in_diff_state(
4478    current: &mut ChildList,
4479    positions: &mut HashMap<NodeId, usize>,
4480    from_index: usize,
4481    target_index: usize,
4482) -> usize {
4483    let child = current.remove(from_index);
4484    let to_index = target_index.min(current.len());
4485    current.insert(to_index, child);
4486    refresh_child_positions(
4487        current,
4488        positions,
4489        from_index.min(to_index),
4490        from_index.max(to_index),
4491    );
4492    to_index
4493}
4494
4495pub(crate) use state::MutableStateInner;
4496pub use state::{
4497    MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
4498    StateSubscriptionHold,
4499};
4500
4501fn hash_key<K: Hash>(key: &K) -> Key {
4502    let mut hasher = hash::default::new();
4503    key.hash(&mut hasher);
4504    hasher.finish()
4505}
4506
4507pub(crate) fn explicit_group_key_seed<K: Hash>(
4508    key: &K,
4509    caller: &'static std::panic::Location<'static>,
4510) -> slot::GroupKeySeed {
4511    let source_key = location_key(caller.file(), caller.line(), caller.column());
4512    let explicit_key = hash_key(key);
4513    slot::GroupKeySeed::keyed(source_key, explicit_key)
4514}
4515
4516#[cfg(test)]
4517#[path = "tests/mod.rs"]
4518mod tests;
4519
4520#[cfg(test)]
4521#[path = "tests/recursive_decrease_increase_test.rs"]
4522mod recursive_decrease_increase_test;
4523
4524pub mod collections;
4525pub mod hash;
4526
4527/// Where a test writes real files. Behind `test-helpers` so only a test build
4528/// of the workspace carries it.
4529#[cfg(any(test, feature = "test-helpers"))]
4530pub mod test_scratch;
4531#[cfg(any(test, feature = "test-helpers"))]
4532pub use test_scratch::test_scratch_dir;
4533
4534pub(crate) fn note_structural(reason: &str, parent_id: NodeId, child_id: NodeId) {
4535    if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
4536        eprintln!("[structural] {reason} parent={parent_id} child={child_id}");
4537    }
4538}
4539
4540pub(crate) fn note_structural_move(parent_id: NodeId, from_index: usize, to_index: usize) {
4541    if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
4542        eprintln!("[structural] move parent={parent_id} from={from_index} to={to_index}");
4543    }
4544}