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