use std::collections::{BTreeMap, BTreeSet};
use crate::{
ApplicationSemanticModel, CapabilityOperationId, EffectComputedPrerequisiteBatch,
EffectExecutionBatch, EffectExecutionPolicy, EffectRenderBoundary, ExecutionBoundary,
IntermediateRepresentation, SemanticId, SourceProvenance,
};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeEffectRegistry {
pub records: BTreeMap<SemanticId, RuntimeEffectRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeEffectRecord {
pub effect: SemanticId,
pub execution_function: SemanticId,
pub initial_trigger_policy: EffectExecutionPolicy,
pub initial_trigger: Option<RuntimeInitialEffectTrigger>,
pub action_batch_triggers: Vec<RuntimeActionBatchEffectTrigger>,
pub capability_operations: Vec<CapabilityOperationId>,
pub execution_boundary: ExecutionBoundary,
pub provenance: SourceProvenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeInitialEffectTrigger {
pub render_boundary: EffectRenderBoundary,
pub required_computed: Vec<SemanticId>,
pub prerequisite_batches: Vec<EffectComputedPrerequisiteBatch>,
pub effect_batch_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeActionBatchEffectTrigger {
pub action_batch: SemanticId,
pub matched_states: Vec<SemanticId>,
pub required_computed: Vec<SemanticId>,
pub prerequisite_batches: Vec<EffectComputedPrerequisiteBatch>,
pub effect_batch_index: u32,
}
impl RuntimeEffectRegistry {
#[must_use]
pub fn record(&self, effect: &SemanticId) -> Option<&RuntimeEffectRecord> {
self.records.get(effect)
}
}
#[must_use]
pub fn build_runtime_effect_registry(
model: &ApplicationSemanticModel,
ir: &IntermediateRepresentation,
) -> RuntimeEffectRegistry {
let executions = ir
.modules
.iter()
.flat_map(|module| &module.effect_executions)
.map(|execution| (execution.effect.clone(), execution))
.collect::<BTreeMap<_, _>>();
let records = executions
.into_iter()
.filter_map(|(effect_id, execution)| {
let effect = model.effects.get(&effect_id)?;
let computed_dependencies = effect_computed_dependencies(model, &effect_id);
Some((
effect_id.clone(),
RuntimeEffectRecord {
effect: effect_id.clone(),
execution_function: execution.function.clone(),
initial_trigger_policy: effect.execution_policy,
initial_trigger: initial_trigger(model, &effect_id, &computed_dependencies),
action_batch_triggers: action_batch_triggers(
model,
&effect_id,
&computed_dependencies,
),
capability_operations: execution.capability_operations.clone(),
execution_boundary: effect.execution_boundary,
provenance: effect.provenance.clone(),
},
))
})
.collect();
RuntimeEffectRegistry { records }
}
fn effect_computed_dependencies(
model: &ApplicationSemanticModel,
effect: &SemanticId,
) -> BTreeSet<SemanticId> {
model
.effect_reactive_analysis
.get(effect)
.into_iter()
.flat_map(|analysis| &analysis.dependencies)
.filter(|dependency| model.computed_values.contains_key(*dependency))
.cloned()
.collect()
}
fn initial_trigger(
model: &ApplicationSemanticModel,
effect: &SemanticId,
computed_dependencies: &BTreeSet<SemanticId>,
) -> Option<RuntimeInitialEffectTrigger> {
let initial = &model.effect_execution_plan.initial;
let effect_batch_index = effect_batch_index(&initial.effect_batches, effect)?;
let render_boundary = initial.render_boundary?;
let required_computed =
select_required_computed(&initial.required_computed, computed_dependencies);
Some(RuntimeInitialEffectTrigger {
render_boundary,
prerequisite_batches: select_prerequisite_batches(
&initial.prerequisite_batches,
&required_computed,
),
required_computed,
effect_batch_index,
})
}
fn action_batch_triggers(
model: &ApplicationSemanticModel,
effect: &SemanticId,
computed_dependencies: &BTreeSet<SemanticId>,
) -> Vec<RuntimeActionBatchEffectTrigger> {
let trigger_evidence = model
.effect_trigger_plan
.action_batch_triggers
.iter()
.filter_map(|trigger| {
trigger
.matched_states
.get(effect)
.map(|states| (trigger.action_batch.clone(), states.clone()))
})
.collect::<BTreeMap<_, _>>();
let mut triggers = model
.effect_execution_plan
.actions
.iter()
.filter_map(|action| {
let effect_batch_index = effect_batch_index(&action.effect_batches, effect)?;
let matched_states = trigger_evidence.get(&action.action_batch)?.clone();
let required_computed =
select_required_computed(&action.required_computed, computed_dependencies);
Some(RuntimeActionBatchEffectTrigger {
action_batch: action.action_batch.clone(),
matched_states,
prerequisite_batches: select_prerequisite_batches(
&action.prerequisite_batches,
&required_computed,
),
required_computed,
effect_batch_index,
})
})
.collect::<Vec<_>>();
triggers.sort_by(|left, right| left.action_batch.cmp(&right.action_batch));
triggers
}
fn effect_batch_index(batches: &[EffectExecutionBatch], effect: &SemanticId) -> Option<u32> {
batches
.iter()
.find(|batch| batch.effects.contains(effect))
.map(|batch| batch.index)
}
fn select_required_computed(
scheduled: &[SemanticId],
dependencies: &BTreeSet<SemanticId>,
) -> Vec<SemanticId> {
scheduled
.iter()
.filter(|computed| dependencies.contains(*computed))
.cloned()
.collect()
}
fn select_prerequisite_batches(
batches: &[EffectComputedPrerequisiteBatch],
required_computed: &[SemanticId],
) -> Vec<EffectComputedPrerequisiteBatch> {
let required = required_computed.iter().collect::<BTreeSet<_>>();
batches
.iter()
.filter_map(|batch| {
let computed = batch
.computed
.iter()
.filter(|computed| required.contains(computed))
.cloned()
.collect::<Vec<_>>();
(!computed.is_empty()).then_some(EffectComputedPrerequisiteBatch {
source_batch_index: batch.source_batch_index,
computed,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::{
build_application_semantic_model, build_runtime_effect_registry, lower_components_to_ir,
optimize_effect_ir, CapabilityOperationId, EffectExecutionPolicy, EffectRenderBoundary,
ExecutionBoundary,
};
#[test]
#[allow(clippy::too_many_lines)]
fn builds_deterministic_runtime_records_from_effect_plans_and_ir() {
let parsed = presolve_parser::parse_file(
"src/RuntimeEffect.tsx",
r#"
@component("x-runtime-effect")
class RuntimeEffect extends Component {
price = state(1);
locale = state("en-US");
theme = state("light");
@computed()
get subtotal() { return this.price * 2; }
@computed()
get total() { return this.subtotal + 1; }
@computed()
get currentLocale() { return this.locale; }
@action()
increment() { this.price += 1; }
@effect()
report() { console.log(this.total, this.currentLocale); }
@effect()
audit() { console.log(this.price); }
@effect()
bootLog() { console.log("ready"); }
@action()
invalidAction() { this.price += 1; }
@effect()
invalid() { this.invalidAction(); }
render() { return <p />; }
}
"#,
);
let model = build_application_semantic_model(&parsed);
let component = &model.components[0];
let price = component.id.state_field("price");
let subtotal = component.id.computed("subtotal");
let total = component.id.computed("total");
let current_locale = component.id.computed("currentLocale");
let report = component.id.effect("report");
let audit = component.id.effect("audit");
let boot_log = component.id.effect("bootLog");
let invalid = component.id.effect("invalid");
let increment = component.id.action_batch("increment");
let registry = build_runtime_effect_registry(
&model,
&optimize_effect_ir(&lower_components_to_ir(&model)).output,
);
let report_record = registry.record(&report).expect("report record");
let audit_record = registry.record(&audit).expect("audit record");
let boot_log_record = registry.record(&boot_log).expect("boot log record");
let initial = report_record
.initial_trigger
.as_ref()
.expect("initial report trigger");
let action = report_record
.action_batch_triggers
.iter()
.find(|trigger| trigger.action_batch == increment)
.expect("increment report trigger");
assert_eq!(registry.records.len(), 3);
assert_eq!(
registry.records.keys().cloned().collect::<Vec<_>>(),
vec![audit.clone(), boot_log, report.clone()]
);
assert!(registry.record(&invalid).is_none());
assert_eq!(report_record.effect, report);
assert_eq!(report_record.execution_function, report_record.effect);
assert_eq!(
report_record.initial_trigger_policy,
EffectExecutionPolicy::AfterInitialRenderAndCompletedActionBatch
);
assert_eq!(report_record.execution_boundary, ExecutionBoundary::Client);
assert_eq!(
initial.render_boundary,
EffectRenderBoundary::AfterInitialRender
);
assert_eq!(
initial.required_computed,
vec![current_locale, subtotal.clone(), total.clone()]
);
assert_eq!(
initial
.prerequisite_batches
.iter()
.map(|batch| batch.computed.clone())
.collect::<Vec<_>>(),
vec![
vec![component.id.computed("currentLocale"), subtotal.clone()],
vec![total.clone()]
]
);
assert_eq!(initial.effect_batch_index, 0);
assert_eq!(action.matched_states, vec![price.clone()]);
assert_eq!(action.required_computed, vec![subtotal, total]);
assert_eq!(
action
.prerequisite_batches
.iter()
.map(|batch| batch.computed.clone())
.collect::<Vec<_>>(),
vec![
vec![component.id.computed("subtotal")],
vec![component.id.computed("total")]
]
);
assert_eq!(action.effect_batch_index, 0);
assert_eq!(
report_record.capability_operations,
vec![CapabilityOperationId("builtin.browser.console.log")]
);
assert_eq!(
audit_record
.initial_trigger
.as_ref()
.expect("initial audit")
.required_computed,
Vec::new()
);
assert_eq!(
audit_record.action_batch_triggers[0].matched_states,
vec![price]
);
assert!(boot_log_record.action_batch_triggers.is_empty());
assert_eq!(
boot_log_record
.initial_trigger
.as_ref()
.expect("initial boot log")
.required_computed,
Vec::new()
);
assert_eq!(
report_record.provenance,
model
.effect(&report_record.effect)
.expect("effect")
.provenance
);
}
}