presolve_compiler/
context_update.rs1use std::collections::BTreeSet;
2
3use crate::{
4 ApplicationSemanticModel, ContextSourcePlanEntry, ContextSourcePlanStatus,
5 ContextValueSourceId, OptimizedContextIrReport, SemanticId,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ContextActionUpdatePlan {
11 pub action_batch: SemanticId,
12 pub invalidated_sources: Vec<ContextValueSourceId>,
13 pub prerequisite_computed_batches: Vec<u32>,
14 pub source_evaluation_batches: Vec<crate::ContextEvaluationBatchId>,
15 pub affected_consumers: Vec<crate::ConsumerId>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct ContextUpdatePlan {
22 pub actions: Vec<ContextActionUpdatePlan>,
23}
24
25#[must_use]
28pub fn build_context_update_plan(
29 model: &ApplicationSemanticModel,
30 optimized: &OptimizedContextIrReport,
31) -> ContextUpdatePlan {
32 let planned = optimized
33 .source_evaluations
34 .iter()
35 .filter_map(|evaluation| {
36 model
37 .context_evaluation
38 .context_source_plan(&evaluation.source)
39 .filter(|entry| entry.status == ContextSourcePlanStatus::Planned)
40 .map(|entry| (evaluation, entry))
41 })
42 .collect::<Vec<_>>();
43 let actions = model
44 .effect_trigger_plan
45 .action_batches
46 .values()
47 .map(|action| {
48 let written = action.written_states.iter().collect::<BTreeSet<_>>();
49 let invalidated_sources = planned
50 .iter()
51 .filter(|(_, entry)| matches!(entry.source, ContextValueSourceId::Provider(_)))
52 .filter(|(_, entry)| source_depends_on_written_state(model, entry, &written))
53 .map(|(evaluation, _)| evaluation.source.clone())
54 .collect::<Vec<_>>();
55 let invalidated = invalidated_sources.iter().collect::<BTreeSet<_>>();
56 let prerequisite_computed_batches = planned
57 .iter()
58 .filter(|(evaluation, _)| invalidated.contains(&evaluation.source))
59 .flat_map(|(evaluation, _)| {
60 evaluation.prerequisite_computed_batches.iter().copied()
61 })
62 .collect::<BTreeSet<_>>()
63 .into_iter()
64 .collect();
65 let source_evaluation_batches = model
66 .context_evaluation
67 .evaluation_batches
68 .iter()
69 .filter(|batch| {
70 batch
71 .sources
72 .iter()
73 .any(|source| invalidated.contains(source))
74 })
75 .map(|batch| batch.id.clone())
76 .collect();
77 let affected_consumers = optimized
78 .optimized_module
79 .context_ir
80 .consumer_bindings
81 .iter()
82 .filter(|binding| invalidated.contains(&binding.source))
83 .map(|binding| binding.consumer.clone())
84 .collect();
85 ContextActionUpdatePlan {
86 action_batch: action.id.clone(),
87 invalidated_sources,
88 prerequisite_computed_batches,
89 source_evaluation_batches,
90 affected_consumers,
91 }
92 })
93 .collect();
94 ContextUpdatePlan { actions }
95}
96
97fn source_depends_on_written_state(
98 model: &ApplicationSemanticModel,
99 entry: &ContextSourcePlanEntry,
100 written: &BTreeSet<&SemanticId>,
101) -> bool {
102 entry
103 .required_state
104 .iter()
105 .any(|state| written.contains(state))
106 || entry.required_computed.iter().any(|computed| {
107 model
108 .reactive_transitive_analysis
109 .dependencies_of(computed.as_str())
110 .iter()
111 .any(|dependency| written.iter().any(|state| state.as_str() == dependency))
112 })
113}
114
115#[cfg(test)]
116mod tests {
117 use crate::{
118 build_application_semantic_model, build_context_update_plan, lower_components_to_ir,
119 optimize_context_ir, ContextValueSourceId, ProviderId,
120 };
121
122 #[test]
123 fn plans_each_reactive_provider_once_per_matching_action_batch() {
124 let model = build_application_semantic_model(&presolve_parser::parse_file(
125 "src/App.tsx",
126 r#"
127@component("x-app")
128class App extends Component {
129 count = state(1);
130 other = state(0);
131 @computed() get doubled(): number { return this.count * 2; }
132 @context() total!: number;
133 @provide(App.total) providedTotal: number = this.doubled;
134 @consume(App.total) total!: number;
135 @action() increment() { this.count += 1; this.count += 1; }
136 @action() ignore() { this.other += 1; }
137 render() { return <main />; }
138}
139"#,
140 ));
141 let component = &model.components[0].id;
142 let source =
143 ContextValueSourceId::Provider(ProviderId::for_component(component, "providedTotal"));
144 let plan = build_context_update_plan(
145 &model,
146 &optimize_context_ir(&lower_components_to_ir(&model)),
147 );
148 let increment = plan
149 .actions
150 .iter()
151 .find(|action| action.action_batch == component.action_batch("increment"))
152 .expect("increment plan");
153 let ignore = plan
154 .actions
155 .iter()
156 .find(|action| action.action_batch == component.action_batch("ignore"))
157 .expect("ignore plan");
158 assert_eq!(increment.invalidated_sources, vec![source]);
159 assert!(increment.affected_consumers.len() == 1);
160 assert!(ignore.invalidated_sources.is_empty());
161 }
162}