1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6 build_computed_instance_slot_registry, build_ordinary_template_instance_registry,
7 build_resume_boundary_graph, build_resume_liveness_plan, build_resume_schema_registry,
8 build_runtime_component_registry, build_state_instance_storage_registry,
9 lower_components_to_ir, ApplicationSemanticModel, ComponentInstanceId,
10 ComponentStructuralRegionId, ConsumerInstanceId, ContextSourceInstanceId, FieldId,
11 FormInstanceId, InstanceContextValueSlotId, OrdinaryTemplateBindingKind, ProviderInstanceId,
12 ResumeAnchorId, ResumeBoundaryId, ResumeBoundaryOwner, ResumeExistingSlot,
13 ResumeLivenessClassificationRef, ResumeRestoreProgramId, ResumeSlotId, ResumeValueCodec,
14 SemanticId, SourceProvenance, TemplateInstanceBindingId,
15};
16
17pub const RESUME_RESTORE_PLAN_VERSION: u32 = 1;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
20pub enum ResumeRestorePhase {
21 R0ValidateAllArtifactsAndSnapshot,
22 R1AllocateEmptyClosedRuntimeRegistries,
23 R2AllocateBoundaryRuntimeRecords,
24 R3RestoreMutableStateAndResources,
25 R4RestoreRetainedComputedCaches,
26 R5RecomputeComputed,
27 R6RestoreContextProviderValues,
28 R7BindContextConsumers,
29 R8RestoreComponentRuntimeAndSlots,
30 R9RestoreStructuralSelection,
31 R10VerifyDomAnchors,
32 R11RestoreFormFieldValues,
33 R12RestoreFormDirtyAndTouched,
34 R13RestoreFormValidation,
35 R14RestoreStableFormSubmission,
36 R15SynchronizeFormControls,
37 R16InstallDomBindings,
38 R17InstallEffectSubscriptions,
39 R18InstallEventDelegationAndActivation,
40 R19MarkBoundariesReady,
41 R20PublishApplicationReady,
42}
43
44impl ResumeRestorePhase {
45 pub const ALL: [Self; 21] = [
46 Self::R0ValidateAllArtifactsAndSnapshot,
47 Self::R1AllocateEmptyClosedRuntimeRegistries,
48 Self::R2AllocateBoundaryRuntimeRecords,
49 Self::R3RestoreMutableStateAndResources,
50 Self::R4RestoreRetainedComputedCaches,
51 Self::R5RecomputeComputed,
52 Self::R6RestoreContextProviderValues,
53 Self::R7BindContextConsumers,
54 Self::R8RestoreComponentRuntimeAndSlots,
55 Self::R9RestoreStructuralSelection,
56 Self::R10VerifyDomAnchors,
57 Self::R11RestoreFormFieldValues,
58 Self::R12RestoreFormDirtyAndTouched,
59 Self::R13RestoreFormValidation,
60 Self::R14RestoreStableFormSubmission,
61 Self::R15SynchronizeFormControls,
62 Self::R16InstallDomBindings,
63 Self::R17InstallEffectSubscriptions,
64 Self::R18InstallEventDelegationAndActivation,
65 Self::R19MarkBoundariesReady,
66 Self::R20PublishApplicationReady,
67 ];
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
71pub enum ResumeRestoreInstruction {
72 AllocateBoundaryRuntimeRecord,
73 DecodeValue {
74 slot_id: ResumeSlotId,
75 codec: ResumeValueCodec,
76 },
77 WriteSlot {
78 slot_id: ResumeSlotId,
79 },
80 RestoreStructuralSelection {
81 region_id: crate::ComponentStructuralRegionId,
82 slot_id: ResumeSlotId,
83 },
84 RestoreFormSlot {
85 form_instance_id: FormInstanceId,
86 field_id: Option<FieldId>,
87 slot_id: ResumeSlotId,
88 },
89 RecomputeComputed {
90 component_instance_id: ComponentInstanceId,
91 computed_id: SemanticId,
92 },
93 BindContextConsumer {
94 consumer_instance_id: ConsumerInstanceId,
95 selected_source: ContextSourceInstanceId,
96 provider_instance_id: Option<ProviderInstanceId>,
97 value_slot_id: InstanceContextValueSlotId,
98 },
99 InstallDomBinding {
100 binding_id: TemplateInstanceBindingId,
101 anchor_id: ResumeAnchorId,
102 },
103 InstallEffectSubscription {
104 effect_id: SemanticId,
105 },
106 MarkBoundaryRestored,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct ResumeRestoreInstructionRecord {
111 pub phase: ResumeRestorePhase,
112 pub instruction: ResumeRestoreInstruction,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct ResumeRestoreSlotAssignment {
117 pub slot: ResumeExistingSlot,
118 pub slot_id: ResumeSlotId,
119 pub phase: ResumeRestorePhase,
120 pub retained: bool,
121 pub recomputable: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct ResumeRestoreProgram {
126 pub program_id: ResumeRestoreProgramId,
127 pub boundary_id: ResumeBoundaryId,
128 pub slot_assignments: Vec<ResumeRestoreSlotAssignment>,
129 pub instructions: Vec<ResumeRestoreInstructionRecord>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ResumeRestoreSchedulePhase {
134 pub phase: ResumeRestorePhase,
135 pub programs: Vec<ResumeRestoreProgramId>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ResumeRestoreApplicationSchedule {
140 pub phases: Vec<ResumeRestoreSchedulePhase>,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
144pub enum ResumeRestoreBlockReason {
145 MissingSlotSchema,
146 UnsupportedRecomputableSlot,
147 MissingComputedProgram,
148 MissingFormSlotOwner,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ResumeRestoreBlock {
153 pub boundary: Option<ResumeBoundaryId>,
154 pub slot: ResumeExistingSlot,
155 pub reason: ResumeRestoreBlockReason,
156 pub provenance: SourceProvenance,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ResumeRestorePlan {
161 pub version: u32,
162 pub programs: Vec<ResumeRestoreProgram>,
163 pub schedule: ResumeRestoreApplicationSchedule,
164 pub blocks: Vec<ResumeRestoreBlock>,
165 pub program_index: BTreeMap<ResumeBoundaryId, usize>,
166}
167
168impl ResumeRestorePlan {
169 #[must_use]
170 pub fn program(&self, boundary: &ResumeBoundaryId) -> Option<&ResumeRestoreProgram> {
171 self.program_index
172 .get(boundary)
173 .and_then(|index| self.programs.get(*index))
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
178pub enum ResumeRestoreIntegrityCode {
179 ProgramReference,
180 PhaseOrDuplicateWrite,
181 MissingCompletion,
182 OrderingOrOutputDrift,
183}
184
185impl ResumeRestoreIntegrityCode {
186 #[must_use]
187 pub const fn code(self) -> &'static str {
188 match self {
189 Self::ProgramReference => "PSASM1359",
190 Self::PhaseOrDuplicateWrite => "PSASM1360",
191 Self::MissingCompletion => "PSASM1361",
192 Self::OrderingOrOutputDrift => "PSASM1362",
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ResumeRestoreIntegrityDiagnostic {
199 pub code: ResumeRestoreIntegrityCode,
200 pub boundary: Option<ResumeBoundaryId>,
201 pub message: String,
202}
203
204#[derive(Debug, Clone)]
205struct RestoreAuthorities {
206 computed: BTreeMap<ResumeExistingSlot, (ComponentInstanceId, SemanticId)>,
207 form: BTreeMap<ResumeExistingSlot, (FormInstanceId, Option<FieldId>)>,
208 context_bindings:
209 BTreeMap<ResumeExistingSlot, Vec<crate::RuntimeComponentContextBindingRecord>>,
210 structural: BTreeMap<ComponentStructuralRegionId, ResumeExistingSlot>,
211}
212
213impl RestoreAuthorities {
214 fn new(model: &ApplicationSemanticModel) -> Self {
215 let ir = lower_components_to_ir(model);
216 let mut computed = BTreeMap::new();
217 for record in build_computed_instance_slot_registry(model, &ir).records {
218 let value = (
219 record.component_instance_id.clone(),
220 record.computed_id.clone(),
221 );
222 computed.insert(
223 ResumeExistingSlot::ComputedCache(record.cache_slot_id),
224 value.clone(),
225 );
226 computed.insert(
227 ResumeExistingSlot::ComputedDirty(record.dirty_slot_id),
228 value,
229 );
230 }
231 let mut form = BTreeMap::new();
232 for instance in model.optimized_form_ir.optimized.instances.values() {
233 for (field, slot) in &instance.storage.value {
234 form.insert(
235 ResumeExistingSlot::FormFieldValue(slot.clone()),
236 (instance.id.clone(), Some(field.clone())),
237 );
238 }
239 for (field, slot) in &instance.storage.dirty {
240 form.insert(
241 ResumeExistingSlot::FormFieldDirty(slot.clone()),
242 (instance.id.clone(), Some(field.clone())),
243 );
244 }
245 for (field, slot) in &instance.storage.touched {
246 form.insert(
247 ResumeExistingSlot::FormFieldTouched(slot.clone()),
248 (instance.id.clone(), Some(field.clone())),
249 );
250 }
251 for (field, slot) in &instance.storage.validation {
252 form.insert(
253 ResumeExistingSlot::FormFieldValidation(slot.clone()),
254 (instance.id.clone(), Some(field.clone())),
255 );
256 }
257 form.insert(
258 ResumeExistingSlot::FormValidationAggregate(instance.storage.aggregate.clone()),
259 (instance.id.clone(), None),
260 );
261 form.insert(
262 ResumeExistingSlot::FormSubmission(instance.storage.submission.clone()),
263 (instance.id.clone(), None),
264 );
265 }
266 let mut context_bindings =
267 BTreeMap::<ResumeExistingSlot, Vec<crate::RuntimeComponentContextBindingRecord>>::new();
268 for binding in build_runtime_component_registry(model, &model.component_ir_optimization)
269 .instance_context_bindings
270 {
271 context_bindings
272 .entry(ResumeExistingSlot::Context(binding.runtime_slot.clone()))
273 .or_default()
274 .push(binding);
275 }
276 for bindings in context_bindings.values_mut() {
277 bindings.sort_by(|left, right| left.consumer_instance.cmp(&right.consumer_instance));
278 }
279 let state_slots = build_state_instance_storage_registry(model, &ir);
280 let structural = build_ordinary_template_instance_registry(model)
281 .bindings
282 .iter()
283 .filter_map(|binding| {
284 let region_kind = match binding.binding_kind {
285 OrdinaryTemplateBindingKind::Conditional => "conditional",
286 OrdinaryTemplateBindingKind::List => "keyed-list",
287 _ => return None,
288 };
289 let [storage] = binding.state_storage_ids.as_slice() else {
290 return None;
291 };
292 let state_slot = state_slots.records.iter().find(|record| {
293 record.component_instance_id == binding.component_instance_id
294 && record.storage_id == *storage
295 })?;
296 Some((
297 ComponentStructuralRegionId::for_template_entity(
298 &binding.declaration_binding_id,
299 region_kind,
300 ),
301 ResumeExistingSlot::State(state_slot.slot_id.clone()),
302 ))
303 })
304 .collect();
305 Self {
306 computed,
307 form,
308 context_bindings,
309 structural,
310 }
311 }
312}
313
314#[must_use]
315#[allow(clippy::too_many_lines)]
316pub fn build_resume_restore_plan(model: &ApplicationSemanticModel) -> ResumeRestorePlan {
317 let graph = build_resume_boundary_graph(model);
318 let liveness = build_resume_liveness_plan(model);
319 let schemas = build_resume_schema_registry(model);
320 let authorities = RestoreAuthorities::new(model);
321 let mut programs = Vec::new();
322 let mut blocks = Vec::new();
323
324 for boundary in &graph.boundaries {
325 let mut by_phase = BTreeMap::<ResumeRestorePhase, Vec<ResumeRestoreInstruction>>::new();
326 by_phase
327 .entry(ResumeRestorePhase::R2AllocateBoundaryRuntimeRecords)
328 .or_default()
329 .push(ResumeRestoreInstruction::AllocateBoundaryRuntimeRecord);
330 let mut slot_assignments = Vec::new();
331 if let Some(schema) = schemas.schema(&boundary.id) {
332 for slot_schema in &schema.slots {
333 let Some(classification) = liveness.classification(&slot_schema.existing_slot)
334 else {
335 blocks.push(ResumeRestoreBlock {
336 boundary: Some(boundary.id.clone()),
337 slot: slot_schema.existing_slot.clone(),
338 reason: ResumeRestoreBlockReason::MissingSlotSchema,
339 provenance: slot_schema.provenance.clone(),
340 });
341 continue;
342 };
343 match classification {
344 ResumeLivenessClassificationRef::Retained(_) => {
345 let phase = restore_phase(&slot_schema.existing_slot);
346 slot_assignments.push(ResumeRestoreSlotAssignment {
347 slot: slot_schema.existing_slot.clone(),
348 slot_id: slot_schema.resume_slot_id.clone(),
349 phase,
350 retained: true,
351 recomputable: false,
352 });
353 let instructions = by_phase.entry(phase).or_default();
354 instructions.push(ResumeRestoreInstruction::DecodeValue {
355 slot_id: slot_schema.resume_slot_id.clone(),
356 codec: slot_schema.codec.clone(),
357 });
358 if is_form_slot(&slot_schema.existing_slot) {
359 if let Some((form_instance_id, field_id)) =
360 authorities.form.get(&slot_schema.existing_slot)
361 {
362 instructions.push(ResumeRestoreInstruction::RestoreFormSlot {
363 form_instance_id: form_instance_id.clone(),
364 field_id: field_id.clone(),
365 slot_id: slot_schema.resume_slot_id.clone(),
366 });
367 } else {
368 blocks.push(ResumeRestoreBlock {
369 boundary: Some(boundary.id.clone()),
370 slot: slot_schema.existing_slot.clone(),
371 reason: ResumeRestoreBlockReason::MissingFormSlotOwner,
372 provenance: slot_schema.provenance.clone(),
373 });
374 }
375 } else {
376 instructions.push(ResumeRestoreInstruction::WriteSlot {
377 slot_id: slot_schema.resume_slot_id.clone(),
378 });
379 }
380 }
381 ResumeLivenessClassificationRef::Recomputable(_) => {
382 slot_assignments.push(ResumeRestoreSlotAssignment {
383 slot: slot_schema.existing_slot.clone(),
384 slot_id: slot_schema.resume_slot_id.clone(),
385 phase: ResumeRestorePhase::R5RecomputeComputed,
386 retained: false,
387 recomputable: true,
388 });
389 if matches!(
390 slot_schema.existing_slot,
391 ResumeExistingSlot::ComputedCache(_)
392 ) {
393 if let Some((component_instance_id, computed_id)) =
394 authorities.computed.get(&slot_schema.existing_slot)
395 {
396 by_phase
397 .entry(ResumeRestorePhase::R5RecomputeComputed)
398 .or_default()
399 .push(ResumeRestoreInstruction::RecomputeComputed {
400 component_instance_id: component_instance_id.clone(),
401 computed_id: computed_id.clone(),
402 });
403 } else {
404 blocks.push(ResumeRestoreBlock {
405 boundary: Some(boundary.id.clone()),
406 slot: slot_schema.existing_slot.clone(),
407 reason: ResumeRestoreBlockReason::MissingComputedProgram,
408 provenance: slot_schema.provenance.clone(),
409 });
410 }
411 } else if !matches!(
412 slot_schema.existing_slot,
413 ResumeExistingSlot::ComputedDirty(_)
414 ) {
415 blocks.push(ResumeRestoreBlock {
416 boundary: Some(boundary.id.clone()),
417 slot: slot_schema.existing_slot.clone(),
418 reason: ResumeRestoreBlockReason::UnsupportedRecomputableSlot,
419 provenance: slot_schema.provenance.clone(),
420 });
421 }
422 }
423 ResumeLivenessClassificationRef::Excluded(_)
424 | ResumeLivenessClassificationRef::Blocked(_) => {
425 blocks.push(ResumeRestoreBlock {
426 boundary: Some(boundary.id.clone()),
427 slot: slot_schema.existing_slot.clone(),
428 reason: ResumeRestoreBlockReason::MissingSlotSchema,
429 provenance: slot_schema.provenance.clone(),
430 });
431 }
432 }
433 }
434 }
435 for slot in &boundary.owned_slots {
436 if let Some(bindings) = authorities.context_bindings.get(slot) {
437 for binding in bindings {
438 by_phase
439 .entry(ResumeRestorePhase::R7BindContextConsumers)
440 .or_default()
441 .push(ResumeRestoreInstruction::BindContextConsumer {
442 consumer_instance_id: binding.consumer_instance.clone(),
443 selected_source: binding.selected_source.clone(),
444 provider_instance_id: binding.provider_source.clone(),
445 value_slot_id: binding.runtime_slot.clone(),
446 });
447 }
448 }
449 }
450 if let ResumeBoundaryOwner::StructuralRegion { region, .. } = &boundary.owner {
451 if let Some(slot) = authorities.structural.get(region) {
452 by_phase
453 .entry(ResumeRestorePhase::R9RestoreStructuralSelection)
454 .or_default()
455 .push(ResumeRestoreInstruction::RestoreStructuralSelection {
456 region_id: region.clone(),
457 slot_id: slot.resume_slot_id(),
458 });
459 }
460 }
461 by_phase
462 .entry(ResumeRestorePhase::R19MarkBoundariesReady)
463 .or_default()
464 .push(ResumeRestoreInstruction::MarkBoundaryRestored);
465 slot_assignments.sort_by(|left, right| left.slot_id.cmp(&right.slot_id));
466 let instructions = ResumeRestorePhase::ALL
467 .iter()
468 .flat_map(|phase| {
469 by_phase
470 .remove(phase)
471 .unwrap_or_default()
472 .into_iter()
473 .map(|instruction| ResumeRestoreInstructionRecord {
474 phase: *phase,
475 instruction,
476 })
477 })
478 .collect();
479 programs.push(ResumeRestoreProgram {
480 program_id: ResumeRestoreProgramId::for_boundary(&boundary.id),
481 boundary_id: boundary.id.clone(),
482 slot_assignments,
483 instructions,
484 });
485 }
486
487 blocks.sort_by(|left, right| {
488 (&left.boundary, &left.slot, left.reason).cmp(&(&right.boundary, &right.slot, right.reason))
489 });
490 let schedule = ResumeRestoreApplicationSchedule {
491 phases: ResumeRestorePhase::ALL
492 .iter()
493 .map(|phase| ResumeRestoreSchedulePhase {
494 phase: *phase,
495 programs: programs
496 .iter()
497 .filter(|program| {
498 program
499 .instructions
500 .iter()
501 .any(|instruction| instruction.phase == *phase)
502 })
503 .map(|program| program.program_id.clone())
504 .collect(),
505 })
506 .collect(),
507 };
508 let program_index = programs
509 .iter()
510 .enumerate()
511 .map(|(index, program)| (program.boundary_id.clone(), index))
512 .collect();
513 ResumeRestorePlan {
514 version: RESUME_RESTORE_PLAN_VERSION,
515 programs,
516 schedule,
517 blocks,
518 program_index,
519 }
520}
521
522fn restore_phase(slot: &ResumeExistingSlot) -> ResumeRestorePhase {
523 match slot {
524 ResumeExistingSlot::State(_) => ResumeRestorePhase::R3RestoreMutableStateAndResources,
525 ResumeExistingSlot::ComputedCache(_) | ResumeExistingSlot::ComputedDirty(_) => {
526 ResumeRestorePhase::R4RestoreRetainedComputedCaches
527 }
528 ResumeExistingSlot::Context(_) => ResumeRestorePhase::R6RestoreContextProviderValues,
529 ResumeExistingSlot::FormFieldValue(_) => ResumeRestorePhase::R11RestoreFormFieldValues,
530 ResumeExistingSlot::FormFieldDirty(_) | ResumeExistingSlot::FormFieldTouched(_) => {
531 ResumeRestorePhase::R12RestoreFormDirtyAndTouched
532 }
533 ResumeExistingSlot::FormFieldValidation(_)
534 | ResumeExistingSlot::FormValidationAggregate(_) => {
535 ResumeRestorePhase::R13RestoreFormValidation
536 }
537 ResumeExistingSlot::FormSubmission(_) => ResumeRestorePhase::R14RestoreStableFormSubmission,
538 ResumeExistingSlot::EffectActivation(_) => {
539 ResumeRestorePhase::R17InstallEffectSubscriptions
540 }
541 }
542}
543
544fn is_form_slot(slot: &ResumeExistingSlot) -> bool {
545 matches!(
546 slot,
547 ResumeExistingSlot::FormFieldValue(_)
548 | ResumeExistingSlot::FormFieldDirty(_)
549 | ResumeExistingSlot::FormFieldTouched(_)
550 | ResumeExistingSlot::FormFieldValidation(_)
551 | ResumeExistingSlot::FormValidationAggregate(_)
552 | ResumeExistingSlot::FormSubmission(_)
553 )
554}
555
556#[allow(clippy::too_many_lines)]
562pub fn validate_resume_restore_plan(
563 model: &ApplicationSemanticModel,
564 plan: &ResumeRestorePlan,
565) -> Result<(), Vec<ResumeRestoreIntegrityDiagnostic>> {
566 let mut diagnostics = Vec::new();
567 let graph = build_resume_boundary_graph(model);
568 let expected_boundaries = graph
569 .boundaries
570 .iter()
571 .map(|boundary| boundary.id.clone())
572 .collect::<Vec<_>>();
573 let actual_boundaries = plan
574 .programs
575 .iter()
576 .map(|program| program.boundary_id.clone())
577 .collect::<Vec<_>>();
578 if expected_boundaries != actual_boundaries
579 || plan.schedule.phases.len() != ResumeRestorePhase::ALL.len()
580 || plan
581 .schedule
582 .phases
583 .iter()
584 .map(|phase| phase.phase)
585 .collect::<Vec<_>>()
586 != ResumeRestorePhase::ALL
587 {
588 diagnostics.push(restore_diagnostic(
589 ResumeRestoreIntegrityCode::ProgramReference,
590 None,
591 "restore programs or fixed phase schedule do not correspond",
592 ));
593 }
594 let known_programs = plan
595 .programs
596 .iter()
597 .map(|program| program.program_id.clone())
598 .collect::<BTreeSet<_>>();
599 if plan
600 .schedule
601 .phases
602 .iter()
603 .flat_map(|phase| &phase.programs)
604 .any(|program| !known_programs.contains(program))
605 {
606 diagnostics.push(restore_diagnostic(
607 ResumeRestoreIntegrityCode::ProgramReference,
608 None,
609 "restore schedule references an unknown program",
610 ));
611 }
612 for program in &plan.programs {
613 let mut writes = BTreeSet::new();
614 let mut previous_phase = None;
615 for record in &program.instructions {
616 if previous_phase.is_some_and(|phase| phase > record.phase) {
617 diagnostics.push(restore_diagnostic(
618 ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
619 Some(program.boundary_id.clone()),
620 "restore instruction phases are out of fixed order",
621 ));
622 }
623 previous_phase = Some(record.phase);
624 if !instruction_phase_is_valid(program, record) {
625 diagnostics.push(restore_diagnostic(
626 ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
627 Some(program.boundary_id.clone()),
628 "restore instruction is assigned to the wrong fixed phase",
629 ));
630 }
631 if let ResumeRestoreInstruction::WriteSlot { slot_id }
632 | ResumeRestoreInstruction::RestoreFormSlot { slot_id, .. } = &record.instruction
633 {
634 if !writes.insert(slot_id.clone()) {
635 diagnostics.push(restore_diagnostic(
636 ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
637 Some(program.boundary_id.clone()),
638 "restore program writes one slot more than once",
639 ));
640 }
641 }
642 }
643 if !matches!(
644 program.instructions.last(),
645 Some(ResumeRestoreInstructionRecord {
646 phase: ResumeRestorePhase::R19MarkBoundariesReady,
647 instruction: ResumeRestoreInstruction::MarkBoundaryRestored,
648 })
649 ) {
650 diagnostics.push(restore_diagnostic(
651 ResumeRestoreIntegrityCode::MissingCompletion,
652 Some(program.boundary_id.clone()),
653 "restore program lacks its final MarkBoundaryRestored",
654 ));
655 }
656 }
657 if plan.version != RESUME_RESTORE_PLAN_VERSION || plan != &build_resume_restore_plan(model) {
658 diagnostics.push(restore_diagnostic(
659 ResumeRestoreIntegrityCode::OrderingOrOutputDrift,
660 None,
661 "restore plan drifted from canonical J2/J3/J6 construction",
662 ));
663 }
664 if diagnostics.is_empty() {
665 Ok(())
666 } else {
667 Err(diagnostics)
668 }
669}
670
671fn instruction_phase_is_valid(
672 program: &ResumeRestoreProgram,
673 record: &ResumeRestoreInstructionRecord,
674) -> bool {
675 match &record.instruction {
676 ResumeRestoreInstruction::AllocateBoundaryRuntimeRecord => {
677 record.phase == ResumeRestorePhase::R2AllocateBoundaryRuntimeRecords
678 }
679 ResumeRestoreInstruction::DecodeValue { slot_id, .. }
680 | ResumeRestoreInstruction::WriteSlot { slot_id }
681 | ResumeRestoreInstruction::RestoreFormSlot { slot_id, .. } => program
682 .slot_assignments
683 .iter()
684 .find(|assignment| &assignment.slot_id == slot_id)
685 .is_some_and(|assignment| assignment.retained && assignment.phase == record.phase),
686 ResumeRestoreInstruction::RestoreStructuralSelection { .. } => {
687 record.phase == ResumeRestorePhase::R9RestoreStructuralSelection
688 }
689 ResumeRestoreInstruction::RecomputeComputed { .. } => {
690 record.phase == ResumeRestorePhase::R5RecomputeComputed
691 }
692 ResumeRestoreInstruction::BindContextConsumer { .. } => {
693 record.phase == ResumeRestorePhase::R7BindContextConsumers
694 }
695 ResumeRestoreInstruction::InstallDomBinding { .. } => {
696 record.phase == ResumeRestorePhase::R16InstallDomBindings
697 }
698 ResumeRestoreInstruction::InstallEffectSubscription { .. } => {
699 record.phase == ResumeRestorePhase::R17InstallEffectSubscriptions
700 }
701 ResumeRestoreInstruction::MarkBoundaryRestored => {
702 record.phase == ResumeRestorePhase::R19MarkBoundariesReady
703 }
704 }
705}
706
707fn restore_diagnostic(
708 code: ResumeRestoreIntegrityCode,
709 boundary: Option<ResumeBoundaryId>,
710 message: &str,
711) -> ResumeRestoreIntegrityDiagnostic {
712 ResumeRestoreIntegrityDiagnostic {
713 code,
714 boundary,
715 message: message.to_string(),
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 fn model(source: &str) -> ApplicationSemanticModel {
724 crate::build_application_semantic_model(&presolve_parser::parse_file(
725 "src/ResumeRestore.tsx",
726 source,
727 ))
728 }
729
730 #[test]
731 fn builds_all_fixed_phases_and_assigns_retained_and_recomputable_slots() {
732 let model = model(
733 r#"@component("x-counter") class Counter {
734 count = state(1);
735 @computed() get doubled() { return this.count * 2; }
736 render() { return <button>{this.doubled}</button>; }
737}"#,
738 );
739 let plan = build_resume_restore_plan(&model);
740 let liveness = build_resume_liveness_plan(&model);
741 assert_eq!(
742 plan.schedule
743 .phases
744 .iter()
745 .map(|phase| phase.phase)
746 .collect::<Vec<_>>(),
747 ResumeRestorePhase::ALL
748 );
749 let assignments = plan
750 .programs
751 .iter()
752 .flat_map(|program| &program.slot_assignments)
753 .map(|assignment| assignment.slot.clone())
754 .collect::<BTreeSet<_>>();
755 assert!(liveness
756 .retained
757 .iter()
758 .all(|record| assignments.contains(&record.slot.existing_slot)));
759 assert!(liveness
760 .recomputable
761 .iter()
762 .all(|record| assignments.contains(&record.slot.existing_slot)));
763 assert!(plan.programs.iter().all(|program| matches!(
764 program.instructions.last(),
765 Some(ResumeRestoreInstructionRecord {
766 phase: ResumeRestorePhase::R19MarkBoundariesReady,
767 instruction: ResumeRestoreInstruction::MarkBoundaryRestored,
768 })
769 )));
770 assert_eq!(validate_resume_restore_plan(&model, &plan), Ok(()));
771 }
772
773 #[test]
774 fn restores_form_slot_classes_in_their_exact_fixed_phases() {
775 let model = model(
776 r#"@component("x-profile") class Profile {
777 @form() profile!: Form;
778 @field(this.profile) name = "";
779 render() { return <input field={this.name} />; }
780}"#,
781 );
782 let plan = build_resume_restore_plan(&model);
783 let assignments = plan
784 .programs
785 .iter()
786 .flat_map(|program| &program.slot_assignments)
787 .map(|assignment| (&assignment.slot, assignment.phase))
788 .collect::<BTreeMap<_, _>>();
789 assert!(assignments.iter().any(|(slot, phase)| matches!(
790 (slot, phase),
791 (
792 ResumeExistingSlot::FormFieldValue(_),
793 ResumeRestorePhase::R11RestoreFormFieldValues
794 )
795 )));
796 assert!(assignments.iter().any(|(slot, phase)| matches!(
797 (slot, phase),
798 (
799 ResumeExistingSlot::FormFieldDirty(_) | ResumeExistingSlot::FormFieldTouched(_),
800 ResumeRestorePhase::R12RestoreFormDirtyAndTouched
801 )
802 )));
803 assert!(assignments.iter().any(|(slot, phase)| matches!(
804 (slot, phase),
805 (
806 ResumeExistingSlot::FormFieldValidation(_)
807 | ResumeExistingSlot::FormValidationAggregate(_),
808 ResumeRestorePhase::R13RestoreFormValidation
809 )
810 )));
811 assert!(assignments.iter().any(|(slot, phase)| matches!(
812 (slot, phase),
813 (
814 ResumeExistingSlot::FormSubmission(_),
815 ResumeRestorePhase::R14RestoreStableFormSubmission
816 )
817 )));
818 }
819
820 #[test]
821 fn structural_regions_restore_from_their_exact_retained_state_slots() {
822 let model = model(
823 r#"@component("x-leaf") class Leaf extends Component { render() { return <span />; } }
824@component("x-page") @route("/") class Page extends Component {
825 visible = state(true);
826 items = state([{ id: "a" }, { id: "b" }]);
827 render() {
828 return <main>{this.visible ? <div><Leaf /></div> : <aside>Hidden</aside>}<ul>{this.items.map(item => <li key={item.id}><Leaf /></li>)}</ul></main>;
829 }
830 hide() { this.visible = false; }
831 trim() { this.items = [{ id: "b" }]; }
832}"#,
833 );
834 let plan = build_resume_restore_plan(&model);
835 let structural = plan
836 .programs
837 .iter()
838 .flat_map(|program| &program.instructions)
839 .filter_map(|record| match &record.instruction {
840 ResumeRestoreInstruction::RestoreStructuralSelection { region_id, slot_id } => {
841 Some((record.phase, region_id.to_string(), slot_id.to_string()))
842 }
843 _ => None,
844 })
845 .collect::<Vec<_>>();
846 assert_eq!(structural.len(), 2);
847 assert!(structural
848 .iter()
849 .all(|(phase, _, _)| { *phase == ResumeRestorePhase::R9RestoreStructuralSelection }));
850 assert!(structural
851 .iter()
852 .any(|(_, region, slot)| region.ends_with(":conditional")
853 && slot.contains("state%3Avisible")));
854 assert!(structural
855 .iter()
856 .any(|(_, region, slot)| region.ends_with(":keyed-list")
857 && slot.contains("state%3Aitems")));
858 assert_eq!(validate_resume_restore_plan(&model, &plan), Ok(()));
859 }
860
861 #[test]
862 fn malformed_reference_phase_write_completion_and_parent_order_are_rejected() {
863 let model = model(
864 r#"@component("x-child") class Child { value = state(1); render() { return <span>{this.value}</span>; } }
865@component("x-parent") @route("/") class Parent { render() { return <Child />; } }"#,
866 );
867 let canonical = build_resume_restore_plan(&model);
868
869 let mut dangling = canonical.clone();
870 dangling.programs.pop();
871 let diagnostics =
872 validate_resume_restore_plan(&model, &dangling).expect_err("dangling reference");
873 assert!(diagnostics
874 .iter()
875 .any(|diagnostic| { diagnostic.code == ResumeRestoreIntegrityCode::ProgramReference }));
876
877 let mut missing = canonical.clone();
878 missing.programs[0].instructions.pop();
879 let diagnostics = validate_resume_restore_plan(&model, &missing).expect_err("missing mark");
880 assert!(diagnostics.iter().any(|diagnostic| {
881 diagnostic.code == ResumeRestoreIntegrityCode::MissingCompletion
882 }));
883
884 let mut wrong_phase = canonical.clone();
885 wrong_phase.programs[0].instructions[0].phase = ResumeRestorePhase::R19MarkBoundariesReady;
886 let diagnostics =
887 validate_resume_restore_plan(&model, &wrong_phase).expect_err("wrong phase");
888 assert!(diagnostics.iter().any(|diagnostic| {
889 diagnostic.code == ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite
890 || diagnostic.code == ResumeRestoreIntegrityCode::OrderingOrOutputDrift
891 }));
892
893 let mut duplicate = canonical.clone();
894 let duplicate_write = duplicate
895 .programs
896 .iter()
897 .enumerate()
898 .find_map(|(index, program)| {
899 program
900 .instructions
901 .iter()
902 .find(|record| {
903 matches!(
904 record.instruction,
905 ResumeRestoreInstruction::WriteSlot { .. }
906 )
907 })
908 .cloned()
909 .map(|record| (index, record))
910 });
911 if let Some((program_index, write)) = duplicate_write {
912 duplicate.programs[program_index]
913 .instructions
914 .insert(1, write);
915 }
916 let diagnostics =
917 validate_resume_restore_plan(&model, &duplicate).expect_err("duplicate write");
918 assert!(diagnostics.iter().any(|diagnostic| {
919 diagnostic.code == ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite
920 || diagnostic.code == ResumeRestoreIntegrityCode::OrderingOrOutputDrift
921 }));
922
923 let mut reversed = canonical;
924 reversed.programs.reverse();
925 let diagnostics =
926 validate_resume_restore_plan(&model, &reversed).expect_err("parent order");
927 assert!(diagnostics
928 .iter()
929 .any(|diagnostic| { diagnostic.code == ResumeRestoreIntegrityCode::ProgramReference }));
930 }
931
932 #[test]
933 fn construction_is_deterministic_under_source_reversal() {
934 let first = presolve_parser::parse_file(
935 "src/A.tsx",
936 r#"@component("x-a") @route("/a") class A { value = state(1); render() { return <button>{this.value}</button>; } }"#,
937 );
938 let second = presolve_parser::parse_file(
939 "src/B.tsx",
940 r#"@component("x-b") @route("/b") class B { enabled = state(true); render() { return <button>{this.enabled}</button>; } }"#,
941 );
942 let forward = crate::build_application_semantic_model_for_unit(
943 &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
944 );
945 let reverse = crate::build_application_semantic_model_for_unit(
946 &crate::CompilationUnit::from_parsed_files(vec![second, first]),
947 );
948 assert_eq!(
949 build_resume_restore_plan(&forward),
950 build_resume_restore_plan(&reverse)
951 );
952 }
953
954 #[test]
955 fn reserves_the_complete_j8_integrity_range() {
956 assert_eq!(
957 [
958 ResumeRestoreIntegrityCode::ProgramReference,
959 ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
960 ResumeRestoreIntegrityCode::MissingCompletion,
961 ResumeRestoreIntegrityCode::OrderingOrOutputDrift,
962 ]
963 .map(ResumeRestoreIntegrityCode::code),
964 ["PSASM1359", "PSASM1360", "PSASM1361", "PSASM1362"]
965 );
966 }
967}