1use 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}