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