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
17pub struct ModelStore {
27 pub(super) inner: Rc<ModelStoreInner>,
28 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
135struct 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 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 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 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 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 }
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 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 }
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 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 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 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 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 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}