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