Skip to main content

presolve_compiler/
resume_activation.rs

1//! J4 canonical activation policy planning over J2/J3 products.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6    build_resume_boundary_graph, build_resume_liveness_plan, ApplicationSemanticModel,
7    FormInstanceId, ResumeBoundaryActivationProgram, ResumeBoundaryGraph, ResumeBoundaryId,
8    ResumeBoundaryKind, ResumeExistingSlot, SourceProvenance,
9};
10
11pub const RESUME_ACTIVATION_PLAN_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum ResumeActivationPolicy {
15    Eager,
16    Visible,
17    Interaction,
18    Manual,
19    None,
20}
21
22impl ResumeActivationPolicy {
23    #[must_use]
24    pub const fn precedence(self) -> u8 {
25        match self {
26            Self::Eager => 4,
27            Self::Visible => 3,
28            Self::Interaction => 2,
29            Self::Manual => 1,
30            Self::None => 0,
31        }
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
36pub enum ResumeActivationPrerequisite {
37    ApplicationBootstrap,
38    RuntimeRegistryInstallation,
39    EventDelegationInstallation,
40    PostRestoreRecomputation(ResumeExistingSlot),
41    ImmediateFormRuntime(FormInstanceId),
42    ExactInteraction(ResumeBoundaryId),
43    RequiredBoundary(ResumeBoundaryId),
44    RetainedSlot(ResumeExistingSlot),
45    RecomputableSlot(ResumeExistingSlot),
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ResumeActivationPolicyDecision {
50    pub boundary: ResumeBoundaryId,
51    pub policy: ResumeActivationPolicy,
52    pub prerequisites: Vec<ResumeActivationPrerequisite>,
53    pub source_authority: String,
54    pub provenance: SourceProvenance,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
58pub enum ResumeActivationBlockReason {
59    UnknownBoundary,
60    MissingInteractionReference,
61    RequiredBoundaryBlocked,
62    UnsupportedLazyEventPayload,
63    NoValidEagerFallback,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ResumeActivationBlock {
68    pub boundary: ResumeBoundaryId,
69    pub reason: ResumeActivationBlockReason,
70    pub provenance: SourceProvenance,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ResumeActivationPlan {
75    pub version: u32,
76    pub decisions: Vec<ResumeActivationPolicyDecision>,
77    pub blocks: Vec<ResumeActivationBlock>,
78    pub decision_index: BTreeMap<ResumeBoundaryId, usize>,
79}
80
81impl ResumeActivationPlan {
82    #[must_use]
83    pub fn decision(&self, boundary: &ResumeBoundaryId) -> Option<&ResumeActivationPolicyDecision> {
84        self.decision_index
85            .get(boundary)
86            .and_then(|index| self.decisions.get(*index))
87    }
88
89    #[must_use]
90    pub fn boundaries_with_policy(&self, policy: ResumeActivationPolicy) -> Vec<&ResumeBoundaryId> {
91        self.decisions
92            .iter()
93            .filter(|decision| decision.policy == policy)
94            .map(|decision| &decision.boundary)
95            .collect()
96    }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
100pub enum ResumeActivationIntegrityCode {
101    MissingOrDuplicatePolicy,
102    InvalidPrerequisite,
103    UnknownEventOrBoundary,
104    InvalidPolicyAuthority,
105    UnsupportedLazyPayload,
106    OrderingOrIndexDrift,
107}
108
109impl ResumeActivationIntegrityCode {
110    #[must_use]
111    pub const fn code(self) -> &'static str {
112        match self {
113            Self::MissingOrDuplicatePolicy => "PSASM1337",
114            Self::InvalidPrerequisite => "PSASM1338",
115            Self::UnknownEventOrBoundary => "PSASM1339",
116            Self::InvalidPolicyAuthority => "PSASM1340",
117            Self::UnsupportedLazyPayload => "PSASM1341",
118            Self::OrderingOrIndexDrift => "PSASM1342",
119        }
120    }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct ResumeActivationIntegrityDiagnostic {
125    pub code: ResumeActivationIntegrityCode,
126    pub boundary: Option<ResumeBoundaryId>,
127    pub message: String,
128}
129
130#[must_use]
131pub fn build_resume_activation_plan(model: &ApplicationSemanticModel) -> ResumeActivationPlan {
132    let boundaries = build_resume_boundary_graph(model);
133    let liveness = build_resume_liveness_plan(model);
134    build_resume_activation_plan_from_products(&boundaries, &liveness)
135}
136
137#[allow(clippy::too_many_lines)]
138fn build_resume_activation_plan_from_products(
139    boundaries: &ResumeBoundaryGraph,
140    liveness: &crate::ResumeLivenessPlan,
141) -> ResumeActivationPlan {
142    let blocked_boundaries = boundaries
143        .blocks
144        .iter()
145        .filter_map(|block| block.candidate_boundary.clone())
146        .collect::<BTreeSet<_>>();
147    let activation_by_boundary = boundaries
148        .activation_references
149        .iter()
150        .map(|reference| (reference.interaction_boundary.clone(), reference))
151        .collect::<BTreeMap<_, _>>();
152    let mut decisions = Vec::new();
153    let mut blocks = Vec::new();
154
155    for boundary in &boundaries.boundaries {
156        let (policy, mut prerequisites, source_authority) = match boundary.kind {
157            ResumeBoundaryKind::ApplicationRoot => (
158                ResumeActivationPolicy::Eager,
159                vec![
160                    ResumeActivationPrerequisite::ApplicationBootstrap,
161                    ResumeActivationPrerequisite::RuntimeRegistryInstallation,
162                    ResumeActivationPrerequisite::EventDelegationInstallation,
163                ],
164                "phase-j-application-bootstrap",
165            ),
166            ResumeBoundaryKind::FormInstance => {
167                let crate::ResumeBoundaryOwner::FormInstance(form) = &boundary.owner else {
168                    blocks.push(ResumeActivationBlock {
169                        boundary: boundary.id.clone(),
170                        reason: ResumeActivationBlockReason::UnknownBoundary,
171                        provenance: boundary.provenance.clone(),
172                    });
173                    continue;
174                };
175                (
176                    ResumeActivationPolicy::Eager,
177                    vec![ResumeActivationPrerequisite::ImmediateFormRuntime(
178                        form.clone(),
179                    )],
180                    "phase-i-immediate-form-runtime",
181                )
182            }
183            ResumeBoundaryKind::Interaction => {
184                let Some(reference) = activation_by_boundary.get(&boundary.id) else {
185                    blocks.push(ResumeActivationBlock {
186                        boundary: boundary.id.clone(),
187                        reason: ResumeActivationBlockReason::MissingInteractionReference,
188                        provenance: boundary.provenance.clone(),
189                    });
190                    continue;
191                };
192                if reference
193                    .required_boundaries
194                    .iter()
195                    .any(|required| blocked_boundaries.contains(required))
196                {
197                    blocks.push(ResumeActivationBlock {
198                        boundary: boundary.id.clone(),
199                        reason: ResumeActivationBlockReason::RequiredBoundaryBlocked,
200                        provenance: boundary.provenance.clone(),
201                    });
202                    continue;
203                }
204                let mut prerequisites = vec![ResumeActivationPrerequisite::ExactInteraction(
205                    boundary.id.clone(),
206                )];
207                prerequisites.extend(
208                    reference
209                        .required_boundaries
210                        .iter()
211                        .cloned()
212                        .map(ResumeActivationPrerequisite::RequiredBoundary),
213                );
214                prerequisites.extend(
215                    reference
216                        .required_retained_slots
217                        .iter()
218                        .cloned()
219                        .map(ResumeActivationPrerequisite::RetainedSlot),
220                );
221                let unsupported_payload = match &reference.program {
222                    ResumeBoundaryActivationProgram::OrdinaryEvent { event_type, .. } => {
223                        event_type != "click"
224                    }
225                    ResumeBoundaryActivationProgram::FormSubmit { .. } => false,
226                };
227                if unsupported_payload {
228                    (
229                        ResumeActivationPolicy::Eager,
230                        prerequisites,
231                        "phase-j-unsupported-lazy-payload-eager-fallback",
232                    )
233                } else {
234                    (
235                        ResumeActivationPolicy::Interaction,
236                        prerequisites,
237                        "phase-h-i-exact-interaction",
238                    )
239                }
240            }
241            ResumeBoundaryKind::ComponentInstance | ResumeBoundaryKind::StructuralRegion => {
242                let recomputable = liveness
243                    .recomputable
244                    .iter()
245                    .filter(|record| record.slot.boundary_candidate.as_ref() == Some(&boundary.id))
246                    .map(|record| {
247                        ResumeActivationPrerequisite::PostRestoreRecomputation(
248                            record.slot.existing_slot.clone(),
249                        )
250                    })
251                    .collect::<Vec<_>>();
252                if recomputable.is_empty() {
253                    (
254                        ResumeActivationPolicy::None,
255                        Vec::new(),
256                        "phase-j-no-independent-executable-work",
257                    )
258                } else {
259                    (
260                        ResumeActivationPolicy::Eager,
261                        recomputable,
262                        "phase-j-post-restore-recomputation",
263                    )
264                }
265            }
266        };
267        prerequisites.sort();
268        prerequisites.dedup();
269        decisions.push(ResumeActivationPolicyDecision {
270            boundary: boundary.id.clone(),
271            policy,
272            prerequisites,
273            source_authority: source_authority.to_string(),
274            provenance: boundary.provenance.clone(),
275        });
276    }
277
278    decisions.sort_by(|left, right| left.boundary.cmp(&right.boundary));
279    blocks
280        .sort_by(|left, right| (&left.boundary, left.reason).cmp(&(&right.boundary, right.reason)));
281    let decision_index = decisions
282        .iter()
283        .enumerate()
284        .map(|(index, decision)| (decision.boundary.clone(), index))
285        .collect();
286    ResumeActivationPlan {
287        version: RESUME_ACTIVATION_PLAN_VERSION,
288        decisions,
289        blocks,
290        decision_index,
291    }
292}
293
294#[must_use]
295pub fn validate_resume_activation_plan(
296    model: &ApplicationSemanticModel,
297    plan: &ResumeActivationPlan,
298) -> Vec<ResumeActivationIntegrityDiagnostic> {
299    let canonical = build_resume_activation_plan(model);
300    let boundaries = build_resume_boundary_graph(model);
301    let boundary_ids = boundaries
302        .boundaries
303        .iter()
304        .map(|boundary| boundary.id.clone())
305        .collect::<BTreeSet<_>>();
306    let mut diagnostics = Vec::new();
307    let mut seen = BTreeSet::new();
308    for (index, decision) in plan.decisions.iter().enumerate() {
309        if !seen.insert(decision.boundary.clone()) {
310            diagnostics.push(integrity(
311                ResumeActivationIntegrityCode::MissingOrDuplicatePolicy,
312                Some(decision.boundary.clone()),
313                "resume boundary received more than one activation policy",
314            ));
315        }
316        if !boundary_ids.contains(&decision.boundary) {
317            diagnostics.push(integrity(
318                ResumeActivationIntegrityCode::UnknownEventOrBoundary,
319                Some(decision.boundary.clone()),
320                "activation policy references an unknown resume boundary",
321            ));
322        }
323        if plan.decision_index.get(&decision.boundary) != Some(&index) {
324            diagnostics.push(integrity(
325                ResumeActivationIntegrityCode::OrderingOrIndexDrift,
326                Some(decision.boundary.clone()),
327                "activation decision index disagrees with canonical boundary order",
328            ));
329        }
330        if matches!(
331            decision.policy,
332            ResumeActivationPolicy::Visible | ResumeActivationPolicy::Manual
333        ) {
334            diagnostics.push(integrity(
335                ResumeActivationIntegrityCode::InvalidPolicyAuthority,
336                Some(decision.boundary.clone()),
337                "Visible or Manual policy lacks an earlier frozen source authority",
338            ));
339        }
340        if decision.policy == ResumeActivationPolicy::None && !decision.prerequisites.is_empty() {
341            diagnostics.push(integrity(
342                ResumeActivationIntegrityCode::InvalidPrerequisite,
343                Some(decision.boundary.clone()),
344                "None policy cannot retain executable prerequisites",
345            ));
346        }
347    }
348    for boundary in &boundary_ids {
349        let classified = plan.decision(boundary).is_some()
350            || plan.blocks.iter().any(|block| &block.boundary == boundary);
351        if !classified {
352            diagnostics.push(integrity(
353                ResumeActivationIntegrityCode::MissingOrDuplicatePolicy,
354                Some(boundary.clone()),
355                "resume boundary has neither one policy decision nor one activation block",
356            ));
357        }
358    }
359    if plan.version != RESUME_ACTIVATION_PLAN_VERSION || plan != &canonical {
360        diagnostics.push(integrity(
361            ResumeActivationIntegrityCode::OrderingOrIndexDrift,
362            None,
363            "activation plan drifted from canonical prerequisites, precedence, or order",
364        ));
365    }
366    diagnostics.sort_by(|left, right| {
367        (left.code, &left.boundary, left.message.as_str()).cmp(&(
368            right.code,
369            &right.boundary,
370            right.message.as_str(),
371        ))
372    });
373    diagnostics.dedup();
374    diagnostics
375}
376
377fn integrity(
378    code: ResumeActivationIntegrityCode,
379    boundary: Option<ResumeBoundaryId>,
380    message: &str,
381) -> ResumeActivationIntegrityDiagnostic {
382    ResumeActivationIntegrityDiagnostic {
383        code,
384        boundary,
385        message: message.to_string(),
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn assigns_eager_interaction_and_none_without_visible_or_manual_heuristics() {
395        let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
396            "src/Activation.tsx",
397            r#"
398@component("x-activation-child") class Child {
399  count = state(1);
400  @computed() get doubled() { return this.count * 2; }
401  @action() increment() { this.count++; }
402  render() { return <button onClick={() => this.increment()}>{this.count}</button>; }
403}
404@component("x-activation-page") @route("/") class Page {
405  render() { return <><Child /><aside /></>; }
406}"#,
407        ));
408        let plan = build_resume_activation_plan(&model);
409        assert!(validate_resume_activation_plan(&model, &plan).is_empty());
410        assert!(!plan
411            .boundaries_with_policy(ResumeActivationPolicy::Eager)
412            .is_empty());
413        assert_eq!(
414            plan.boundaries_with_policy(ResumeActivationPolicy::Interaction)
415                .len(),
416            1
417        );
418        assert!(plan
419            .boundaries_with_policy(ResumeActivationPolicy::Visible)
420            .is_empty());
421        assert!(plan
422            .boundaries_with_policy(ResumeActivationPolicy::Manual)
423            .is_empty());
424        assert!(plan
425            .boundaries_with_policy(ResumeActivationPolicy::None)
426            .iter()
427            .all(|boundary| plan.decision(boundary).unwrap().prerequisites.is_empty()));
428    }
429
430    #[test]
431    fn form_runtime_is_eager_while_submit_activation_is_interaction_scoped() {
432        let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
433            "src/FormActivation.tsx",
434            r#"@component("x-form-activation") @route("/") class X { @form() @serialize("json") form!: Form; @field(this.form) value = ""; @action() @submit(this.form) save(): void {} render() { return <form form={this.form}><input field={this.value}/></form>; } }"#,
435        ));
436        let plan = build_resume_activation_plan(&model);
437        assert_eq!(
438            plan.boundaries_with_policy(ResumeActivationPolicy::Eager)
439                .iter()
440                .filter(|boundary| {
441                    plan.decision(boundary)
442                        .unwrap()
443                        .prerequisites
444                        .iter()
445                        .any(|prerequisite| {
446                            matches!(
447                                prerequisite,
448                                ResumeActivationPrerequisite::ImmediateFormRuntime(_)
449                            )
450                        })
451                })
452                .count(),
453            1
454        );
455        assert_eq!(
456            plan.boundaries_with_policy(ResumeActivationPolicy::Interaction)
457                .len(),
458            1
459        );
460    }
461
462    #[test]
463    fn fixed_precedence_is_not_a_cost_heuristic() {
464        let policies = [
465            ResumeActivationPolicy::Manual,
466            ResumeActivationPolicy::Interaction,
467            ResumeActivationPolicy::Visible,
468            ResumeActivationPolicy::Eager,
469            ResumeActivationPolicy::None,
470        ];
471        assert_eq!(
472            policies
473                .into_iter()
474                .max_by_key(|policy| policy.precedence()),
475            Some(ResumeActivationPolicy::Eager)
476        );
477    }
478
479    #[test]
480    fn reserves_the_complete_j4_integrity_range() {
481        assert_eq!(
482            [
483                ResumeActivationIntegrityCode::MissingOrDuplicatePolicy,
484                ResumeActivationIntegrityCode::InvalidPrerequisite,
485                ResumeActivationIntegrityCode::UnknownEventOrBoundary,
486                ResumeActivationIntegrityCode::InvalidPolicyAuthority,
487                ResumeActivationIntegrityCode::UnsupportedLazyPayload,
488                ResumeActivationIntegrityCode::OrderingOrIndexDrift,
489            ]
490            .map(ResumeActivationIntegrityCode::code),
491            [
492                "PSASM1337",
493                "PSASM1338",
494                "PSASM1339",
495                "PSASM1340",
496                "PSASM1341",
497                "PSASM1342",
498            ]
499        );
500    }
501}