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 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#[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}