Skip to main content

tessera_ui/
session.rs

1//! Runtime sessions with explicit resource ownership.
2//!
3//! ## Usage
4//!
5//! Own independent UI roots and release their resources when the session ends.
6
7use std::{
8    any::{Any, TypeId},
9    cell::{Cell, OnceCell, RefCell},
10    marker::PhantomData,
11    rc::{Rc, Weak},
12    sync::Arc,
13};
14
15use rustc_hash::FxHashMap as HashMap;
16
17use crate::{execution_context::ExecutionContext, runtime::RuntimeGlobals};
18
19#[derive(Default)]
20struct Resources {
21    shared: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
22    local: HashMap<TypeId, Rc<dyn Any>>,
23    order: Vec<(TypeId, bool)>,
24}
25
26pub(crate) struct SessionState {
27    identity: u128,
28    root_claimed: Cell<bool>,
29    closing: Cell<bool>,
30    density: Cell<f64>,
31    execution_context: OnceCell<RefCell<ExecutionContext>>,
32    runtime_globals: OnceCell<Box<RuntimeGlobals>>,
33    resources: RefCell<Resources>,
34    previous: RefCell<SessionSelection>,
35}
36
37#[derive(Clone, Default)]
38struct SessionSelection {
39    state: Weak<SessionState>,
40    identity: u128,
41}
42
43impl SessionSelection {
44    fn new(state: &Rc<SessionState>) -> Self {
45        Self {
46            state: Rc::downgrade(state),
47            identity: state.identity,
48        }
49    }
50
51    fn selects(&self, state: &Rc<SessionState>) -> bool {
52        self.state.as_ptr() == Rc::as_ptr(state)
53    }
54}
55
56thread_local! {
57    static ACTIVE_SESSION: RefCell<SessionSelection> = const { RefCell::new(SessionSelection {
58        state: Weak::new(),
59        identity: 0,
60    }) };
61}
62
63fn active_session_state() -> Option<Rc<SessionState>> {
64    ACTIVE_SESSION
65        .try_with(|active| active.borrow().state.upgrade())
66        .ok()
67        .flatten()
68}
69
70/// An owner for the resources of one UI session.
71#[derive(Clone)]
72pub struct RuntimeSession(Rc<SessionState>);
73
74/// A non-owning reference used to deliver results to a live UI session.
75#[derive(Clone)]
76pub struct WeakRuntimeSession(Weak<SessionState>);
77
78impl WeakRuntimeSession {
79    /// Runs work on the owning UI thread while the session is still alive.
80    pub fn run<R>(&self, work: impl FnOnce() -> R) -> Option<R> {
81        self.0
82            .upgrade()
83            .filter(|owner| !owner.closing.get())
84            .map(|owner| RuntimeSession(owner).run(work))
85    }
86}
87
88/// The identity shared by observations from one runtime session.
89#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
90pub struct RuntimeSessionId(u128);
91
92impl Default for RuntimeSession {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl RuntimeSession {
99    /// Creates a reactive state lifetime owned by this UI session.
100    pub fn state_owner(&self) -> crate::StateOwner {
101        self.run(crate::StateOwner::new)
102    }
103
104    /// Captures result delivery to this session's UI thread.
105    pub fn dispatcher(&self) -> crate::UiDispatcher {
106        self.run(crate::dispatcher::current)
107    }
108    /// Returns a reference that does not keep this session alive.
109    pub fn downgrade(&self) -> WeakRuntimeSession {
110        WeakRuntimeSession(Rc::downgrade(&self.0))
111    }
112    /// Returns a shared resource owned by this session.
113    pub fn resource<T: Default + Send + Sync + 'static>(&self) -> Arc<T> {
114        self.run(runtime_resource::<T>)
115    }
116
117    /// Returns a resource that stays on this session's UI thread.
118    pub fn local_resource<T: Default + 'static>(&self) -> Rc<T> {
119        self.run(|| self.local_resource_in_session())
120    }
121
122    fn local_resource_in_session<T: Default + 'static>(&self) -> Rc<T> {
123        let key = TypeId::of::<T>();
124        if let Some(resource) = self.0.resources.borrow().local.get(&key).cloned() {
125            return Rc::downcast(resource).unwrap_or_else(|_| unreachable!());
126        }
127        let resource = Rc::new(T::default());
128        let stored = {
129            let mut resources = self.0.resources.borrow_mut();
130            if let Some(stored) = resources.local.get(&key) {
131                stored.clone()
132            } else {
133                resources.local.insert(key, resource.clone());
134                resources.order.push((key, false));
135                resource.clone()
136            }
137        };
138        Rc::downcast(stored).unwrap_or_else(|_| unreachable!())
139    }
140
141    /// Returns this session's identity for correlating observations.
142    pub fn identity(&self) -> RuntimeSessionId {
143        RuntimeSessionId(self.0.identity)
144    }
145    /// Creates an independent session.
146    pub fn new() -> Self {
147        Self(Rc::new(SessionState {
148            identity: uuid::Uuid::new_v4().as_u128(),
149            root_claimed: Cell::new(false),
150            closing: Cell::new(false),
151            density: Cell::new(1.0),
152            execution_context: OnceCell::new(),
153            runtime_globals: OnceCell::new(),
154            resources: RefCell::new(Resources::default()),
155            previous: RefCell::default(),
156        }))
157    }
158
159    /// Runs work with this session's resources available.
160    pub fn run<R>(&self, work: impl FnOnce() -> R) -> R {
161        let _guard = self.enter();
162        work()
163    }
164
165    /// Selects this session until the returned guard is dropped.
166    pub fn enter(&self) -> RuntimeSessionGuard {
167        let previous = ACTIVE_SESSION.with(|active| active.replace(SessionSelection::new(&self.0)));
168        RuntimeSessionGuard {
169            previous,
170            _owner: self.clone(),
171        }
172    }
173
174    pub(crate) fn activate(&self) {
175        ACTIVE_SESSION.with(|active| {
176            let mut active = active.borrow_mut();
177            if !active.selects(&self.0) {
178                *self.0.previous.borrow_mut() =
179                    std::mem::replace(&mut *active, SessionSelection::new(&self.0));
180            }
181        });
182    }
183
184    pub(crate) fn current() -> Option<Self> {
185        active_session_state().map(Self)
186    }
187
188    pub(crate) fn execution_context(&self) -> &RefCell<ExecutionContext> {
189        self.0.execution_context.get_or_init(RefCell::default)
190    }
191
192    pub(crate) fn runtime_globals(&self) -> &RuntimeGlobals {
193        self.0.runtime_globals.get_or_init(Box::default)
194    }
195
196    pub(crate) fn density(&self) -> f64 {
197        self.0.density.get()
198    }
199
200    pub(crate) fn set_density(&self, density: f64) {
201        self.0.density.set(density);
202    }
203
204    pub(crate) fn accepts_work(&self) -> bool {
205        !self.0.closing.get()
206    }
207
208    pub(crate) fn for_root() -> Self {
209        let session = Self::current()
210            .filter(|session| !session.0.root_claimed.get())
211            .unwrap_or_default();
212        session.0.root_claimed.set(true);
213        session
214    }
215}
216
217pub(crate) fn current_session_identity() -> Option<u128> {
218    ACTIVE_SESSION
219        .try_with(|active| {
220            let selected = active.borrow();
221            (selected.state.strong_count() != 0).then_some(selected.identity)
222        })
223        .ok()
224        .flatten()
225}
226
227pub(crate) fn with_active_execution_context<R>(
228    read: impl FnOnce(&RefCell<ExecutionContext>) -> R,
229) -> R {
230    let session = RuntimeSession::current().expect("a runtime session must be active");
231    read(session.execution_context())
232}
233
234pub(crate) fn with_runtime_globals<R>(read: impl FnOnce(&RuntimeGlobals) -> R) -> R {
235    try_with_runtime_globals(read).expect("a runtime session must be active")
236}
237
238pub(crate) fn try_with_runtime_globals<R>(
239    read: impl FnOnce(&RuntimeGlobals) -> R,
240) -> Result<R, ()> {
241    let session = RuntimeSession::current().ok_or(())?;
242    Ok(read(session.runtime_globals()))
243}
244
245impl Drop for RuntimeSession {
246    #[inline]
247    fn drop(&mut self) {
248        if Rc::strong_count(&self.0) == 1 {
249            self.shutdown();
250        }
251    }
252}
253
254impl RuntimeSession {
255    #[cold]
256    fn shutdown(&self) {
257        self.0.closing.set(true);
258        let previous = self.0.previous.borrow().clone();
259        let was_active = ACTIVE_SESSION
260            .try_with(|active| active.borrow().selects(&self.0))
261            .unwrap_or(false);
262        {
263            let _guard = self.enter();
264            crate::dispatcher::close();
265            crate::runtime::shutdown_runtime();
266            loop {
267                let removed = {
268                    let mut resources = self.0.resources.borrow_mut();
269                    resources.order.pop().map(|(key, shared)| {
270                        if shared {
271                            (resources.shared.remove(&key), None)
272                        } else {
273                            (None, resources.local.remove(&key))
274                        }
275                    })
276                };
277                let Some(removed) = removed else { break };
278                drop(removed);
279            }
280        }
281        if was_active {
282            let _ = ACTIVE_SESSION.try_with(|active| active.replace(previous));
283        }
284    }
285}
286
287/// Restores the previous active session when a scoped operation finishes.
288pub struct RuntimeSessionGuard {
289    previous: SessionSelection,
290    _owner: RuntimeSession,
291}
292
293pub(crate) struct SessionLocal<T>(PhantomData<fn() -> T>);
294
295impl<T: Default + 'static> SessionLocal<T> {
296    pub(crate) const fn new() -> Self {
297        Self(PhantomData)
298    }
299
300    pub(crate) fn with<R>(&self, read: impl FnOnce(&T) -> R) -> R {
301        self.try_with(read)
302            .expect("a runtime session must be active")
303    }
304
305    pub(crate) fn try_with<R>(&self, read: impl FnOnce(&T) -> R) -> Result<R, ()> {
306        let session = RuntimeSession::current().ok_or(())?;
307        let resource = session.local_resource_in_session::<T>();
308        Ok(read(&resource))
309    }
310}
311
312impl<T: Default + 'static> SessionLocal<RefCell<T>> {
313    pub(crate) fn with_borrow<R>(&self, read: impl FnOnce(&T) -> R) -> R {
314        self.with(|value| read(&value.borrow()))
315    }
316
317    pub(crate) fn with_borrow_mut<R>(&self, update: impl FnOnce(&mut T) -> R) -> R {
318        self.with(|value| update(&mut value.borrow_mut()))
319    }
320}
321
322impl<T: Copy + Default + 'static> SessionLocal<std::cell::Cell<T>> {
323    pub(crate) fn get(&self) -> T {
324        self.with(std::cell::Cell::get)
325    }
326    pub(crate) fn set(&self, value: T) {
327        self.with(|slot| slot.set(value));
328    }
329    pub(crate) fn replace(&self, value: T) -> T {
330        self.with(|slot| slot.replace(value))
331    }
332}
333
334impl Drop for RuntimeSessionGuard {
335    fn drop(&mut self) {
336        let _ =
337            ACTIVE_SESSION.try_with(|active| active.replace(std::mem::take(&mut self.previous)));
338    }
339}
340
341/// Returns a shared resource owned by the active UI session.
342///
343/// Without an active session, the returned resource is owned only by its caller.
344pub fn runtime_resource<T: Default + Send + Sync + 'static>() -> Arc<T> {
345    let Some(session) = RuntimeSession::current() else {
346        return Arc::new(T::default());
347    };
348    let key = TypeId::of::<T>();
349    if let Some(resource) = session.0.resources.borrow().shared.get(&key).cloned() {
350        return Arc::downcast(resource).unwrap_or_else(|_| unreachable!());
351    }
352    let resource = Arc::new(T::default());
353    let stored = {
354        let mut resources = session.0.resources.borrow_mut();
355        if let Some(stored) = resources.shared.get(&key) {
356            stored.clone()
357        } else {
358            resources.shared.insert(key, resource.clone());
359            resources.order.push((key, true));
360            resource.clone()
361        }
362    };
363    Arc::downcast(stored).unwrap_or_else(|_| unreachable!())
364}
365
366/// Returns a thread-local resource owned by the active UI session.
367///
368/// Use this for platform objects that must stay on their creating thread.
369/// Without an active session, the returned resource is owned only by its caller.
370pub fn local_runtime_resource<T: Default + 'static>() -> Rc<T> {
371    let Some(session) = RuntimeSession::current() else {
372        return Rc::new(T::default());
373    };
374    session.local_resource_in_session()
375}
376
377#[cfg(all(test, feature = "testing"))]
378mod tests {
379    use std::{cell::Cell, panic::AssertUnwindSafe, rc::Rc};
380
381    use crate::{
382        State, remember,
383        testing::{TestProbe, layout_test, pump_layout_test_session, start_layout_test_session},
384    };
385
386    use super::{RuntimeSession, local_runtime_resource};
387
388    #[crate::tessera(crate)]
389    fn observed_state(output: TestProbe<Option<State<usize>>>) {
390        let state = remember(|| 7usize);
391        let _value = state.get();
392        output.set(Some(state));
393    }
394
395    #[test]
396    fn independent_sessions_reject_foreign_states_and_release_their_resources() {
397        let output = TestProbe::default();
398        let mut first = start_layout_test_session(layout_test({
399            let output = output.clone();
400            move || observed_state(output.clone())
401        }));
402        pump_layout_test_session(&mut first, 0);
403        let state = output.get().unwrap();
404        let resource = first.run(local_runtime_resource::<Cell<usize>>);
405        let weak = Rc::downgrade(&resource);
406        drop(resource);
407
408        let second = RuntimeSession::new();
409        second.run(|| {
410            assert!(!state.is_alive());
411            assert!(std::panic::catch_unwind(AssertUnwindSafe(|| state.set(12))).is_err());
412            assert_eq!(local_runtime_resource::<Cell<usize>>().get(), 0);
413        });
414        first.run(|| {
415            assert_eq!(state.get(), 7);
416            state.set(9);
417        });
418        pump_layout_test_session(&mut first, 1);
419        assert_eq!(
420            first
421                .work()
422                .count("observed_state", crate::WorkPhase::Build),
423            1
424        );
425        let _selected = second.enter();
426        drop(first);
427        assert!(weak.upgrade().is_none());
428        assert_eq!(
429            RuntimeSession::current().unwrap().identity(),
430            second.identity()
431        );
432        second.run(|| assert!(!state.is_alive()));
433    }
434
435    #[test]
436    fn session_selection_restores_density_and_rejects_an_expired_owner() {
437        let first = RuntimeSession::new();
438        first.activate();
439        crate::dp::set_scale_factor(2.0);
440        let second = RuntimeSession::new();
441        second.run(|| {
442            assert_eq!(crate::dp::scale_factor(), 1.0);
443            crate::dp::set_scale_factor(3.0);
444        });
445        assert_eq!(super::current_session_identity(), Some(first.identity().0));
446        assert_eq!(crate::dp::scale_factor(), 2.0);
447
448        let scope = second.enter();
449        drop(first);
450        assert_eq!(super::current_session_identity(), Some(second.identity().0));
451        assert_eq!(crate::dp::scale_factor(), 3.0);
452        drop(scope);
453
454        assert_eq!(super::current_session_identity(), None);
455        assert_eq!(crate::dp::scale_factor(), 1.0);
456    }
457
458    #[derive(Default)]
459    struct CleanupDependency(Cell<usize>);
460
461    #[derive(Default)]
462    struct DependentResource(std::cell::RefCell<Option<TestProbe<Vec<usize>>>>);
463
464    impl Drop for DependentResource {
465        fn drop(&mut self) {
466            if let Some(output) = self.0.get_mut().take() {
467                output.with_mut(|values| {
468                    values.push(local_runtime_resource::<CleanupDependency>().0.get())
469                });
470            }
471        }
472    }
473
474    #[test]
475    fn session_resources_drop_after_dependents_with_owner_selected_and_registry_unborrowed() {
476        let session = RuntimeSession::new();
477        let output = TestProbe::default();
478        session.run(|| {
479            local_runtime_resource::<CleanupDependency>().0.set(42);
480            local_runtime_resource::<DependentResource>()
481                .0
482                .replace(Some(output.clone()));
483        });
484        drop(session);
485        assert_eq!(output.get(), [42]);
486    }
487
488    struct ObserveClockOnDrop(TestProbe<Vec<u64>>);
489
490    impl Drop for ObserveClockOnDrop {
491        fn drop(&mut self) {
492            self.0
493                .with_mut(|values| values.push(crate::current_frame_nanos()));
494        }
495    }
496
497    #[crate::tessera(crate)]
498    fn clock_probe(output: TestProbe<Vec<u64>>) {
499        let active = remember(|| true);
500        if active.get() {
501            let capture = ObserveClockOnDrop(output.clone());
502            crate::receive_frame_nanos(move |nanos| {
503                assert_eq!(crate::current_frame_nanos(), nanos);
504                let _ = &capture;
505                output.with_mut(|values| values.push(nanos));
506                active.set(false);
507                crate::FrameNanosControl::Stop
508            });
509        }
510    }
511
512    #[test]
513    fn frame_callbacks_and_stopped_captures_read_the_clock_without_borrowing_the_registry() {
514        let output = TestProbe::default();
515        let mut session = start_layout_test_session(layout_test({
516            let output = output.clone();
517            move || clock_probe(output.clone())
518        }));
519        pump_layout_test_session(&mut session, 0);
520        assert_eq!(session.frame_clock_diagnostics().pending_subscriptions, 1);
521        pump_layout_test_session(&mut session, 10);
522        assert_eq!(output.get(), [10, 10]);
523        assert_eq!(session.frame_clock_diagnostics().callbacks_executed, 1);
524        assert_eq!(session.frame_clock_diagnostics().pending_subscriptions, 0);
525        pump_layout_test_session(&mut session, 20);
526        assert_eq!(session.frame_clock_diagnostics().callbacks_executed, 0);
527    }
528
529    #[crate::tessera(crate)]
530    fn persistent_clock_probe(output: TestProbe<Vec<u64>>) {
531        let capture = ObserveClockOnDrop(output);
532        crate::receive_frame_nanos(move |_| {
533            let _ = &capture;
534            crate::FrameNanosControl::Continue
535        });
536    }
537
538    #[test]
539    fn shutdown_drops_clock_captures_while_the_owning_clock_remains_available() {
540        let output = TestProbe::default();
541        let mut session = start_layout_test_session(layout_test({
542            let output = output.clone();
543            move || persistent_clock_probe(output.clone())
544        }));
545        pump_layout_test_session(&mut session, 0);
546        pump_layout_test_session(&mut session, 17);
547        output.take();
548        drop(session);
549        assert_eq!(output.get(), [17]);
550    }
551
552    #[test]
553    fn conditional_state_writes_preserve_idle_frames_and_replay_changed_readers() {
554        let output = TestProbe::default();
555        let mut session = start_layout_test_session(layout_test({
556            let output = output.clone();
557            move || observed_state(output.clone())
558        }));
559        pump_layout_test_session(&mut session, 0);
560        let state = output.get().unwrap();
561        assert!(!state.set_if_changed(7));
562        assert!(!crate::runtime::has_pending_build_invalidations());
563        pump_layout_test_session(&mut session, 1);
564        assert_eq!(
565            session
566                .work()
567                .count("observed_state", crate::WorkPhase::Build),
568            0
569        );
570        assert!(state.set_if_changed(9));
571        assert!(crate::runtime::has_pending_build_invalidations());
572        pump_layout_test_session(&mut session, 2);
573        assert_eq!(state.get(), 9);
574        assert_eq!(
575            session
576                .work()
577                .count("observed_state", crate::WorkPhase::Build),
578            1
579        );
580    }
581
582    #[crate::tessera(crate)]
583    fn owned_state_reader(value: State<usize>, output: TestProbe<usize>) {
584        output.set(value.get());
585    }
586
587    #[test]
588    fn controller_owned_state_replays_readers_and_dispatches_worker_results_on_its_ui_session() {
589        let runtime = RuntimeSession::new();
590        let _scope = runtime.enter();
591        let owner = crate::StateOwner::new();
592        let value = owner.state_with_key("route", || 7usize);
593        assert!(
594            owner.state_with_key("route", || panic!("existing state was reinitialized")) == value
595        );
596        let output = TestProbe::new(0usize);
597        let mut session = start_layout_test_session(layout_test({
598            let output = output.clone();
599            move || owned_state_reader(value, output.clone())
600        }));
601        pump_layout_test_session(&mut session, 0);
602        assert_eq!(output.get(), 7);
603        let dispatcher = owner.dispatcher();
604        assert!(
605            std::thread::spawn(move || dispatcher.dispatch(move || value.set(11)))
606                .join()
607                .unwrap()
608        );
609        assert_eq!(output.get(), 7);
610        pump_layout_test_session(&mut session, 1);
611        assert_eq!(output.get(), 11);
612        assert_eq!(
613            session
614                .work()
615                .count("owned_state_reader", crate::WorkPhase::Build),
616            1
617        );
618        drop(owner);
619        assert!(!value.is_alive());
620        let next = crate::StateOwner::new();
621        let replacement = next.state_with_key("route", || 19usize);
622        assert_eq!(replacement.get(), 19);
623        assert!(!value.is_alive());
624        assert!(std::panic::catch_unwind(AssertUnwindSafe(|| value.set(99))).is_err());
625        assert_eq!(replacement.get(), 19);
626    }
627
628    struct StatefulModifier {
629        extra: bool,
630        output: TestProbe<Vec<State<usize>>>,
631    }
632
633    impl crate::BuildModifierNode for StatefulModifier {
634        fn materialize(
635            &self,
636            context: &mut crate::ModifierBuildContext,
637        ) -> Option<crate::Modifier> {
638            if self.extra {
639                let _ = context.state(|| false);
640            }
641            let state = context.state(|| 3usize);
642            let _ = state.get();
643            self.output.with_mut(|values| values.push(state));
644            Some(crate::Modifier::new())
645        }
646    }
647
648    #[crate::tessera(crate)]
649    fn modifier_owner_fixture(
650        visible: State<bool>,
651        extra: State<bool>,
652        output: TestProbe<Vec<State<usize>>>,
653    ) {
654        use crate::FocusModifierExt as _;
655        let requester = remember(crate::FocusRequester::new).get();
656        let mut modifier = crate::Modifier::new();
657        let extra = extra.get();
658        if extra {
659            modifier = modifier
660                .focus_requester(requester)
661                .on_layout_coordinates(|_| {});
662        }
663        if visible.get() {
664            modifier = modifier
665                .push_build(StatefulModifier {
666                    extra,
667                    output: output.clone(),
668                })
669                .push_build(StatefulModifier { extra, output });
670        }
671        crate::layout::layout().modifier(modifier);
672    }
673
674    #[test]
675    fn capability_state_survives_unrelated_modifiers_and_dies_with_its_attachment() {
676        let runtime = RuntimeSession::new();
677        let _scope = runtime.enter();
678        let owner = crate::StateOwner::new();
679        let visible = owner.state_with_key("visible", || true);
680        let extra = owner.state_with_key("extra", || false);
681        let output = TestProbe::default();
682        let unused = crate::Modifier::new().push_build(StatefulModifier {
683            extra: false,
684            output: output.clone(),
685        });
686        assert!(output.with(Vec::is_empty));
687        drop(unused);
688        let mut session = start_layout_test_session(layout_test({
689            let output = output.clone();
690            move || modifier_owner_fixture(visible, extra, output.clone())
691        }));
692        pump_layout_test_session(&mut session, 0);
693        let original = output.take();
694        assert_eq!(original.len(), 2);
695        assert!(original[0] != original[1]);
696        original[0].set(8);
697        for (frame, enabled) in [(1, false), (2, true), (3, false), (4, true)] {
698            extra.set(enabled);
699            pump_layout_test_session(&mut session, frame);
700            assert!(output.take() == original);
701            assert_eq!(original[0].get(), 8);
702            assert_eq!(original[1].get(), 3);
703        }
704        visible.set(false);
705        pump_layout_test_session(&mut session, 5);
706        assert!(original.iter().all(|state| !state.is_alive()));
707        assert!(output.with(Vec::is_empty));
708        visible.set(true);
709        pump_layout_test_session(&mut session, 6);
710        let replacement = output.take();
711        assert_eq!(replacement.len(), 2);
712        assert!(replacement[0] != original[0]);
713        assert_eq!(replacement[0].get(), 3);
714    }
715
716    struct ClockModifier(TestProbe<Vec<u64>>);
717
718    impl crate::BuildModifierNode for ClockModifier {
719        fn materialize(
720            &self,
721            context: &mut crate::ModifierBuildContext,
722        ) -> Option<crate::Modifier> {
723            let output = self.0.clone();
724            context.receive_frame_nanos(move |nanos| {
725                output.with_mut(|values| values.push(nanos));
726                crate::FrameNanosControl::Continue
727            });
728            Some(crate::Modifier::new())
729        }
730    }
731
732    #[crate::tessera(crate)]
733    fn clock_modifier_fixture(visible: State<bool>, output: TestProbe<Vec<u64>>) {
734        let modifier = if visible.get() {
735            crate::Modifier::new().push_build(ClockModifier(output))
736        } else {
737            crate::Modifier::new()
738        };
739        crate::layout::layout().modifier(modifier);
740    }
741
742    #[test]
743    fn removing_a_stateful_modifier_unsubscribes_its_clock_before_the_next_frame() {
744        let runtime = RuntimeSession::new();
745        let _scope = runtime.enter();
746        let owner = crate::StateOwner::new();
747        let visible = owner.state_with_key("visible", || true);
748        let output = TestProbe::default();
749        let mut session = start_layout_test_session(layout_test({
750            let output = output.clone();
751            move || clock_modifier_fixture(visible, output.clone())
752        }));
753        pump_layout_test_session(&mut session, 0);
754        assert_eq!(session.frame_clock_diagnostics().pending_subscriptions, 1);
755        pump_layout_test_session(&mut session, 1);
756        assert_eq!(output.get(), [1]);
757        assert_eq!(session.frame_clock_diagnostics().pending_subscriptions, 1);
758        visible.set(false);
759        pump_layout_test_session(&mut session, 2);
760        assert_eq!(output.get(), [1, 2]);
761        assert_eq!(session.frame_clock_diagnostics().pending_subscriptions, 0);
762        pump_layout_test_session(&mut session, 3);
763        assert_eq!(session.frame_clock_diagnostics().callbacks_executed, 0);
764        assert_eq!(output.get(), [1, 2]);
765    }
766
767    struct DropIntoState(State<usize>);
768
769    impl Drop for DropIntoState {
770        fn drop(&mut self) {
771            if self.0.is_alive() {
772                self.0.with_mut(|value| *value += 1);
773            }
774        }
775    }
776
777    #[test]
778    fn closing_an_owner_during_state_mutation_drops_its_taken_value_outside_the_slot_borrow() {
779        let runtime = RuntimeSession::new();
780        runtime.run(|| {
781            let observer = crate::StateOwner::new();
782            let observed = observer.state_with_key("drops", || 0usize);
783            let owner = crate::StateOwner::new();
784            let value = owner.state_with_key("resource", || DropIntoState(observed));
785            value.with_mut(|_| owner.close());
786            assert!(!value.is_alive());
787            assert_eq!(observed.get(), 1);
788        });
789    }
790
791    #[test]
792    fn closing_a_state_owner_discards_pending_results_before_they_can_touch_freed_state() {
793        let runtime = RuntimeSession::new();
794        let _scope = runtime.enter();
795        let owner = crate::StateOwner::new();
796        let value = owner.state_with_key("route", || 5usize);
797        let dispatcher = owner.dispatcher();
798        assert!(dispatcher.dispatch(move || value.set(9)));
799        owner.close();
800        assert!(!dispatcher.is_alive());
801        assert!(!dispatcher.dispatch(|| panic!("closed owner accepted a result")));
802        crate::dispatcher::flush();
803        assert!(!value.is_alive());
804        let next = crate::StateOwner::new();
805        assert_eq!(next.state_with_key("route", || 13usize).get(), 13);
806    }
807
808    #[derive(Default)]
809    struct DispatcherOnDrop(std::cell::RefCell<Option<TestProbe<Option<crate::UiDispatcher>>>>);
810
811    impl Drop for DispatcherOnDrop {
812        fn drop(&mut self) {
813            if let Some(output) = self.0.get_mut().take() {
814                output.set(Some(crate::dispatcher::current()));
815            }
816        }
817    }
818
819    #[test]
820    fn services_created_during_resource_cleanup_cannot_reopen_a_closed_session() {
821        let session = RuntimeSession::new();
822        let output = TestProbe::default();
823        let weak = session.downgrade();
824        session
825            .local_resource::<DispatcherOnDrop>()
826            .0
827            .replace(Some(output.clone()));
828        drop(session);
829        let dispatcher = output
830            .take()
831            .expect("resource destructor captured its session service");
832        assert!(!dispatcher.is_alive());
833        assert!(!dispatcher.dispatch(|| panic!("closed session accepted destructor work")));
834        assert!(weak.run(|| ()).is_none());
835    }
836
837    #[test]
838    fn session_shutdown_rejects_dispatched_work_and_closes_external_state_owners() {
839        let runtime = RuntimeSession::new();
840        let owner = runtime.state_owner();
841        let dispatcher = runtime.dispatcher();
842        let value = runtime.run(|| owner.state_with_key("value", || 7usize));
843        drop(runtime);
844        assert!(!owner.is_alive());
845        assert!(!dispatcher.is_alive());
846        assert!(!dispatcher.dispatch(|| panic!("shutdown work executed")));
847        let other = RuntimeSession::new();
848        other.run(|| assert!(!value.is_alive()));
849    }
850}