1use std::{
2 any::Any,
3 cell::RefCell,
4 collections::{HashMap, HashSet, VecDeque},
5 rc::Rc,
6};
7
8use slotmap::SlotMap;
9#[cfg(debug_assertions)]
10use slotmap::SparseSecondaryMap;
11
12use crate::{LocalProperty, TransitionManager, TransitionQueueEntry};
13
14use super::{private::PropertyId, PropertyValue};
15
16#[derive(Clone, Debug, Default)]
17pub struct EffectDrainReport {
18 pub ran: usize,
19 pub popped: usize,
20 pub skipped_clean: usize,
21 pub skipped_unregistered: usize,
22 pub skipped_missing: usize,
23 pub remaining: usize,
24 pub cutoffs_evaluated: usize,
25 pub cutoffs_suppressed: usize,
26 pub cutoffs_propagated: usize,
27 pub remaining_cutoffs: usize,
28 pub budget_exhausted: bool,
29 pub top_effects: Vec<(String, usize)>,
30 pub top_cutoffs: Vec<(String, usize)>,
31}
32
33thread_local! {
34 pub(crate) static PROPERTY_TABLE: PropertyTable = PropertyTable::default();
36 pub(crate) static PROPERTY_TIME: RefCell<LocalProperty<u64>> = RefCell::new(LocalProperty::new(0));
38 pub(crate) static PROPERTY_MILLIS: RefCell<LocalProperty<u64>> = RefCell::new(LocalProperty::new(0));
40}
41
42pub struct PropertyData {
44 owner_graph: Option<u64>,
45 owner_scope: Option<std::rc::Weak<super::shared_graph::BindingScopeInner>>,
46 typed_data: Box<dyn Any>,
50 pub inbound: Vec<PropertyId>,
52 pub outbound: Vec<PropertyId>,
54 pub dirty: bool,
58 pub(super) cutoff_settler: Option<Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>>,
61}
62
63impl PropertyData {
64 fn typed_data<T: 'static>(&mut self) -> &mut TypedPropertyData<T> {
65 self.typed_data
66 .downcast_mut::<TypedPropertyData<T>>()
67 .unwrap_or_else(|| panic!("Failed to downcast to TypedPropertyData<{}>. The actual type does not match the expected type.", std::any::type_name::<T>()))
68 }
69}
70
71pub struct TypedPropertyData<T> {
73 value: T,
74 shared: Option<std::sync::Weak<super::shared_property::SharedCell<T>>>,
75 publisher: Option<Rc<dyn Fn(&T)>>,
76 transition_manager: Option<TransitionManager<T>>,
77 property_type: PropertyType<T>,
79}
80
81#[derive(Clone)]
83pub(crate) enum PropertyType<T> {
84 Literal,
85 Computed {
86 evaluator: Rc<dyn Fn() -> T>,
88 cutoff: Option<Cutoff<T>>,
89 },
90}
91
92#[derive(Clone)]
93pub(crate) struct Cutoff<T> {
94 pub(super) predicate: Rc<dyn Fn(&T, &T) -> bool>,
95 pub(super) initialized: bool,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub(super) enum CutoffEvaluation {
100 NotCutoff,
101 Initialized,
102 Suppressed,
103 Propagated,
104}
105
106#[derive(Default)]
108pub(crate) struct PropertyTable {
109 pub(crate) property_map: RefCell<SlotMap<PropertyId, Entry>>,
112 #[cfg(debug_assertions)]
113 debug_names: RefCell<SparseSecondaryMap<PropertyId, String>>,
114 effect_debug_names: RefCell<HashMap<PropertyId, String>>,
115 effect_properties: RefCell<HashSet<PropertyId>>,
116 queued_effects: RefCell<VecDeque<PropertyId>>,
117 queued_effect_set: RefCell<HashSet<PropertyId>>,
118 queued_cutoffs: RefCell<VecDeque<PropertyId>>,
119 queued_cutoff_set: RefCell<HashSet<PropertyId>>,
120}
121
122pub struct Entry {
124 ref_count: usize,
125 data: Option<PropertyData>,
126}
127
128impl PropertyTable {
129 pub(super) fn set_shared<T: super::SharedPropertyValue>(
130 &self,
131 id: PropertyId,
132 shared: std::sync::Weak<super::shared_property::SharedCell<T>>,
133 publisher: Rc<dyn Fn(&T)>,
134 ) {
135 self.with_property_data_mut(id, |data| {
136 let typed = data.typed_data::<T>();
137 typed.shared = Some(shared);
138 typed.publisher = Some(publisher);
139 });
140 }
141
142 pub(super) fn clear_shared<T: PropertyValue>(&self, id: PropertyId) {
143 self.with_property_data_mut(id, |data| {
144 let typed = data.typed_data::<T>();
145 typed.shared = None;
146 typed.publisher = None;
147 });
148 }
149
150 pub(super) fn shared_storage<T: super::SharedPropertyValue>(
151 &self,
152 id: PropertyId,
153 ) -> Option<std::sync::Arc<super::shared_property::SharedCell<T>>> {
154 self.with_property_data_mut(id, |data| data.typed_data::<T>().shared.as_ref()?.upgrade())
155 }
156
157 fn publish_stored<T: PropertyValue>(&self, id: PropertyId) {
158 let publication = self.with_property_data_mut(id, |data| {
159 let typed = data.typed_data::<T>();
160 Some((typed.publisher.as_ref()?.clone(), typed.value.clone()))
161 });
162 if let Some((publish, value)) = publication {
163 publish(&value);
164 }
165 }
166
167 pub(crate) fn has_value_type<T: PropertyValue>(&self, id: PropertyId) -> bool {
168 self.with_property_data(id, |data| data.typed_data.is::<TypedPropertyData<T>>())
169 }
170
171 pub fn get_value<T: PropertyValue>(&self, id: PropertyId) -> T {
175 self.update_value::<T>(id);
176 self.with_property_data_mut(id, |property_data| {
177 property_data.typed_data::<T>().value.clone()
178 })
179 }
180
181 pub fn read_value<T: PropertyValue, V>(&self, id: PropertyId, f: impl FnOnce(&T) -> V) -> V {
183 self.update_value::<T>(id);
184 self.with_property_data_mut(id, |property_data| {
185 f(&property_data.typed_data::<T>().value)
186 })
187 }
188
189 pub fn set_value<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
193 self.import_shared(id, new_val);
194 self.publish_stored::<T>(id);
195 }
196
197 pub(super) fn import_shared<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
199 self.with_property_data_mut(id, |property_data: &mut PropertyData| {
200 let typed_data = property_data.typed_data();
201 typed_data.value = new_val;
202 property_data.dirty = false;
203 });
204 self.dirtify_outbound(id);
205 }
206
207 pub fn invalidate(&self, id: PropertyId) {
208 let (newly_dirty, is_cutoff) = self.with_property_data_mut(id, |property_data| {
209 if property_data.dirty {
210 (false, property_data.cutoff_settler.is_some())
211 } else {
212 property_data.dirty = true;
213 (true, property_data.cutoff_settler.is_some())
214 }
215 });
216 if newly_dirty {
217 if is_cutoff {
218 self.enqueue_cutoff(id);
219 } else {
220 self.enqueue_effect_if_registered(id);
221 self.dirtify_outbound(id);
222 }
223 }
224 }
225
226 pub fn add_entry<T: PropertyValue>(
228 &self,
229 start_val: T,
230 inbound: Vec<PropertyId>,
231 data: PropertyType<T>,
232 debug_name: Option<&str>,
233 ) -> PropertyId {
234 #[cfg(not(debug_assertions))]
235 let _ = debug_name;
236
237 let is_cutoff = matches!(
238 &data,
239 PropertyType::Computed {
240 cutoff: Some(_),
241 ..
242 }
243 );
244 let cutoff_settler = is_cutoff.then(|| {
245 Rc::new(|table: &PropertyTable, id| table.update_value::<T>(id))
246 as Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>
247 });
248
249 let id = {
250 let Ok(mut sm) = self.property_map.try_borrow_mut() else {
251 #[cfg(debug_assertions)]
252 panic!(
253 "couldn't create new property \"{}\"- table already borrowed",
254 debug_name.unwrap_or("<no name>")
255 );
256 #[cfg(not(debug_assertions))]
257 panic!("couldn't create new property - table already borrowed");
258 };
259 let entry = Entry {
260 ref_count: 1,
261 data: Some(PropertyData {
262 owner_graph: super::shared_graph::current_graph_id(),
263 owner_scope: super::shared_graph::current_binding_scope(),
264 inbound,
265 dirty: true,
266 typed_data: Box::new(TypedPropertyData {
267 value: start_val,
268 shared: None,
269 publisher: None,
270 property_type: data,
271 transition_manager: None,
272 }),
273 outbound: Vec::with_capacity(0),
274 cutoff_settler,
275 }),
276 };
277 sm.insert(entry)
278 };
279 #[cfg(debug_assertions)]
280 {
281 if let Some(name) = debug_name {
282 self.debug_names.borrow_mut().insert(id, name.to_owned());
283 }
284 }
285 self.connect_inbound(id);
286 id
287 }
288
289 pub fn transition<T: PropertyValue>(
293 &self,
294 id: PropertyId,
295 transition: TransitionQueueEntry<T>,
296 overwrite: bool,
297 ) {
298 let mut should_reconnect_inbound = false;
299 let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
300 let (millis_id, curr_millis) =
301 PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
302 self.with_property_data_mut(id, |property_data: &mut PropertyData| {
303 let typed_data = property_data.typed_data::<T>();
304 let transition_manager = typed_data.transition_manager.get_or_insert_with(|| {
305 TransitionManager::new(typed_data.value.clone(), curr_time, curr_millis)
306 });
307 if overwrite {
308 transition_manager.reset_transitions(curr_time, curr_millis);
309 }
310 transition_manager.push_transition(transition);
311 if !property_data.inbound.contains(&time_id) {
312 should_reconnect_inbound = true;
313 property_data.inbound.push(time_id);
314 }
315 if !property_data.inbound.contains(&millis_id) {
316 should_reconnect_inbound = true;
317 property_data.inbound.push(millis_id);
318 }
319 });
320 if should_reconnect_inbound {
321 self.disconnect_inbound(id);
322 self.connect_inbound(id);
323 }
324 }
325
326 pub fn cancel_transitions<T: PropertyValue>(&self, id: PropertyId) {
327 let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
328 let (millis_id, curr_millis) =
329 PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
330 let has_transition = self.with_property_data(id, |property_data| {
331 property_data
332 .typed_data
333 .downcast_ref::<TypedPropertyData<T>>()
334 .expect("property type should match")
335 .transition_manager
336 .is_some()
337 });
338 if !has_transition {
339 return;
340 }
341
342 self.disconnect_inbound(id);
343 let value_changed = self.with_property_data_mut(id, |property_data| {
344 let typed_data = property_data.typed_data::<T>();
345 let current_value = typed_data
346 .transition_manager
347 .as_mut()
348 .and_then(|manager| manager.compute_eased_value(curr_time, curr_millis));
349 let value_changed = current_value.is_some();
350 if let Some(current_value) = current_value {
351 typed_data.value = current_value;
352 }
353 typed_data.transition_manager = None;
354 property_data
355 .inbound
356 .retain(|dependency| *dependency != time_id && *dependency != millis_id);
357 property_data.dirty = false;
358 value_changed
359 });
360 self.connect_inbound(id);
361
362 if value_changed {
363 self.dirtify_outbound(id);
364 }
365 }
366
367 fn with_entry_mut<V>(&self, id: PropertyId, f: impl FnOnce(&mut Entry) -> V) -> V {
373 let mut sm = self.property_map.borrow_mut();
374 let data = sm.get_mut(id).unwrap();
375 let return_value = f(data);
376 return_value
377 }
378
379 pub fn with_property_data_mut<V>(
384 &self,
385 id: PropertyId,
386 f: impl FnOnce(&mut PropertyData) -> V,
387 ) -> V {
388 let mut property_data = self.with_entry_mut(id, |entry| {
390 entry
391 .data
392 .take()
393 .expect("property data should not have already been taken")
394 });
395
396 let res = f(&mut property_data);
400
401 self.with_entry_mut(id, |entry| {
403 entry.data = Some(property_data);
404 });
405
406 res
407 }
408
409 pub fn with_property_data<V>(&self, id: PropertyId, f: impl FnOnce(&PropertyData) -> V) -> V {
414 self.with_property_data_mut(id, |property_data| f(&*property_data))
415 }
416
417 pub fn increase_ref_count(&self, id: PropertyId) -> usize {
419 self.with_entry_mut(id, |entry| {
420 entry.ref_count += 1;
421 entry.ref_count
422 })
423 }
424
425 pub fn decrease_ref_count(&self, id: PropertyId) -> usize {
427 self.with_entry_mut(id, |entry| {
428 entry.ref_count -= 1;
429 entry.ref_count
430 })
431 }
432
433 pub fn replace_property_keep_outbound_connections<T: PropertyValue>(
438 &self,
439 source_id: PropertyId,
440 target_id: PropertyId,
441 ) {
442 if source_id == target_id {
443 return;
444 }
445
446 self.disconnect_inbound(source_id);
449
450 self.with_property_data_mut(source_id, |source_property_data| {
452 self.with_property_data_mut(target_id, |target_property_data| {
453 source_property_data
455 .inbound
456 .clone_from(&target_property_data.inbound);
457 source_property_data.dirty = target_property_data.dirty
461 && !matches!(
462 target_property_data.typed_data::<T>().property_type,
463 PropertyType::Literal
464 );
465 source_property_data.cutoff_settler = target_property_data.cutoff_settler.clone();
466 source_property_data.owner_graph = target_property_data.owner_graph;
467 source_property_data.owner_scope = target_property_data.owner_scope.clone();
468 let source_typed = source_property_data.typed_data::<T>();
469 let target_typed = target_property_data.typed_data::<T>();
470 source_typed.value = target_typed.value.clone();
471 source_typed.property_type = target_typed.property_type.clone();
472 if target_property_data.dirty {
473 if let PropertyType::Computed {
474 cutoff: Some(cutoff),
475 ..
476 } = &mut source_typed.property_type
477 {
478 cutoff.initialized = false;
479 }
480 }
481 });
482 });
483
484 self.connect_inbound(source_id);
486
487 if self.is_registered_effect(source_id) && self.is_cutoff(source_id) {
488 panic!("cutoff properties cannot be registered as effects");
489 }
490 if self.is_cutoff(source_id) && self.is_dirty(source_id) {
491 self.enqueue_cutoff(source_id);
492 }
493
494 self.dirtify_outbound(source_id);
497
498 #[cfg(debug_assertions)]
499 {
500 let target_name = self.debug_name(target_id);
502 let mut names = self.debug_names.borrow_mut();
503 names.insert(source_id, format!("{}", target_name));
504 }
505
506 self.enqueue_effect_if_registered(source_id);
507 if !self.is_dirty(source_id) {
508 self.publish_stored::<T>(source_id);
509 }
510 }
511
512 pub fn update_value<T: PropertyValue>(&self, id: PropertyId) -> CutoffEvaluation {
514 let mut remove_dep_from_literal = false;
515 let mut publish_literal = false;
516 let evaluator = self.with_property_data_mut(id, |property_data| {
517 if property_data.dirty == false {
519 return None;
520 }
521 property_data.dirty = false;
522 let typed_data = property_data.typed_data::<T>();
523 match &mut typed_data.property_type {
524 PropertyType::Computed { evaluator, cutoff } => Some((
525 Rc::clone(&evaluator),
526 cutoff.as_ref().map(|cutoff| {
527 (
528 Rc::clone(&cutoff.predicate),
529 cutoff.initialized,
530 typed_data.value.clone(),
531 )
532 }),
533 )),
534 PropertyType::Literal => {
535 let tm = typed_data.transition_manager.as_mut()?;
536 let curr_time = PROPERTY_TIME.with_borrow(|time| time.get());
537 let curr_millis = PROPERTY_MILLIS.with_borrow(|time| time.get());
538 let value = tm.compute_eased_value(curr_time, curr_millis);
539 if let Some(interp_value) = value {
540 typed_data.value = interp_value;
541 publish_literal = true;
542 } else {
543 remove_dep_from_literal = true;
545 typed_data.transition_manager = None;
546 }
547 None
548 }
549 }
550 });
551
552 if remove_dep_from_literal {
553 self.disconnect_inbound(id);
554 self.with_property_data_mut(id, |property_data| {
555 property_data.inbound.clear();
556 });
557 }
558
559 if publish_literal {
560 self.publish_stored::<T>(id);
561 }
562
563 if let Some((evaluator, cutoff)) = evaluator {
564 let new_value = { evaluator() };
569 let cutoff_evaluation = match cutoff.as_ref() {
572 None => CutoffEvaluation::NotCutoff,
573 Some((_, false, _)) => CutoffEvaluation::Initialized,
574 Some((predicate, true, old_value)) if predicate(old_value, &new_value) => {
575 CutoffEvaluation::Suppressed
576 }
577 Some(_) => CutoffEvaluation::Propagated,
578 };
579 self.with_property_data_mut(id, |property_data| {
580 let typed_data = property_data.typed_data();
581 if let PropertyType::Computed {
582 cutoff: Some(cutoff),
583 ..
584 } = &mut typed_data.property_type
585 {
586 cutoff.initialized = true;
587 }
588
589 if cutoff_evaluation != CutoffEvaluation::Suppressed {
590 typed_data.value = new_value;
591 }
592 });
593
594 if cutoff_evaluation == CutoffEvaluation::Propagated {
595 self.dirtify_outbound(id);
596 }
597 if cutoff_evaluation != CutoffEvaluation::Suppressed {
598 self.publish_stored::<T>(id);
599 }
600 cutoff_evaluation
601 } else {
602 CutoffEvaluation::NotCutoff
603 }
604 }
605
606 pub fn remove_entry(&self, id: PropertyId) {
608 let res = {
609 self.unregister_effect(id);
610 self.queued_cutoff_set.borrow_mut().remove(&id);
611 self.disconnect_outbound(id);
612 self.disconnect_inbound(id);
613 let Ok(mut sm) = self.property_map.try_borrow_mut() else {
614 #[cfg(debug_assertions)]
615 panic!(
616 "failed to remove property \"{}\" - propertytable already borrowed",
617 self.debug_name(id),
618 );
619 #[cfg(not(debug_assertions))]
620 panic!("failed to remove property - propertytable already borrowed");
621 };
622 sm.remove(id).expect("tried to remove non-existent prop")
623 };
624 drop(res);
625 }
626
627 #[cfg(debug_assertions)]
629 pub fn debug_name(&self, id: PropertyId) -> String {
630 self.debug_names
631 .borrow()
632 .get(id)
633 .as_ref()
634 .map(|s| s.as_str())
635 .unwrap_or("<NO DEBUG NAME>")
636 .to_owned()
637 }
638
639 pub(crate) fn total_properties_count(&self) -> usize {
641 self.property_map.borrow().len()
642 }
643
644 pub(crate) fn register_effect(&self, id: PropertyId) {
645 self.register_effect_with_name(id, None);
646 }
647
648 pub(crate) fn register_effect_with_name(&self, id: PropertyId, debug_name: Option<&str>) {
649 if self.is_cutoff(id) {
650 panic!("cutoff properties cannot be registered as effects");
651 }
652 if let Some(debug_name) = debug_name {
653 self.effect_debug_names
654 .borrow_mut()
655 .insert(id, debug_name.to_owned());
656 }
657 self.effect_properties.borrow_mut().insert(id);
658 self.enqueue_effect_if_registered(id);
659 }
660
661 pub(crate) fn drain_effects(&self, max_iterations: usize) -> usize {
662 let mut work_ran = 0;
663 while work_ran < max_iterations {
664 if let Some(id) = self.pop_queued_cutoff() {
665 if !self.has_live_entry(id)
666 || !self.is_cutoff(id)
667 || !self.is_dirty(id)
668 || !self.work_enabled(id)
669 {
670 continue;
671 }
672 let settler = self.cutoff_settler(id);
673 settler(self, id);
674 work_ran += 1;
675 continue;
676 }
677
678 let Some(id) = self.pop_queued_effect() else {
679 break;
680 };
681 if !self.is_registered_effect(id) || !self.has_live_entry(id) {
682 continue;
683 }
684 let dirty = self.with_property_data(id, |property_data| property_data.dirty);
685 if !self.work_enabled(id) {
686 continue;
687 }
688 if dirty {
689 self.update_value::<()>(id);
690 work_ran += 1;
691 }
692 }
693 work_ran
694 }
695
696 pub(crate) fn drain_effects_with_report(&self, max_iterations: usize) -> EffectDrainReport {
697 let mut report = EffectDrainReport::default();
698 let mut effect_counts = HashMap::new();
699 let mut cutoff_counts = HashMap::new();
700
701 let mut work_ran = 0;
702 while work_ran < max_iterations {
703 if let Some(id) = self.pop_queued_cutoff() {
704 if !self.has_live_entry(id)
705 || !self.is_cutoff(id)
706 || !self.is_dirty(id)
707 || !self.work_enabled(id)
708 {
709 continue;
710 }
711 let cutoff_name = self.debug_name_for_diagnostics(id);
712 let cutoff_category = cutoff_name
713 .split_once(" (")
714 .map(|(category, _)| category)
715 .unwrap_or(&cutoff_name)
716 .to_owned();
717 let settler = self.cutoff_settler(id);
718 match settler(self, id) {
719 CutoffEvaluation::Suppressed => report.cutoffs_suppressed += 1,
720 CutoffEvaluation::Propagated => report.cutoffs_propagated += 1,
721 CutoffEvaluation::NotCutoff | CutoffEvaluation::Initialized => {}
722 }
723 report.cutoffs_evaluated += 1;
724 work_ran += 1;
725 *cutoff_counts.entry(cutoff_category).or_insert(0) += 1;
726 continue;
727 }
728
729 let Some(id) = self.pop_queued_effect() else {
730 break;
731 };
732 report.popped += 1;
733
734 if !self.is_registered_effect(id) {
735 report.skipped_unregistered += 1;
736 continue;
737 }
738 if !self.has_live_entry(id) {
739 report.skipped_missing += 1;
740 continue;
741 }
742
743 if !self.work_enabled(id) {
744 continue;
745 }
746 let dirty = self.with_property_data(id, |property_data| property_data.dirty);
747 if dirty {
748 let effect_name = self.debug_name_for_diagnostics(id);
749 self.update_value::<()>(id);
750 report.ran += 1;
751 work_ran += 1;
752 *effect_counts.entry(effect_name).or_insert(0) += 1;
753 } else {
754 report.skipped_clean += 1;
755 }
756 }
757
758 report.remaining = self.queued_effects.borrow().len();
759 report.remaining_cutoffs = self.queued_cutoffs.borrow().len();
760 report.budget_exhausted =
761 work_ran == max_iterations && (report.remaining > 0 || report.remaining_cutoffs > 0);
762 report.top_effects = effect_counts.into_iter().collect();
763 report
764 .top_effects
765 .sort_by(|(left_name, left_count), (right_name, right_count)| {
766 right_count
767 .cmp(left_count)
768 .then_with(|| left_name.cmp(right_name))
769 });
770 report.top_effects.truncate(8);
771 report.top_cutoffs = cutoff_counts.into_iter().collect();
772 report
773 .top_cutoffs
774 .sort_by(|(left_name, left_count), (right_name, right_count)| {
775 right_count
776 .cmp(left_count)
777 .then_with(|| left_name.cmp(right_name))
778 });
779 report.top_cutoffs.truncate(8);
780 report
781 }
782
783 pub(crate) fn enqueue_effect_if_registered(&self, id: PropertyId) {
784 if self.is_registered_effect(id) {
785 self.enqueue_effect(id);
786 }
787 }
788
789 fn enqueue_effect(&self, id: PropertyId) {
790 let mut queued_set = self.queued_effect_set.borrow_mut();
791 if queued_set.insert(id) {
792 self.queued_effects.borrow_mut().push_back(id);
793 }
794 }
795
796 fn pop_queued_effect(&self) -> Option<PropertyId> {
797 let id = self.pop_owned_work(&self.queued_effects)?;
798 self.queued_effect_set.borrow_mut().remove(&id);
799 Some(id)
800 }
801
802 pub(crate) fn enqueue_cutoff(&self, id: PropertyId) {
803 let mut queued_set = self.queued_cutoff_set.borrow_mut();
804 if queued_set.insert(id) {
805 self.queued_cutoffs.borrow_mut().push_back(id);
806 }
807 }
808
809 fn pop_queued_cutoff(&self) -> Option<PropertyId> {
810 let id = self.pop_owned_work(&self.queued_cutoffs)?;
811 self.queued_cutoff_set.borrow_mut().remove(&id);
812 Some(id)
813 }
814
815 fn pop_owned_work(&self, queue: &RefCell<VecDeque<PropertyId>>) -> Option<PropertyId> {
816 let owner = super::shared_graph::current_graph_id();
817 let mut queue = queue.borrow_mut();
818 let position = queue.iter().position(|id| {
819 let map = self.property_map.borrow();
820 let graph = map
821 .get(*id)
822 .and_then(|entry| entry.data.as_ref())
823 .and_then(|data| data.owner_graph);
824 owner.is_none() || graph.is_none() || graph == owner
825 })?;
826 queue.remove(position)
827 }
828
829 fn work_enabled(&self, id: PropertyId) -> bool {
830 self.with_property_data(id, |data| {
831 data.owner_scope.as_ref().map_or(true, |scope| {
832 scope.upgrade().is_some_and(|scope| !scope.closed.get())
833 })
834 })
835 }
836
837 fn unregister_effect(&self, id: PropertyId) {
838 self.effect_properties.borrow_mut().remove(&id);
839 self.effect_debug_names.borrow_mut().remove(&id);
840 self.queued_effect_set.borrow_mut().remove(&id);
841 }
842
843 fn is_registered_effect(&self, id: PropertyId) -> bool {
844 self.effect_properties.borrow().contains(&id)
845 }
846
847 pub(crate) fn is_cutoff(&self, id: PropertyId) -> bool {
848 self.with_property_data(id, |data| data.cutoff_settler.is_some())
849 }
850
851 fn is_dirty(&self, id: PropertyId) -> bool {
852 self.with_property_data(id, |data| data.dirty)
853 }
854
855 fn cutoff_settler(
856 &self,
857 id: PropertyId,
858 ) -> Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation> {
859 self.with_property_data(id, |data| {
860 data.cutoff_settler
861 .as_ref()
862 .expect("queued cutoff property must have a settler")
863 .clone()
864 })
865 }
866
867 pub(super) fn has_live_entry(&self, id: PropertyId) -> bool {
868 self.property_map
869 .borrow()
870 .get(id)
871 .and_then(|entry| entry.data.as_ref())
872 .is_some()
873 }
874
875 fn debug_name_for_diagnostics(&self, id: PropertyId) -> String {
876 if let Some(name) = self.effect_debug_names.borrow().get(&id) {
877 return name.clone();
878 }
879
880 #[cfg(debug_assertions)]
881 {
882 if let Some(name) = self.debug_names.borrow().get(id) {
883 return name.clone();
884 }
885 }
886
887 format!("{id:?}")
888 }
889
890 pub(crate) fn outbound_debug_names(&self, id: PropertyId) -> Vec<String> {
891 self.with_property_data(id, |data| {
892 data.outbound
893 .iter()
894 .copied()
895 .filter(|outbound_id| self.has_live_entry(*outbound_id))
896 .map(|outbound_id| self.debug_name_for_diagnostics(outbound_id))
897 .collect()
898 })
899 }
900
901 pub(crate) fn has_direct_outbound(
902 &self,
903 source_id: PropertyId,
904 outbound_id: PropertyId,
905 ) -> bool {
906 self.with_property_data(source_id, |data| data.outbound.contains(&outbound_id))
907 }
908}