Skip to main content

presolve_compiler/
resume_plan.rs

1use crate::application_semantic_model::ApplicationSemanticModel;
2use crate::component_graph::render_event_handlers;
3use crate::semantic_id::SemanticId;
4use crate::{
5    build_context_resume_plan, build_context_update_plan, build_effect_resume_plan,
6    build_runtime_computed_registry, build_runtime_context_registry, build_runtime_effect_registry,
7    lower_components_to_ir, optimize_context_ir, optimize_effect_ir, ContextResumePlan,
8    EffectResumePlan,
9};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ResumePlan {
14    pub components: Vec<ResumeComponentPlan>,
15    pub effects: EffectResumePlan,
16    pub contexts: ContextResumePlan,
17    pub component_instances: Vec<ComponentInstanceResumePlan>,
18    pub structural_regions: Vec<StructuralRegionResumePlan>,
19    pub slot_bindings: Vec<SlotBindingResumePlan>,
20    /// I15 planning metadata only. It deliberately carries no live browser
21    /// state and Phase J remains the sole restoration authority.
22    pub form_instances: Vec<FormInstanceResumePlan>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct FormInstanceResumePlan {
27    pub form_instance: String,
28    pub form: String,
29    pub component_instance: String,
30    pub fields: Vec<FormFieldResumePlan>,
31    pub aggregate_validation_slot: String,
32    pub submission_slot: String,
33    pub serializable: bool,
34    pub pending_validation_status: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct FormFieldResumePlan {
39    pub field: String,
40    pub value_slot: String,
41    pub dirty_slot: String,
42    pub touched_slot: String,
43    pub validation_slot: String,
44    pub serializable: bool,
45}
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct ComponentInstanceResumePlan {
48    pub instance: String,
49    pub resume_id: String,
50    pub component: String,
51    pub parent_instance: Option<String>,
52    pub active_status: String,
53    pub structural_region: Option<String>,
54}
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct StructuralRegionResumePlan {
57    pub region: String,
58    pub resume_id: String,
59    pub active_status: String,
60}
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct SlotBindingResumePlan {
63    pub binding: String,
64    pub resume_id: String,
65    pub caller_instance: String,
66    pub callee_instance: String,
67}
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ResumeComponentPlan {
70    pub component: SemanticId,
71    pub state: Vec<SemanticId>,
72    pub computed: Vec<ResumeComputedPlan>,
73    pub events: Vec<SemanticId>,
74}
75
76/// One serializable, compiler-lowered computed cache available to resumability.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct ResumeComputedPlan {
79    pub computed: SemanticId,
80    pub cache_slot: String,
81    pub dirty_flag: String,
82    pub initial_dirty: bool,
83}
84
85#[must_use]
86#[allow(clippy::too_many_lines)]
87pub fn build_resume_plan(model: &ApplicationSemanticModel) -> ResumePlan {
88    let ir = lower_components_to_ir(model);
89    let registry = build_runtime_computed_registry(model, &ir);
90    let effect_registry = build_runtime_effect_registry(model, &optimize_effect_ir(&ir).output);
91    let context_ir = optimize_context_ir(&ir);
92    let context_registry = build_runtime_context_registry(model, &context_ir);
93    let context_updates = build_context_update_plan(model, &context_ir);
94
95    ResumePlan {
96        components: model
97            .components
98            .iter()
99            .map(|component| ResumeComponentPlan {
100                component: component.id.clone(),
101                state: component
102                    .state_fields
103                    .iter()
104                    .map(|field| field.id.clone())
105                    .collect(),
106                computed: registry
107                    .records
108                    .values()
109                    .filter(|record| {
110                        record.computed.as_str().starts_with(component.id.as_str())
111                            && record.serialization
112                                == crate::SerializationCompatibility::Serializable
113                    })
114                    .map(|record| ResumeComputedPlan {
115                        computed: record.computed.clone(),
116                        cache_slot: record.cache_slot.as_str().to_string(),
117                        dirty_flag: record.dirty_flag.id.as_str().to_string(),
118                        initial_dirty: record.dirty_flag.initial_value,
119                    })
120                    .collect(),
121                events: component.render.as_ref().map_or_else(Vec::new, |render| {
122                    render_event_handlers(render)
123                        .into_iter()
124                        .map(|handler| handler.id.clone())
125                        .collect()
126                }),
127            })
128            .collect(),
129        effects: build_effect_resume_plan(model, &effect_registry),
130        contexts: build_context_resume_plan(&context_registry, &context_updates),
131        component_instances: model
132            .component_instance_plan
133            .instances
134            .values()
135            .map(|instance| ComponentInstanceResumePlan {
136                instance: instance.id.to_string(),
137                resume_id: format!("resume-instance:{}", instance.id),
138                component: instance.component.to_string(),
139                parent_instance: instance.parent_instance.as_ref().map(ToString::to_string),
140                active_status: if instance.status == crate::ComponentInstanceStatus::Planned {
141                    "active".to_string()
142                } else {
143                    "inactive".to_string()
144                },
145                structural_region: instance.structural_region.as_ref().map(ToString::to_string),
146            })
147            .collect(),
148        structural_regions: model
149            .component_instance_plan
150            .instances
151            .values()
152            .filter_map(|instance| instance.structural_region.as_ref())
153            .collect::<std::collections::BTreeSet<_>>()
154            .into_iter()
155            .map(|region| StructuralRegionResumePlan {
156                region: region.to_string(),
157                resume_id: format!("resume-region:{region}"),
158                active_status: "inactive".to_string(),
159            })
160            .collect(),
161        slot_bindings: model
162            .slot_bindings
163            .bindings
164            .values()
165            .map(|binding| SlotBindingResumePlan {
166                binding: binding.id.to_string(),
167                resume_id: format!("resume-slot-binding:{}", binding.id),
168                caller_instance: binding.caller_instance.to_string(),
169                callee_instance: binding.callee_instance.to_string(),
170            })
171            .collect(),
172        form_instances: model
173            .optimized_form_ir
174            .optimized
175            .instances
176            .values()
177            .map(|instance| FormInstanceResumePlan {
178                form_instance: instance.id.to_string(),
179                form: instance.form.to_string(),
180                component_instance: instance.component_instance.to_string(),
181                fields: instance
182                    .storage
183                    .value
184                    .iter()
185                    .map(|(field, value_slot)| FormFieldResumePlan {
186                        field: field.to_string(),
187                        value_slot: value_slot.as_str().to_string(),
188                        dirty_slot: instance.storage.dirty[field].as_str().to_string(),
189                        touched_slot: instance.storage.touched[field].as_str().to_string(),
190                        validation_slot: instance.storage.validation[field].as_str().to_string(),
191                        serializable: model.form_fields.get(field).is_some_and(|field| {
192                            crate::serialization_compatibility(&field.semantic_type)
193                                == crate::SerializationCompatibility::Serializable
194                        }),
195                    })
196                    .collect(),
197                aggregate_validation_slot: instance.storage.aggregate.as_str().to_string(),
198                submission_slot: instance.storage.submission.as_str().to_string(),
199                serializable: model
200                    .form_fields
201                    .values()
202                    .filter(|field| field.owner_form == instance.form)
203                    .all(|field| {
204                        crate::serialization_compatibility(&field.semantic_type)
205                            == crate::SerializationCompatibility::Serializable
206                    }),
207                pending_validation_status: "none".to_string(),
208            })
209            .collect(),
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use crate::{build_application_semantic_model, build_resume_plan};
216
217    #[test]
218    fn plans_only_serializable_lowered_computed_caches_for_resumption() {
219        let parsed = presolve_parser::parse_file(
220            "src/ResumeComputed.tsx",
221            r#"
222@component("x-resume-computed")
223class ResumeComputed extends Component {
224  count = state(1);
225
226  @computed()
227  get doubled() { return this.count * 2; }
228
229  @computed()
230  get unresolved() { return this.missing; }
231}
232"#,
233        );
234        let model = build_application_semantic_model(&parsed);
235        let component = &model.components[0];
236        let doubled = component.id.computed("doubled");
237        let plan = build_resume_plan(&model);
238        let component_plan = &plan.components[0];
239
240        assert_eq!(component_plan.computed.len(), 1);
241        assert_eq!(component_plan.computed[0].computed, doubled);
242        assert_eq!(
243            component_plan.computed[0].cache_slot,
244            format!("{doubled}/runtime:cache")
245        );
246        assert_eq!(
247            component_plan.computed[0].dirty_flag,
248            format!("{doubled}/runtime:dirty")
249        );
250        assert!(component_plan.computed[0].initial_dirty);
251    }
252}