Skip to main content

fret_runtime/model/
store.rs

1use std::{
2    any::Any,
3    cell::RefCell,
4    collections::HashSet,
5    marker::PhantomData,
6    panic::{AssertUnwindSafe, Location, catch_unwind, resume_unwind},
7    rc::Rc,
8};
9
10use slotmap::SlotMap;
11
12use super::ModelId;
13use super::debug::{ModelChangedDebugInfo, ModelCreatedDebugInfo};
14use super::error::ModelUpdateError;
15use super::handle::Model;
16
17/// Main-thread-only storage for typed models.
18///
19/// `ModelStore` is an `Rc`-backed container that holds model values behind type-erased entries and
20/// provides leasing-based read/update access.
21///
22/// Key properties:
23/// - Models are removed when their last strong [`Model<T>`] handle is dropped.
24/// - `take_changed_models` provides an incremental change list for driving UI updates.
25/// - In debug/strict modes, the store tracks lease locations to surface misuse early.
26pub struct ModelStore {
27    pub(super) inner: Rc<ModelStoreInner>,
28    // Models are main-thread only. Enforce this at compile time by making the store (and all
29    // derived handles) `!Send` + `!Sync`.
30    pub(super) _not_send: PhantomData<std::rc::Rc<()>>,
31}
32
33#[derive(Default)]
34pub(super) struct ModelStoreInner {
35    state: RefCell<ModelStoreState>,
36}
37
38#[cfg(not(test))]
39fn strict_runtime_enabled() -> bool {
40    static STRICT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
41    *STRICT.get_or_init(crate::strict_runtime::strict_runtime_enabled_from_env)
42}
43
44#[cfg(test)]
45thread_local! {
46    static STRICT_RUNTIME_OVERRIDE: std::cell::Cell<Option<bool>> =
47        const { std::cell::Cell::new(None) };
48}
49
50#[cfg(test)]
51fn strict_runtime_enabled() -> bool {
52    STRICT_RUNTIME_OVERRIDE
53        .with(|cell| cell.get())
54        .unwrap_or_else(crate::strict_runtime::strict_runtime_enabled_from_env)
55}
56
57#[cfg(test)]
58struct StrictRuntimeGuard(Option<bool>);
59
60#[cfg(test)]
61fn strict_runtime_for_tests(value: bool) -> StrictRuntimeGuard {
62    let prev = STRICT_RUNTIME_OVERRIDE.with(|cell| {
63        let prev = cell.get();
64        cell.set(Some(value));
65        prev
66    });
67    StrictRuntimeGuard(prev)
68}
69
70#[cfg(test)]
71impl Drop for StrictRuntimeGuard {
72    fn drop(&mut self) {
73        STRICT_RUNTIME_OVERRIDE.with(|cell| cell.set(self.0));
74    }
75}
76
77#[derive(Default)]
78struct ModelStoreState {
79    storage: SlotMap<ModelId, ModelEntry>,
80    changed: Vec<ModelId>,
81    changed_dedup: HashSet<ModelId>,
82}
83
84struct ModelEntry {
85    value: Option<Box<dyn Any>>,
86    revision: u64,
87    strong: u64,
88    pending_drop: bool,
89    #[cfg(debug_assertions)]
90    created_at: &'static Location<'static>,
91    #[cfg(debug_assertions)]
92    created_type: &'static str,
93    #[cfg(debug_assertions)]
94    leased_at: Option<&'static Location<'static>>,
95    #[cfg(debug_assertions)]
96    leased_type: Option<&'static str>,
97    #[cfg(debug_assertions)]
98    last_changed_at: Option<&'static Location<'static>>,
99    #[cfg(debug_assertions)]
100    last_changed_type: Option<&'static str>,
101}
102
103pub(super) struct ModelLease<T: Any> {
104    id: ModelId,
105    value: Option<Box<T>>,
106    dirty: bool,
107}
108
109impl<T: Any> ModelLease<T> {
110    pub(super) fn value_ref(&self) -> &T {
111        self.value
112            .as_deref()
113            .expect("leased model must contain a value")
114    }
115
116    pub(super) fn value_mut(&mut self) -> &mut T {
117        self.value
118            .as_deref_mut()
119            .expect("leased model must contain a value")
120    }
121
122    pub(super) fn mark_dirty(&mut self) {
123        self.dirty = true;
124    }
125}
126
127impl<T: Any> Drop for ModelLease<T> {
128    fn drop(&mut self) {
129        if self.value.is_some() && !std::thread::panicking() {
130            debug_assert!(false, "ModelLease must be ended with ModelStore::end_lease");
131        }
132    }
133}
134
135/// Untyped lease used by `ModelStore::update_any`.
136struct ModelLeaseAny {
137    id: ModelId,
138    value: Option<Box<dyn Any>>,
139    dirty: bool,
140}
141
142impl ModelLeaseAny {
143    fn value_mut(&mut self) -> &mut dyn Any {
144        self.value
145            .as_deref_mut()
146            .expect("leased model must contain a value")
147    }
148
149    fn mark_dirty(&mut self) {
150        self.dirty = true;
151    }
152}
153
154impl Drop for ModelLeaseAny {
155    fn drop(&mut self) {
156        if self.value.is_some() && !std::thread::panicking() {
157            debug_assert!(
158                false,
159                "ModelLeaseAny must be ended with ModelStore::end_lease_any"
160            );
161        }
162    }
163}
164
165impl ModelStore {
166    #[track_caller]
167    fn state(&self) -> std::cell::Ref<'_, ModelStoreState> {
168        match self.inner.state.try_borrow() {
169            Ok(guard) => guard,
170            Err(_) => {
171                let caller = Location::caller();
172                panic!(
173                    "model store is already mutably borrowed (re-entrant borrow) at {}:{}:{}; \
174                     this likely indicates a bug where store methods call back into user code while \
175                     holding a store borrow",
176                    caller.file(),
177                    caller.line(),
178                    caller.column()
179                );
180            }
181        }
182    }
183
184    #[track_caller]
185    fn state_mut(&self) -> std::cell::RefMut<'_, ModelStoreState> {
186        match self.inner.state.try_borrow_mut() {
187            Ok(guard) => guard,
188            Err(_) => {
189                let caller = Location::caller();
190                panic!(
191                    "model store is already borrowed (re-entrant borrow_mut) at {}:{}:{}; \
192                     this likely indicates a bug where store methods call back into user code while \
193                     holding a store borrow",
194                    caller.file(),
195                    caller.line(),
196                    caller.column()
197                );
198            }
199        }
200    }
201
202    #[cfg(test)]
203    fn state_lock_is_held_for_tests(&self) -> bool {
204        self.inner.state.try_borrow_mut().is_err()
205    }
206
207    fn mark_changed_locked(state: &mut ModelStoreState, id: ModelId) {
208        if state.changed_dedup.insert(id) {
209            state.changed.push(id);
210        }
211    }
212
213    #[cfg(debug_assertions)]
214    #[allow(dead_code)]
215    fn debug_lease_info(&self, id: ModelId) -> Option<(&'static str, &'static Location<'static>)> {
216        let state = self.state();
217        let entry = state.storage.get(id)?;
218        let leased_at = entry.leased_at?;
219        let leased_type = entry.leased_type.unwrap_or("<unknown>");
220        Some((leased_type, leased_at))
221    }
222
223    #[cfg(debug_assertions)]
224    fn debug_created_info(
225        &self,
226        id: ModelId,
227    ) -> Option<(&'static str, &'static Location<'static>)> {
228        let state = self.state();
229        let entry = state.storage.get(id)?;
230        Some((entry.created_type, entry.created_at))
231    }
232
233    pub fn debug_created_info_for_id(&self, id: ModelId) -> Option<ModelCreatedDebugInfo> {
234        #[cfg(debug_assertions)]
235        {
236            let (type_name, at) = self.debug_created_info(id)?;
237            Some(ModelCreatedDebugInfo {
238                type_name,
239                file: at.file(),
240                line: at.line(),
241                column: at.column(),
242            })
243        }
244
245        #[cfg(not(debug_assertions))]
246        {
247            let _ = id;
248            None
249        }
250    }
251
252    pub fn debug_last_changed_info_for_id(&self, id: ModelId) -> Option<ModelChangedDebugInfo> {
253        #[cfg(debug_assertions)]
254        {
255            let state = self.state();
256            let entry = state.storage.get(id)?;
257            let at = entry.last_changed_at?;
258            let type_name = entry.last_changed_type.unwrap_or("<unknown>");
259            Some(ModelChangedDebugInfo {
260                type_name,
261                file: at.file(),
262                line: at.line(),
263                column: at.column(),
264            })
265        }
266
267        #[cfg(not(debug_assertions))]
268        {
269            let _ = id;
270            None
271        }
272    }
273
274    pub(super) fn inc_strong(&self, id: ModelId) {
275        let mut state = self.state_mut();
276        let Some(entry) = state.storage.get_mut(id) else {
277            return;
278        };
279        entry.strong = entry.strong.saturating_add(1);
280    }
281
282    pub(super) fn dec_strong(&self, id: ModelId) {
283        // IMPORTANT: do not drop removed values while holding a store borrow.
284        //
285        // Model values may themselves contain `Model<_>` handles (e.g. composite component state),
286        // and dropping those handles re-enters the store to decrement refcounts. Holding a borrow
287        // while dropping would trigger a re-entrant borrow panic (and used to deadlock when this
288        // was a mutex).
289        let removed = {
290            let mut state = self.state_mut();
291            let should_remove_now = {
292                let Some(entry) = state.storage.get_mut(id) else {
293                    return;
294                };
295                entry.strong = entry.strong.saturating_sub(1);
296                if entry.strong != 0 {
297                    return;
298                }
299                let should_remove_now = entry.value.is_some();
300                if !should_remove_now {
301                    entry.pending_drop = true;
302                }
303                should_remove_now
304            };
305            Self::mark_changed_locked(&mut state, id);
306            should_remove_now.then(|| state.storage.remove(id))
307        };
308
309        drop(removed);
310    }
311
312    pub(super) fn upgrade_strong(&self, id: ModelId) -> bool {
313        let mut state = self.state_mut();
314        let Some(entry) = state.storage.get_mut(id) else {
315            return false;
316        };
317        if entry.strong == 0 {
318            return false;
319        }
320        entry.strong = entry.strong.saturating_add(1);
321        true
322    }
323
324    /// Returns and clears the list of models that were marked changed since the last call.
325    ///
326    /// Dropped models are filtered out.
327    pub fn take_changed_models(&mut self) -> Vec<ModelId> {
328        let mut state = self.state_mut();
329        state.changed_dedup.clear();
330        let changed = std::mem::take(&mut state.changed);
331        changed
332            .into_iter()
333            .filter(|&id| {
334                state
335                    .storage
336                    .get(id)
337                    .is_some_and(|entry| entry.strong > 0 && entry.value.is_some())
338            })
339            .collect()
340    }
341
342    #[track_caller]
343    pub fn insert<T: Any>(&mut self, value: T) -> Model<T> {
344        #[cfg(debug_assertions)]
345        let caller = Location::caller();
346        let mut state = self.state_mut();
347        let id = state.storage.insert(ModelEntry {
348            value: Some(Box::new(value)),
349            revision: 0,
350            strong: 1,
351            pending_drop: false,
352            #[cfg(debug_assertions)]
353            created_at: caller,
354            #[cfg(debug_assertions)]
355            created_type: std::any::type_name::<T>(),
356            #[cfg(debug_assertions)]
357            leased_at: None,
358            #[cfg(debug_assertions)]
359            leased_type: None,
360            #[cfg(debug_assertions)]
361            last_changed_at: None,
362            #[cfg(debug_assertions)]
363            last_changed_type: None,
364        });
365        Model::from_store_id(self.clone(), id)
366    }
367
368    pub fn read<T: Any, R>(
369        &self,
370        model: &Model<T>,
371        f: impl FnOnce(&T) -> R,
372    ) -> Result<R, ModelUpdateError> {
373        // IMPORTANT: do not run user code while holding a store borrow.
374        //
375        // Model values can contain other `Model<_>` handles. Cloning/dropping those handles
376        // re-enters this store, so holding a borrow would trigger a re-entrant borrow panic (and
377        // used to deadlock when this was a mutex).
378        let mut lease = self.lease_shared(model)?;
379        let result = if cfg!(panic = "unwind") {
380            catch_unwind(AssertUnwindSafe(|| f(lease.value_ref())))
381        } else {
382            Ok(f(lease.value_ref()))
383        };
384
385        self.end_lease_shared(&mut lease, None);
386
387        match result {
388            Ok(value) => Ok(value),
389            Err(panic) => resume_unwind(panic),
390        }
391    }
392
393    pub fn try_get_copied<T: Any + Copy>(
394        &self,
395        model: &Model<T>,
396    ) -> Result<Option<T>, ModelUpdateError> {
397        match self.read(model, |v| *v) {
398            Ok(v) => Ok(Some(v)),
399            Err(ModelUpdateError::NotFound) => Ok(None),
400            Err(err) => Err(err),
401        }
402    }
403
404    pub fn get_copied<T: Any + Copy>(&self, model: &Model<T>) -> Option<T> {
405        match self.try_get_copied(model) {
406            Ok(v) => v,
407            Err(err) => {
408                if strict_runtime_enabled() {
409                    self.panic_model_get_error::<T>(model.id(), "get_copied", err);
410                }
411                #[cfg(debug_assertions)]
412                self.debug_log_model_get_error::<T>(model.id(), "get_copied", err);
413                None
414            }
415        }
416    }
417
418    pub fn try_get_cloned<T: Any + Clone>(
419        &self,
420        model: &Model<T>,
421    ) -> Result<Option<T>, ModelUpdateError> {
422        match self.read(model, |v| v.clone()) {
423            Ok(v) => Ok(Some(v)),
424            Err(ModelUpdateError::NotFound) => Ok(None),
425            Err(err) => Err(err),
426        }
427    }
428
429    pub fn get_cloned<T: Any + Clone>(&self, model: &Model<T>) -> Option<T> {
430        match self.try_get_cloned(model) {
431            Ok(v) => v,
432            Err(err) => {
433                if strict_runtime_enabled() {
434                    self.panic_model_get_error::<T>(model.id(), "get_cloned", err);
435                }
436                #[cfg(debug_assertions)]
437                self.debug_log_model_get_error::<T>(model.id(), "get_cloned", err);
438                None
439            }
440        }
441    }
442
443    #[cfg(debug_assertions)]
444    #[track_caller]
445    fn debug_log_model_get_error<T: Any>(
446        &self,
447        id: ModelId,
448        op: &'static str,
449        err: ModelUpdateError,
450    ) {
451        let caller = Location::caller();
452        match err {
453            ModelUpdateError::AlreadyLeased => {
454                if let Some((ty, at)) = self.debug_lease_info(id) {
455                    eprintln!(
456                        "model access error: op={op} err=AlreadyLeased id={id:?} type={ty} leased_at={}:{}:{} accessed_at={}:{}:{}",
457                        at.file(),
458                        at.line(),
459                        at.column(),
460                        caller.file(),
461                        caller.line(),
462                        caller.column()
463                    );
464                } else {
465                    eprintln!(
466                        "model access error: op={op} err=AlreadyLeased id={id:?} accessed_at={}:{}:{} (lease origin unknown)",
467                        caller.file(),
468                        caller.line(),
469                        caller.column()
470                    );
471                }
472            }
473            ModelUpdateError::TypeMismatch => {
474                if let Some((stored, at)) = self.debug_created_info(id) {
475                    eprintln!(
476                        "model access error: op={op} err=TypeMismatch id={id:?} stored_type={stored} stored_at={}:{}:{} expected_type={} accessed_at={}:{}:{}",
477                        at.file(),
478                        at.line(),
479                        at.column(),
480                        std::any::type_name::<T>(),
481                        caller.file(),
482                        caller.line(),
483                        caller.column()
484                    );
485                } else {
486                    eprintln!(
487                        "model access error: op={op} err=TypeMismatch id={id:?} expected_type={} accessed_at={}:{}:{}",
488                        std::any::type_name::<T>(),
489                        caller.file(),
490                        caller.line(),
491                        caller.column()
492                    );
493                }
494            }
495            ModelUpdateError::NotFound => {}
496        }
497    }
498
499    #[track_caller]
500    fn panic_model_get_error<T: Any>(
501        &self,
502        id: ModelId,
503        op: &'static str,
504        err: ModelUpdateError,
505    ) -> ! {
506        let caller = Location::caller();
507        match err {
508            ModelUpdateError::AlreadyLeased => {
509                #[cfg(debug_assertions)]
510                if let Some((ty, at)) = self.debug_lease_info(id) {
511                    panic!(
512                        "strict runtime: model access error: op={op} err=AlreadyLeased id={id:?} type={ty} leased_at={}:{}:{} accessed_at={}:{}:{}",
513                        at.file(),
514                        at.line(),
515                        at.column(),
516                        caller.file(),
517                        caller.line(),
518                        caller.column()
519                    );
520                }
521                panic!(
522                    "strict runtime: model access error: op={op} err=AlreadyLeased id={id:?} accessed_at={}:{}:{}",
523                    caller.file(),
524                    caller.line(),
525                    caller.column()
526                );
527            }
528            ModelUpdateError::TypeMismatch => {
529                #[cfg(debug_assertions)]
530                if let Some((stored, at)) = self.debug_created_info(id) {
531                    panic!(
532                        "strict runtime: model access error: op={op} err=TypeMismatch id={id:?} stored_type={stored} stored_at={}:{}:{} expected_type={} accessed_at={}:{}:{}",
533                        at.file(),
534                        at.line(),
535                        at.column(),
536                        std::any::type_name::<T>(),
537                        caller.file(),
538                        caller.line(),
539                        caller.column()
540                    );
541                }
542                panic!(
543                    "strict runtime: model access error: op={op} err=TypeMismatch id={id:?} expected_type={} accessed_at={}:{}:{}",
544                    std::any::type_name::<T>(),
545                    caller.file(),
546                    caller.line(),
547                    caller.column()
548                );
549            }
550            ModelUpdateError::NotFound => {
551                panic!(
552                    "strict runtime: model access error: op={op} err=NotFound id={id:?} accessed_at={}:{}:{}",
553                    caller.file(),
554                    caller.line(),
555                    caller.column()
556                );
557            }
558        }
559    }
560
561    pub fn revision<T: Any>(&self, model: &Model<T>) -> Option<u64> {
562        let state = self.state();
563        state.storage.get(model.id()).map(|e| e.revision)
564    }
565
566    #[track_caller]
567    pub fn update<T: Any, R>(
568        &mut self,
569        model: &Model<T>,
570        f: impl FnOnce(&mut T) -> R,
571    ) -> Result<R, ModelUpdateError> {
572        let changed_at = Location::caller();
573
574        let mut lease = self.lease(model)?;
575        let result = if cfg!(panic = "unwind") {
576            catch_unwind(AssertUnwindSafe(|| f(lease.value_mut())))
577        } else {
578            Ok(f(lease.value_mut()))
579        };
580
581        match result {
582            Ok(value) => {
583                lease.mark_dirty();
584                self.end_lease_with_changed_at(&mut lease, changed_at);
585                Ok(value)
586            }
587            Err(panic) => {
588                self.end_lease(&mut lease);
589                resume_unwind(panic)
590            }
591        }
592    }
593
594    #[track_caller]
595    pub fn update_any<R>(
596        &mut self,
597        id: ModelId,
598        f: impl FnOnce(&mut dyn Any) -> R,
599    ) -> Result<R, ModelUpdateError> {
600        let changed_at = Location::caller();
601
602        let mut lease = self.lease_any(id)?;
603        let result = if cfg!(panic = "unwind") {
604            catch_unwind(AssertUnwindSafe(|| f(lease.value_mut())))
605        } else {
606            Ok(f(lease.value_mut()))
607        };
608
609        match result {
610            Ok(value) => {
611                lease.mark_dirty();
612                self.end_lease_any_with_changed_at(&mut lease, changed_at);
613                Ok(value)
614            }
615            Err(panic) => {
616                self.end_lease_any(&mut lease);
617                resume_unwind(panic)
618            }
619        }
620    }
621
622    #[track_caller]
623    fn lease_shared<T: Any>(&self, model: &Model<T>) -> Result<ModelLease<T>, ModelUpdateError> {
624        #[cfg(debug_assertions)]
625        let caller = Location::caller();
626        let boxed = {
627            let mut state = self.state_mut();
628            let entry = state
629                .storage
630                .get_mut(model.id())
631                .ok_or(ModelUpdateError::NotFound)?;
632            if entry.strong == 0 {
633                return Err(ModelUpdateError::NotFound);
634            }
635
636            match entry.value.take() {
637                Some(value) => {
638                    #[cfg(debug_assertions)]
639                    {
640                        entry.leased_at = Some(caller);
641                        entry.leased_type = Some(std::any::type_name::<T>());
642                    }
643                    value
644                }
645                None => {
646                    #[cfg(debug_assertions)]
647                    {
648                        let leased_type = entry.leased_type.unwrap_or("<unknown>");
649                        if let Some(leased_at) = entry.leased_at {
650                            eprintln!(
651                                "model already leased: id={:?} type={} leased_at={}:{}:{} attempted_at={}:{}:{}",
652                                model.id(),
653                                leased_type,
654                                leased_at.file(),
655                                leased_at.line(),
656                                leased_at.column(),
657                                caller.file(),
658                                caller.line(),
659                                caller.column()
660                            );
661                        } else {
662                            eprintln!(
663                                "model already leased: id={:?} type={} attempted_at={}:{}:{} (lease origin unknown)",
664                                model.id(),
665                                leased_type,
666                                caller.file(),
667                                caller.line(),
668                                caller.column()
669                            );
670                        }
671                    }
672                    return Err(ModelUpdateError::AlreadyLeased);
673                }
674            }
675        };
676
677        match boxed.downcast::<T>() {
678            Ok(value) => Ok(ModelLease {
679                id: model.id(),
680                value: Some(value),
681                dirty: false,
682            }),
683            Err(boxed) => {
684                #[cfg(debug_assertions)]
685                {
686                    let state = self.state();
687                    if let Some(entry) = state.storage.get(model.id()) {
688                        eprintln!(
689                            "model type mismatch: id={:?} stored_type={} stored_at={}:{}:{} expected_type={} attempted_at={}:{}:{}",
690                            model.id(),
691                            entry.created_type,
692                            entry.created_at.file(),
693                            entry.created_at.line(),
694                            entry.created_at.column(),
695                            std::any::type_name::<T>(),
696                            caller.file(),
697                            caller.line(),
698                            caller.column()
699                        );
700                    }
701                }
702
703                let mut state = self.state_mut();
704                if let Some(entry) = state.storage.get_mut(model.id())
705                    && entry.value.is_none()
706                {
707                    entry.value = Some(boxed);
708                    #[cfg(debug_assertions)]
709                    {
710                        entry.leased_at = None;
711                        entry.leased_type = None;
712                    }
713                }
714                Err(ModelUpdateError::TypeMismatch)
715            }
716        }
717    }
718
719    pub(super) fn lease<T: Any>(
720        &mut self,
721        model: &Model<T>,
722    ) -> Result<ModelLease<T>, ModelUpdateError> {
723        self.lease_shared(model)
724    }
725
726    #[track_caller]
727    fn lease_any(&mut self, id: ModelId) -> Result<ModelLeaseAny, ModelUpdateError> {
728        #[cfg(debug_assertions)]
729        let caller = Location::caller();
730        let boxed = {
731            let mut state = self.state_mut();
732            let entry = state
733                .storage
734                .get_mut(id)
735                .ok_or(ModelUpdateError::NotFound)?;
736            if entry.strong == 0 {
737                return Err(ModelUpdateError::NotFound);
738            }
739
740            match entry.value.take() {
741                Some(value) => {
742                    #[cfg(debug_assertions)]
743                    {
744                        entry.leased_at = Some(caller);
745                        entry.leased_type = Some(entry.created_type);
746                    }
747                    value
748                }
749                None => {
750                    #[cfg(debug_assertions)]
751                    {
752                        let leased_type = entry.leased_type.unwrap_or("<unknown>");
753                        if let Some(leased_at) = entry.leased_at {
754                            eprintln!(
755                                "model already leased: id={id:?} type={leased_type} leased_at={}:{}:{} attempted_at={}:{}:{}",
756                                leased_at.file(),
757                                leased_at.line(),
758                                leased_at.column(),
759                                caller.file(),
760                                caller.line(),
761                                caller.column()
762                            );
763                        } else {
764                            eprintln!(
765                                "model already leased: id={id:?} type={leased_type} attempted_at={}:{}:{} (lease origin unknown)",
766                                caller.file(),
767                                caller.line(),
768                                caller.column()
769                            );
770                        }
771                    }
772                    return Err(ModelUpdateError::AlreadyLeased);
773                }
774            }
775        };
776
777        Ok(ModelLeaseAny {
778            id,
779            value: Some(boxed),
780            dirty: false,
781        })
782    }
783
784    fn end_lease_shared<T: Any>(
785        &self,
786        lease: &mut ModelLease<T>,
787        changed_at: Option<&'static Location<'static>>,
788    ) {
789        let Some(value) = lease.value.take() else {
790            return;
791        };
792
793        // Same borrow-drop rule as `dec_strong`: do not drop removed values while holding a borrow.
794        let removed = {
795            let mut state = self.state_mut();
796            let (mark_dirty, should_remove) = {
797                let entry = state.storage.get_mut(lease.id).expect("leased id exists");
798                assert!(entry.value.is_none(), "model entry should be leased");
799                entry.value = Some(value);
800                #[cfg(debug_assertions)]
801                {
802                    entry.leased_at = None;
803                    entry.leased_type = None;
804                }
805                if lease.dirty {
806                    entry.revision = entry.revision.saturating_add(1);
807                    #[cfg(debug_assertions)]
808                    {
809                        entry.last_changed_at = changed_at;
810                        entry.last_changed_type = Some(std::any::type_name::<T>());
811                    }
812                    // NOTE: `entry` holds a mutable borrow of `state`, so defer the `mark_changed` call.
813                }
814                let should_remove = entry.pending_drop && entry.strong == 0;
815                (lease.dirty, should_remove)
816            };
817            if mark_dirty {
818                Self::mark_changed_locked(&mut state, lease.id);
819            }
820            if should_remove {
821                Self::mark_changed_locked(&mut state, lease.id);
822                Some(state.storage.remove(lease.id))
823            } else {
824                None
825            }
826        };
827
828        drop(removed);
829    }
830
831    pub(super) fn end_lease<T: Any>(&mut self, lease: &mut ModelLease<T>) {
832        self.end_lease_shared(lease, None);
833    }
834
835    pub(super) fn end_lease_with_changed_at<T: Any>(
836        &mut self,
837        lease: &mut ModelLease<T>,
838        changed_at: &'static Location<'static>,
839    ) {
840        self.end_lease_shared(lease, Some(changed_at));
841    }
842
843    fn end_lease_any_shared(
844        &self,
845        lease: &mut ModelLeaseAny,
846        changed_at: Option<&'static Location<'static>>,
847    ) {
848        let Some(value) = lease.value.take() else {
849            return;
850        };
851
852        // Same borrow-drop rule as `dec_strong`: do not drop removed values while holding a borrow.
853        let removed = {
854            let mut state = self.state_mut();
855            let (mark_dirty, should_remove) = {
856                let entry = state.storage.get_mut(lease.id).expect("leased id exists");
857                assert!(entry.value.is_none(), "model entry should be leased");
858                entry.value = Some(value);
859                #[cfg(debug_assertions)]
860                {
861                    entry.leased_at = None;
862                    entry.leased_type = None;
863                }
864                if lease.dirty {
865                    entry.revision = entry.revision.saturating_add(1);
866                    #[cfg(debug_assertions)]
867                    {
868                        entry.last_changed_at = changed_at;
869                        entry.last_changed_type = Some(entry.created_type);
870                    }
871                    // NOTE: `entry` holds a mutable borrow of `state`, so defer the `mark_changed` call.
872                }
873                let should_remove = entry.pending_drop && entry.strong == 0;
874                (lease.dirty, should_remove)
875            };
876            if mark_dirty {
877                Self::mark_changed_locked(&mut state, lease.id);
878            }
879            if should_remove {
880                Self::mark_changed_locked(&mut state, lease.id);
881                Some(state.storage.remove(lease.id))
882            } else {
883                None
884            }
885        };
886
887        drop(removed);
888    }
889
890    fn end_lease_any(&mut self, lease: &mut ModelLeaseAny) {
891        self.end_lease_any_shared(lease, None);
892    }
893
894    fn end_lease_any_with_changed_at(
895        &mut self,
896        lease: &mut ModelLeaseAny,
897        changed_at: &'static Location<'static>,
898    ) {
899        self.end_lease_any_shared(lease, Some(changed_at));
900    }
901
902    pub fn notify_with_changed_at<T: Any>(
903        &mut self,
904        model: &Model<T>,
905        changed_at: &'static Location<'static>,
906    ) -> Result<(), ModelUpdateError> {
907        let id = model.id();
908
909        let mut state = self.state_mut();
910        {
911            let Some(entry) = state.storage.get_mut(id) else {
912                return Err(ModelUpdateError::NotFound);
913            };
914            if entry.strong == 0 {
915                return Err(ModelUpdateError::NotFound);
916            }
917            let Some(value) = entry.value.as_ref() else {
918                return Err(ModelUpdateError::AlreadyLeased);
919            };
920            if !value.is::<T>() {
921                return Err(ModelUpdateError::TypeMismatch);
922            }
923
924            entry.revision = entry.revision.saturating_add(1);
925            #[cfg(debug_assertions)]
926            {
927                entry.last_changed_at = Some(changed_at);
928                entry.last_changed_type = Some(std::any::type_name::<T>());
929            }
930        }
931
932        Self::mark_changed_locked(&mut state, id);
933        Ok(())
934    }
935
936    #[track_caller]
937    pub fn notify<T: Any>(&mut self, model: &Model<T>) -> Result<(), ModelUpdateError> {
938        self.notify_with_changed_at(model, Location::caller())
939    }
940}
941
942impl Clone for ModelStore {
943    fn clone(&self) -> Self {
944        Self {
945            inner: self.inner.clone(),
946            _not_send: PhantomData,
947        }
948    }
949}
950
951impl Default for ModelStore {
952    fn default() -> Self {
953        Self {
954            inner: Rc::new(ModelStoreInner::default()),
955            _not_send: PhantomData,
956        }
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::super::{Model, ModelUpdateError};
963    use super::*;
964
965    #[cfg(debug_assertions)]
966    #[test]
967    fn lease_markers_are_set_and_cleared() {
968        let mut store = ModelStore::default();
969        let model = store.insert(123_u32);
970        let id = model.id();
971
972        let mut lease = store.lease(&model).unwrap();
973        assert!(store.debug_lease_info(id).is_some());
974
975        store.end_lease(&mut lease);
976        assert!(store.debug_lease_info(id).is_none());
977    }
978
979    #[test]
980    fn read_does_not_hold_store_lock_while_running_user_code() {
981        #[derive(Clone)]
982        struct Outer {
983            inner: Model<u32>,
984        }
985
986        let mut store = ModelStore::default();
987        let inner = store.insert(123_u32);
988        let outer = store.insert(Outer {
989            inner: inner.clone(),
990        });
991
992        let out = store
993            .read(&outer, |outer| {
994                assert!(
995                    !store.state_lock_is_held_for_tests(),
996                    "ModelStore::read must not hold a store borrow while running user closures"
997                );
998
999                // If `read` regresses to holding the lock while executing the closure, this clone
1000                // would re-enter the store and could trigger a re-entrant borrow panic (and used to
1001                // deadlock when this was a mutex). The borrow probe above turns that into a
1002                // deterministic assertion failure.
1003                let _inner_clone = outer.inner.clone();
1004
1005                1u32
1006            })
1007            .expect("outer model should be readable");
1008
1009        assert_eq!(out, 1);
1010    }
1011
1012    #[test]
1013    fn dropping_last_strong_does_not_drop_value_while_holding_store_lock() {
1014        struct DropProbe {
1015            store: ModelStore,
1016        }
1017
1018        impl Drop for DropProbe {
1019            fn drop(&mut self) {
1020                assert!(
1021                    !self.store.state_lock_is_held_for_tests(),
1022                    "model value must not be dropped while holding a store borrow"
1023                );
1024            }
1025        }
1026
1027        let mut store = ModelStore::default();
1028        let model = store.insert(DropProbe {
1029            store: store.clone(),
1030        });
1031
1032        drop(model);
1033    }
1034
1035    #[test]
1036    fn strong_handle_drop_removes_entry() {
1037        let mut store = ModelStore::default();
1038        let model = store.insert(123_u32);
1039        let id = model.id();
1040
1041        assert!(store.state().storage.contains_key(id));
1042        drop(model);
1043        assert!(!store.state().storage.contains_key(id));
1044    }
1045
1046    #[test]
1047    fn clone_increments_and_decrements_strong_count() {
1048        let mut store = ModelStore::default();
1049        let model = store.insert(123_u32);
1050        let id = model.id();
1051
1052        let model2 = model.clone();
1053        {
1054            let state = store.state();
1055            let entry = state.storage.get(id).unwrap();
1056            assert_eq!(entry.strong, 2);
1057        }
1058
1059        drop(model);
1060        {
1061            let state = store.state();
1062            let entry = state.storage.get(id).unwrap();
1063            assert_eq!(entry.strong, 1);
1064        }
1065
1066        drop(model2);
1067        assert!(!store.state().storage.contains_key(id));
1068    }
1069
1070    #[test]
1071    fn dropping_last_strong_while_leased_defers_removal_until_end_lease() {
1072        let mut store = ModelStore::default();
1073        let model = store.insert(String::from("hello"));
1074        let id = model.id();
1075
1076        let mut lease = store.lease(&model).unwrap();
1077        drop(model);
1078
1079        {
1080            let state = store.state();
1081            let entry = state.storage.get(id).unwrap();
1082            assert_eq!(entry.strong, 0);
1083            assert!(entry.value.is_none());
1084            assert!(entry.pending_drop);
1085        }
1086
1087        store.end_lease(&mut lease);
1088        assert!(!store.state().storage.contains_key(id));
1089    }
1090
1091    #[test]
1092    fn take_changed_models_filters_dropped_entries() {
1093        let mut store = ModelStore::default();
1094        let model = store.insert(123_u32);
1095
1096        let _ = store.update(&model, |v| *v = 456_u32);
1097        drop(model);
1098
1099        let changed = store.take_changed_models();
1100        assert_eq!(changed.len(), 0);
1101    }
1102
1103    #[allow(clippy::mutable_key_type)]
1104    #[test]
1105    fn model_equality_and_hash_are_scoped_to_the_store() {
1106        let mut store_a = ModelStore::default();
1107        let mut store_b = ModelStore::default();
1108
1109        let a = store_a.insert(1u32);
1110        let b = store_b.insert(1u32);
1111
1112        assert_ne!(a, b);
1113
1114        let mut set = std::collections::HashSet::new();
1115        set.insert(a.clone());
1116        set.insert(b.clone());
1117        assert_eq!(set.len(), 2);
1118
1119        let weak_a = a.downgrade();
1120        let weak_b = b.downgrade();
1121        assert_ne!(weak_a, weak_b);
1122    }
1123
1124    #[test]
1125    fn notify_marks_changed_and_bumps_revision() {
1126        let mut store = ModelStore::default();
1127        let model = store.insert(123_u32);
1128
1129        assert_eq!(store.revision(&model), Some(0));
1130        store.notify(&model).expect("notify should succeed");
1131        assert_eq!(store.revision(&model), Some(1));
1132
1133        let changed = store.take_changed_models();
1134        assert_eq!(changed, vec![model.id()]);
1135    }
1136
1137    #[test]
1138    fn notify_errors_while_leased() {
1139        let mut store = ModelStore::default();
1140        let model = store.insert(123_u32);
1141
1142        let mut lease = store.lease(&model).expect("lease should succeed");
1143        let err = store.notify(&model).expect_err("notify should fail");
1144        assert!(matches!(err, ModelUpdateError::AlreadyLeased));
1145
1146        store.end_lease(&mut lease);
1147    }
1148
1149    #[test]
1150    fn update_does_not_hold_store_lock_while_running_user_code() {
1151        #[derive(Clone)]
1152        struct Outer {
1153            inner: Model<u32>,
1154            touched: u32,
1155        }
1156
1157        let mut store = ModelStore::default();
1158        let store_probe = store.clone();
1159        let inner = store.insert(123_u32);
1160        let outer = store.insert(Outer {
1161            inner: inner.clone(),
1162            touched: 0,
1163        });
1164
1165        store
1166            .update(&outer, |outer| {
1167                assert!(
1168                    !store_probe.state_lock_is_held_for_tests(),
1169                    "ModelStore::update must not hold a store borrow while running user closures"
1170                );
1171
1172                // Re-enter the store while `update` is executing user code to ensure lock/borrow
1173                // discipline remains correct.
1174                assert_eq!(store_probe.get_copied(&outer.inner), Some(123_u32));
1175
1176                outer.touched += 1;
1177            })
1178            .expect("outer model should be updatable");
1179
1180        let touched = store
1181            .read(&outer, |outer| outer.touched)
1182            .expect("outer readable");
1183        assert_eq!(touched, 1);
1184    }
1185
1186    #[test]
1187    fn update_any_does_not_hold_store_lock_while_running_user_code() {
1188        #[derive(Clone)]
1189        struct Outer {
1190            inner: Model<u32>,
1191            touched: u32,
1192        }
1193
1194        let mut store = ModelStore::default();
1195        let store_probe = store.clone();
1196        let inner = store.insert(123_u32);
1197        let outer = store.insert(Outer {
1198            inner: inner.clone(),
1199            touched: 0,
1200        });
1201
1202        store
1203            .update_any(outer.id(), |any| {
1204                assert!(
1205                    !store_probe.state_lock_is_held_for_tests(),
1206                    "ModelStore::update_any must not hold a store borrow while running user closures"
1207                );
1208
1209                let outer = any.downcast_mut::<Outer>().expect("stored type should match");
1210
1211                // Re-enter the store while `update_any` is executing user code to ensure
1212                // lock/borrow discipline remains correct.
1213                assert_eq!(store_probe.get_copied(&outer.inner), Some(123_u32));
1214
1215                outer.touched += 1;
1216            })
1217            .expect("update_any should succeed");
1218
1219        let touched = store
1220            .read(&outer, |outer| outer.touched)
1221            .expect("outer readable");
1222        assert_eq!(touched, 1);
1223    }
1224
1225    #[test]
1226    fn update_any_updates_value_and_bumps_revision() {
1227        let mut store = ModelStore::default();
1228        let model = store.insert(1_u32);
1229
1230        assert_eq!(store.revision(&model), Some(0));
1231        store
1232            .update_any(model.id(), |any| {
1233                let v = any.downcast_mut::<u32>().expect("stored type should match");
1234                *v = 2;
1235            })
1236            .expect("update_any should succeed");
1237
1238        assert_eq!(store.get_copied(&model), Some(2));
1239        assert_eq!(store.revision(&model), Some(1));
1240
1241        let changed = store.take_changed_models();
1242        assert_eq!(changed, vec![model.id()]);
1243    }
1244
1245    #[test]
1246    fn update_any_errors_while_leased() {
1247        let mut store = ModelStore::default();
1248        let model = store.insert(1_u32);
1249        let mut lease = store.lease(&model).expect("lease should succeed");
1250
1251        let err = store
1252            .update_any(model.id(), |_any| {})
1253            .expect_err("update_any should fail while leased");
1254        assert!(matches!(err, ModelUpdateError::AlreadyLeased));
1255
1256        store.end_lease(&mut lease);
1257    }
1258
1259    #[test]
1260    fn get_copied_returns_none_while_leased_and_try_get_copied_returns_error() {
1261        let mut store = ModelStore::default();
1262        let model = store.insert(123_u32);
1263
1264        let mut lease = store.lease(&model).expect("lease should succeed");
1265
1266        assert_eq!(store.get_copied(&model), None);
1267        assert!(matches!(
1268            store.try_get_copied(&model),
1269            Err(ModelUpdateError::AlreadyLeased)
1270        ));
1271
1272        store.end_lease(&mut lease);
1273        assert_eq!(store.get_copied(&model), Some(123_u32));
1274    }
1275
1276    #[test]
1277    fn get_cloned_returns_none_on_type_mismatch_and_try_get_cloned_returns_error() {
1278        let mut store = ModelStore::default();
1279        let model_u32 = store.insert(123_u32);
1280        let id = model_u32.id();
1281
1282        // Construct an intentionally invalid handle to exercise the TypeMismatch error path.
1283        let model_string = Model::<String>::from_store_id(store.clone(), id);
1284
1285        assert_eq!(store.get_cloned(&model_string), None);
1286        assert!(matches!(
1287            store.try_get_cloned(&model_string),
1288            Err(ModelUpdateError::TypeMismatch)
1289        ));
1290
1291        // Ensure the original value remains intact after the failed access.
1292        assert_eq!(store.get_copied(&model_u32), Some(123_u32));
1293    }
1294
1295    #[test]
1296    fn update_unwind_does_not_poison_store_state() {
1297        if !cfg!(panic = "unwind") {
1298            return;
1299        }
1300
1301        let mut store = ModelStore::default();
1302        let model = store.insert(1_u32);
1303
1304        let result = catch_unwind(AssertUnwindSafe(|| {
1305            let _ = store.update(&model, |_v| panic!("boom"));
1306        }));
1307        assert!(result.is_err());
1308
1309        assert_eq!(store.get_copied(&model), Some(1_u32));
1310        assert!(matches!(store.try_get_copied(&model), Ok(Some(1_u32))));
1311    }
1312
1313    #[test]
1314    fn strict_runtime_panics_on_already_leased_get_copied() {
1315        let _guard = strict_runtime_for_tests(true);
1316
1317        let mut store = ModelStore::default();
1318        let model = store.insert(123_u32);
1319
1320        let mut lease = store.lease(&model).expect("lease should succeed");
1321        let result = catch_unwind(AssertUnwindSafe(|| {
1322            let _ = store.get_copied(&model);
1323        }));
1324        assert!(result.is_err());
1325
1326        store.end_lease(&mut lease);
1327    }
1328}