1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 ApplicationSemanticModel, CapabilityOperationId, EffectComputedPrerequisiteBatch,
5 EffectDeclaration, EffectExecutionBatch, EffectExecutionPolicy, EffectRenderBoundary,
6 ExecutionBoundary, IntermediateRepresentation, SemanticId, SourceProvenance,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct RuntimeEffectRegistry {
15 pub records: BTreeMap<SemanticId, RuntimeEffectRecord>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct RuntimeEffectRecord {
21 pub effect: SemanticId,
22 pub execution_function: SemanticId,
23 pub initial_trigger_policy: EffectExecutionPolicy,
24 pub initial_trigger: Option<RuntimeInitialEffectTrigger>,
25 pub action_batch_triggers: Vec<RuntimeActionBatchEffectTrigger>,
26 pub capability_operations: Vec<CapabilityOperationId>,
27 pub execution_boundary: ExecutionBoundary,
28 pub declaration_order: Option<u32>,
30 pub run_on_resume: bool,
33 pub provenance: SourceProvenance,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct RuntimeInitialEffectTrigger {
39 pub render_boundary: EffectRenderBoundary,
40 pub required_computed: Vec<SemanticId>,
41 pub prerequisite_batches: Vec<EffectComputedPrerequisiteBatch>,
42 pub effect_batch_index: u32,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct RuntimeActionBatchEffectTrigger {
48 pub action_batch: SemanticId,
49 pub matched_states: Vec<SemanticId>,
50 pub required_computed: Vec<SemanticId>,
51 pub prerequisite_batches: Vec<EffectComputedPrerequisiteBatch>,
52 pub effect_batch_index: u32,
53}
54
55impl RuntimeEffectRegistry {
56 #[must_use]
57 pub fn record(&self, effect: &SemanticId) -> Option<&RuntimeEffectRecord> {
58 self.records.get(effect)
59 }
60}
61
62#[must_use]
68pub fn build_runtime_effect_registry(
69 model: &ApplicationSemanticModel,
70 ir: &IntermediateRepresentation,
71) -> RuntimeEffectRegistry {
72 let executions = ir
73 .modules
74 .iter()
75 .flat_map(|module| &module.effect_executions)
76 .map(|execution| (execution.effect.clone(), execution))
77 .collect::<BTreeMap<_, _>>();
78 let records = executions
79 .into_iter()
80 .filter_map(|(effect_id, execution)| {
81 let effect = model.effects.get(&effect_id)?;
82 let computed_dependencies = effect_computed_dependencies(model, &effect_id);
83 Some((
84 effect_id.clone(),
85 RuntimeEffectRecord {
86 effect: effect_id.clone(),
87 execution_function: execution.function.clone(),
88 initial_trigger_policy: effect.execution_policy,
89 initial_trigger: initial_trigger(model, &effect_id, &computed_dependencies),
90 action_batch_triggers: action_batch_triggers(
91 model,
92 &effect_id,
93 &computed_dependencies,
94 ),
95 capability_operations: execution.capability_operations.clone(),
96 execution_boundary: effect.execution_boundary,
97 declaration_order: effect.declaration_order,
98 run_on_resume: matches!(effect.declaration, EffectDeclaration::V2Field),
99 provenance: effect.provenance.clone(),
100 },
101 ))
102 })
103 .collect();
104
105 RuntimeEffectRegistry { records }
106}
107
108fn effect_computed_dependencies(
109 model: &ApplicationSemanticModel,
110 effect: &SemanticId,
111) -> BTreeSet<SemanticId> {
112 model
113 .effect_reactive_analysis
114 .get(effect)
115 .into_iter()
116 .flat_map(|analysis| &analysis.dependencies)
117 .filter(|dependency| model.computed_values.contains_key(*dependency))
118 .cloned()
119 .collect()
120}
121
122fn initial_trigger(
123 model: &ApplicationSemanticModel,
124 effect: &SemanticId,
125 computed_dependencies: &BTreeSet<SemanticId>,
126) -> Option<RuntimeInitialEffectTrigger> {
127 let initial = &model.effect_execution_plan.initial;
128 let effect_batch_index = effect_batch_index(&initial.effect_batches, effect)?;
129 let render_boundary = initial.render_boundary?;
130 let required_computed =
131 select_required_computed(&initial.required_computed, computed_dependencies);
132 Some(RuntimeInitialEffectTrigger {
133 render_boundary,
134 prerequisite_batches: select_prerequisite_batches(
135 &initial.prerequisite_batches,
136 &required_computed,
137 ),
138 required_computed,
139 effect_batch_index,
140 })
141}
142
143fn action_batch_triggers(
144 model: &ApplicationSemanticModel,
145 effect: &SemanticId,
146 computed_dependencies: &BTreeSet<SemanticId>,
147) -> Vec<RuntimeActionBatchEffectTrigger> {
148 let trigger_evidence = model
149 .effect_trigger_plan
150 .action_batch_triggers
151 .iter()
152 .filter_map(|trigger| {
153 trigger
154 .matched_states
155 .get(effect)
156 .map(|states| (trigger.action_batch.clone(), states.clone()))
157 })
158 .collect::<BTreeMap<_, _>>();
159 let mut triggers = model
160 .effect_execution_plan
161 .actions
162 .iter()
163 .filter_map(|action| {
164 let effect_batch_index = effect_batch_index(&action.effect_batches, effect)?;
165 let matched_states = trigger_evidence.get(&action.action_batch)?.clone();
166 let required_computed =
167 select_required_computed(&action.required_computed, computed_dependencies);
168 Some(RuntimeActionBatchEffectTrigger {
169 action_batch: action.action_batch.clone(),
170 matched_states,
171 prerequisite_batches: select_prerequisite_batches(
172 &action.prerequisite_batches,
173 &required_computed,
174 ),
175 required_computed,
176 effect_batch_index,
177 })
178 })
179 .collect::<Vec<_>>();
180 triggers.sort_by(|left, right| left.action_batch.cmp(&right.action_batch));
181 triggers
182}
183
184fn effect_batch_index(batches: &[EffectExecutionBatch], effect: &SemanticId) -> Option<u32> {
185 batches
186 .iter()
187 .find(|batch| batch.effects.contains(effect))
188 .map(|batch| batch.index)
189}
190
191fn select_required_computed(
192 scheduled: &[SemanticId],
193 dependencies: &BTreeSet<SemanticId>,
194) -> Vec<SemanticId> {
195 scheduled
196 .iter()
197 .filter(|computed| dependencies.contains(*computed))
198 .cloned()
199 .collect()
200}
201
202fn select_prerequisite_batches(
203 batches: &[EffectComputedPrerequisiteBatch],
204 required_computed: &[SemanticId],
205) -> Vec<EffectComputedPrerequisiteBatch> {
206 let required = required_computed.iter().collect::<BTreeSet<_>>();
207 batches
208 .iter()
209 .filter_map(|batch| {
210 let computed = batch
211 .computed
212 .iter()
213 .filter(|computed| required.contains(computed))
214 .cloned()
215 .collect::<Vec<_>>();
216 (!computed.is_empty()).then_some(EffectComputedPrerequisiteBatch {
217 source_batch_index: batch.source_batch_index,
218 computed,
219 })
220 })
221 .collect()
222}
223
224#[cfg(test)]
225mod tests {
226 use crate::{
227 build_application_semantic_model, build_runtime_effect_registry, lower_components_to_ir,
228 optimize_effect_ir, CapabilityOperationId, EffectExecutionPolicy, EffectRenderBoundary,
229 ExecutionBoundary,
230 };
231
232 #[test]
233 #[allow(clippy::too_many_lines)]
234 fn builds_deterministic_runtime_records_from_effect_plans_and_ir() {
235 let parsed = presolve_parser::parse_file(
236 "src/RuntimeEffect.tsx",
237 r#"
238@component("x-runtime-effect")
239class RuntimeEffect extends Component {
240 price = state(1);
241 locale = state("en-US");
242 theme = state("light");
243
244 @computed()
245 get subtotal() { return this.price * 2; }
246
247 @computed()
248 get total() { return this.subtotal + 1; }
249
250 @computed()
251 get currentLocale() { return this.locale; }
252
253 @action()
254 increment() { this.price += 1; }
255
256 @effect()
257 report() { console.log(this.total, this.currentLocale); }
258
259 @effect()
260 audit() { console.log(this.price); }
261
262 @effect()
263 bootLog() { console.log("ready"); }
264
265 @action()
266 invalidAction() { this.price += 1; }
267
268 @effect()
269 invalid() { this.invalidAction(); }
270
271 render() { return <p />; }
272}
273"#,
274 );
275 let model = build_application_semantic_model(&parsed);
276 let component = &model.components[0];
277 let price = component.id.state_field("price");
278 let subtotal = component.id.computed("subtotal");
279 let total = component.id.computed("total");
280 let current_locale = component.id.computed("currentLocale");
281 let report = component.id.effect("report");
282 let audit = component.id.effect("audit");
283 let boot_log = component.id.effect("bootLog");
284 let invalid = component.id.effect("invalid");
285 let increment = component.id.action_batch("increment");
286 let registry = build_runtime_effect_registry(
287 &model,
288 &optimize_effect_ir(&lower_components_to_ir(&model)).output,
289 );
290 let report_record = registry.record(&report).expect("report record");
291 let audit_record = registry.record(&audit).expect("audit record");
292 let boot_log_record = registry.record(&boot_log).expect("boot log record");
293 let initial = report_record
294 .initial_trigger
295 .as_ref()
296 .expect("initial report trigger");
297 let action = report_record
298 .action_batch_triggers
299 .iter()
300 .find(|trigger| trigger.action_batch == increment)
301 .expect("increment report trigger");
302
303 assert_eq!(registry.records.len(), 3);
304 assert_eq!(
305 registry.records.keys().cloned().collect::<Vec<_>>(),
306 vec![audit.clone(), boot_log, report.clone()]
307 );
308 assert!(registry.record(&invalid).is_none());
309 assert_eq!(report_record.effect, report);
310 assert_eq!(report_record.execution_function, report_record.effect);
311 assert_eq!(
312 report_record.initial_trigger_policy,
313 EffectExecutionPolicy::AfterInitialRenderAndCompletedActionBatch
314 );
315 assert_eq!(report_record.execution_boundary, ExecutionBoundary::Client);
316 assert!(!report_record.run_on_resume);
317 assert_eq!(
318 initial.render_boundary,
319 EffectRenderBoundary::AfterInitialRender
320 );
321 assert_eq!(
322 initial.required_computed,
323 vec![current_locale, subtotal.clone(), total.clone()]
324 );
325 assert_eq!(
326 initial
327 .prerequisite_batches
328 .iter()
329 .map(|batch| batch.computed.clone())
330 .collect::<Vec<_>>(),
331 vec![
332 vec![component.id.computed("currentLocale"), subtotal.clone()],
333 vec![total.clone()]
334 ]
335 );
336 assert_eq!(initial.effect_batch_index, 0);
337 assert_eq!(action.matched_states, vec![price.clone()]);
338 assert_eq!(action.required_computed, vec![subtotal, total]);
339 assert_eq!(
340 action
341 .prerequisite_batches
342 .iter()
343 .map(|batch| batch.computed.clone())
344 .collect::<Vec<_>>(),
345 vec![
346 vec![component.id.computed("subtotal")],
347 vec![component.id.computed("total")]
348 ]
349 );
350 assert_eq!(action.effect_batch_index, 0);
351 assert_eq!(
352 report_record.capability_operations,
353 vec![CapabilityOperationId("builtin.browser.console.log")]
354 );
355 assert_eq!(
356 audit_record
357 .initial_trigger
358 .as_ref()
359 .expect("initial audit")
360 .required_computed,
361 Vec::new()
362 );
363 assert_eq!(
364 audit_record.action_batch_triggers[0].matched_states,
365 vec![price]
366 );
367 assert!(boot_log_record.action_batch_triggers.is_empty());
368 assert_eq!(
369 boot_log_record
370 .initial_trigger
371 .as_ref()
372 .expect("initial boot log")
373 .required_computed,
374 Vec::new()
375 );
376 assert_eq!(
377 report_record.provenance,
378 model
379 .effect(&report_record.effect)
380 .expect("effect")
381 .provenance
382 );
383 }
384}