1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6 build_resume_activation_plan, build_resume_boundary_graph, ApplicationSemanticModel,
7 ResumeActivationPolicy, ResumeActivationPrerequisite, ResumeActivationRootKind,
8 ResumeBoundaryActivationProgram, ResumeBoundaryId, ResumeChunkId, ResumeExistingSlot,
9 SemanticId, SourceProvenance,
10};
11
12pub const RESUME_CHUNK_GRAPH_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum ResumeChunkRootKind {
16 Eager,
17 Interaction,
18 Visible,
19 Manual,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
23pub enum ResumeChunkProgram {
24 RuntimeBootstrap,
25 RuntimeRegistries,
26 EventDelegation,
27 PostRestoreRecomputation(ResumeExistingSlot),
28 ImmediateFormRuntime(crate::FormInstanceId),
29 OrdinaryEvent {
30 event: SemanticId,
31 handler: SemanticId,
32 action_batch: SemanticId,
33 program: SemanticId,
34 },
35 FormSubmit {
36 submission_host: crate::SubmissionHostId,
37 submission_plan: crate::SubmissionPlanId,
38 submit_action: SemanticId,
39 action_batch: SemanticId,
40 serialization_plan: crate::SerializationPlanId,
41 },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ResumeChunkProgramInclusion {
46 pub chunk_id: ResumeChunkId,
47 pub program: ResumeChunkProgram,
48 pub required_boundaries: Vec<ResumeBoundaryId>,
49 pub provenance: SourceProvenance,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ResumeChunkModulePlan {
54 pub module_path_stem: String,
55 pub canonical_module_bytes: String,
56 pub content_hash: String,
57 pub module_path: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ResumeChunk {
62 pub id: ResumeChunkId,
63 pub root_kind: ResumeChunkRootKind,
64 pub root_boundary: Option<ResumeBoundaryId>,
65 pub required_boundaries: Vec<ResumeBoundaryId>,
66 pub programs: Vec<ResumeChunkProgram>,
67 pub dependency_chunks: Vec<ResumeChunkId>,
68 pub module: ResumeChunkModulePlan,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
72pub enum ResumeChunkBlockReason {
73 MissingActivationBoundary,
74 MissingProgram,
75 UnsupportedActivationPolicy,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ResumeChunkBlock {
80 pub root_boundary: Option<ResumeBoundaryId>,
81 pub reason: ResumeChunkBlockReason,
82 pub provenance: SourceProvenance,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ResumeChunkGraph {
87 pub version: u32,
88 pub eager_chunk: ResumeChunkId,
89 pub chunks: Vec<ResumeChunk>,
90 pub inclusions: Vec<ResumeChunkProgramInclusion>,
91 pub blocks: Vec<ResumeChunkBlock>,
92 pub chunk_index: BTreeMap<ResumeChunkId, usize>,
93}
94
95impl ResumeChunkGraph {
96 #[must_use]
97 pub fn chunk(&self, id: &ResumeChunkId) -> Option<&ResumeChunk> {
98 self.chunk_index
99 .get(id)
100 .and_then(|index| self.chunks.get(*index))
101 }
102
103 #[must_use]
104 pub fn chunk_for_root(&self, boundary: &ResumeBoundaryId) -> Option<&ResumeChunk> {
105 self.chunks
106 .iter()
107 .find(|chunk| chunk.root_boundary.as_ref() == Some(boundary))
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
112pub enum ResumeChunkIntegrityCode {
113 DuplicateInclusion,
114 MissingProgram,
115 DependencyCycle,
116 RootCorrespondence,
117 UnrelatedProgram,
118 OrderingOrOutputDrift,
119}
120
121impl ResumeChunkIntegrityCode {
122 #[must_use]
123 pub const fn code(self) -> &'static str {
124 match self {
125 Self::DuplicateInclusion => "PSASM1343",
126 Self::MissingProgram => "PSASM1344",
127 Self::DependencyCycle => "PSASM1345",
128 Self::RootCorrespondence => "PSASM1346",
129 Self::UnrelatedProgram => "PSASM1347",
130 Self::OrderingOrOutputDrift => "PSASM1348",
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct ResumeChunkIntegrityDiagnostic {
137 pub code: ResumeChunkIntegrityCode,
138 pub chunk: Option<ResumeChunkId>,
139 pub message: String,
140}
141
142#[must_use]
143#[allow(clippy::too_many_lines)]
144pub fn build_resume_chunk_graph(model: &ApplicationSemanticModel) -> ResumeChunkGraph {
149 let activation = build_resume_activation_plan(model);
150 let boundaries = build_resume_boundary_graph(model);
151 let eager_id =
152 ResumeChunkId::for_activation_root(ResumeActivationRootKind::Eager, "application");
153 let eager_provenance = boundaries
154 .boundaries
155 .iter()
156 .find(|boundary| boundary.kind == crate::ResumeBoundaryKind::ApplicationRoot)
157 .map(|boundary| boundary.provenance.clone())
158 .expect("canonical application root boundary");
159 let mut eager_programs = vec![
160 ResumeChunkProgram::RuntimeBootstrap,
161 ResumeChunkProgram::RuntimeRegistries,
162 ResumeChunkProgram::EventDelegation,
163 ];
164 let mut eager_boundaries = Vec::new();
165 for decision in activation
166 .decisions
167 .iter()
168 .filter(|decision| decision.policy == ResumeActivationPolicy::Eager)
169 {
170 eager_boundaries.push(decision.boundary.clone());
171 for prerequisite in &decision.prerequisites {
172 match prerequisite {
173 ResumeActivationPrerequisite::PostRestoreRecomputation(slot)
174 | ResumeActivationPrerequisite::RecomputableSlot(slot) => {
175 eager_programs.push(ResumeChunkProgram::PostRestoreRecomputation(slot.clone()));
176 }
177 ResumeActivationPrerequisite::ImmediateFormRuntime(form) => {
178 eager_programs.push(ResumeChunkProgram::ImmediateFormRuntime(form.clone()));
179 }
180 ResumeActivationPrerequisite::ApplicationBootstrap
181 | ResumeActivationPrerequisite::RuntimeRegistryInstallation
182 | ResumeActivationPrerequisite::EventDelegationInstallation
183 | ResumeActivationPrerequisite::ExactInteraction(_)
184 | ResumeActivationPrerequisite::RequiredBoundary(_)
185 | ResumeActivationPrerequisite::RetainedSlot(_) => {}
186 }
187 }
188 }
189 eager_boundaries.sort();
190 eager_boundaries.dedup();
191 eager_programs.sort();
192 eager_programs.dedup();
193 let mut chunks = vec![chunk(
194 eager_id.clone(),
195 ResumeChunkRootKind::Eager,
196 None,
197 eager_boundaries.clone(),
198 eager_programs.clone(),
199 )];
200 let mut inclusions = eager_programs
201 .iter()
202 .cloned()
203 .map(|program| ResumeChunkProgramInclusion {
204 chunk_id: eager_id.clone(),
205 program,
206 required_boundaries: eager_boundaries.clone(),
207 provenance: eager_provenance.clone(),
208 })
209 .collect::<Vec<_>>();
210 let mut blocks = Vec::new();
211
212 for decision in activation.decisions.iter().filter(|decision| {
213 matches!(
214 decision.policy,
215 ResumeActivationPolicy::Interaction
216 | ResumeActivationPolicy::Visible
217 | ResumeActivationPolicy::Manual
218 )
219 }) {
220 let Some(reference) = boundaries
221 .activation_references
222 .iter()
223 .find(|reference| reference.interaction_boundary == decision.boundary)
224 else {
225 blocks.push(ResumeChunkBlock {
226 root_boundary: Some(decision.boundary.clone()),
227 reason: ResumeChunkBlockReason::MissingActivationBoundary,
228 provenance: decision.provenance.clone(),
229 });
230 continue;
231 };
232 let (root_kind, activation_kind) = match decision.policy {
233 ResumeActivationPolicy::Interaction => (
234 ResumeChunkRootKind::Interaction,
235 ResumeActivationRootKind::Event,
236 ),
237 ResumeActivationPolicy::Visible => (
238 ResumeChunkRootKind::Visible,
239 ResumeActivationRootKind::Visible,
240 ),
241 ResumeActivationPolicy::Manual => (
242 ResumeChunkRootKind::Manual,
243 ResumeActivationRootKind::Manual,
244 ),
245 ResumeActivationPolicy::Eager | ResumeActivationPolicy::None => unreachable!(),
246 };
247 let id = ResumeChunkId::for_activation_root(activation_kind, decision.boundary.as_str());
248 let program = match &reference.program {
249 ResumeBoundaryActivationProgram::OrdinaryEvent {
250 declaration_event,
251 handler_method,
252 action_batch,
253 existing_program,
254 ..
255 } => ResumeChunkProgram::OrdinaryEvent {
256 event: declaration_event.clone(),
257 handler: handler_method.clone(),
258 action_batch: action_batch.clone(),
259 program: existing_program.clone(),
260 },
261 ResumeBoundaryActivationProgram::FormSubmit {
262 submission_host,
263 submission_plan,
264 submit_action,
265 action_batch,
266 serialization_plan,
267 ..
268 } => ResumeChunkProgram::FormSubmit {
269 submission_host: submission_host.clone(),
270 submission_plan: submission_plan.clone(),
271 submit_action: submit_action.clone(),
272 action_batch: action_batch.clone(),
273 serialization_plan: serialization_plan.clone(),
274 },
275 };
276 let mut required_boundaries = reference.required_boundaries.clone();
277 required_boundaries.sort();
278 required_boundaries.dedup();
279 chunks.push(chunk(
280 id.clone(),
281 root_kind,
282 Some(decision.boundary.clone()),
283 required_boundaries.clone(),
284 vec![program.clone()],
285 ));
286 inclusions.push(ResumeChunkProgramInclusion {
287 chunk_id: id,
288 program,
289 required_boundaries,
290 provenance: reference.provenance.clone(),
291 });
292 }
293
294 chunks.sort_by(|left, right| {
295 (
296 left.root_kind != ResumeChunkRootKind::Eager,
297 &left.root_boundary,
298 &left.id,
299 )
300 .cmp(&(
301 right.root_kind != ResumeChunkRootKind::Eager,
302 &right.root_boundary,
303 &right.id,
304 ))
305 });
306 inclusions.sort_by(|left, right| {
307 (&left.chunk_id, &left.program).cmp(&(&right.chunk_id, &right.program))
308 });
309 blocks.sort_by(|left, right| {
310 (&left.root_boundary, left.reason).cmp(&(&right.root_boundary, right.reason))
311 });
312 let chunk_index = chunks
313 .iter()
314 .enumerate()
315 .map(|(index, chunk)| (chunk.id.clone(), index))
316 .collect();
317 ResumeChunkGraph {
318 version: RESUME_CHUNK_GRAPH_VERSION,
319 eager_chunk: eager_id,
320 chunks,
321 inclusions,
322 blocks,
323 chunk_index,
324 }
325}
326
327fn chunk(
328 id: ResumeChunkId,
329 root_kind: ResumeChunkRootKind,
330 root_boundary: Option<ResumeBoundaryId>,
331 required_boundaries: Vec<ResumeBoundaryId>,
332 programs: Vec<ResumeChunkProgram>,
333) -> ResumeChunk {
334 let kind = match root_kind {
335 ResumeChunkRootKind::Eager => "boot",
336 ResumeChunkRootKind::Interaction => "event",
337 ResumeChunkRootKind::Visible => "visible",
338 ResumeChunkRootKind::Manual => "manual",
339 };
340 let short = safe_stem(
341 root_boundary
342 .as_ref()
343 .map_or("application", ResumeBoundaryId::as_str),
344 );
345 let canonical_module_bytes = format!(
346 "// chunk={id}\n// kind={root_kind:?}\n// root={}\n// boundaries={}\n// programs={}\nexport {{}};\n",
347 root_boundary.as_ref().map_or("", ResumeBoundaryId::as_str),
348 required_boundaries
349 .iter()
350 .map(ToString::to_string)
351 .collect::<Vec<_>>()
352 .join(","),
353 programs
354 .iter()
355 .map(|program| format!("{program:?}"))
356 .collect::<Vec<_>>()
357 .join(",")
358 );
359 let build_hash = crate::ResumeBuildId::for_public_inputs(&canonical_module_bytes);
360 let content_hash = build_hash
361 .as_str()
362 .strip_prefix("resume-build:")
363 .unwrap_or(build_hash.as_str())
364 .to_string();
365 let module_path_stem = format!("{kind}.{short}");
366 let module_path = format!("{module_path_stem}.{content_hash}.js");
367 ResumeChunk {
368 id,
369 root_kind,
370 root_boundary,
371 required_boundaries,
372 programs,
373 dependency_chunks: Vec::new(),
374 module: ResumeChunkModulePlan {
375 module_path_stem,
376 canonical_module_bytes,
377 content_hash,
378 module_path,
379 },
380 }
381}
382
383fn safe_stem(value: &str) -> String {
384 let mut stem = value
385 .chars()
386 .map(|character| {
387 if character.is_ascii_alphanumeric() {
388 character
389 } else {
390 '-'
391 }
392 })
393 .collect::<String>();
394 while stem.contains("--") {
395 stem = stem.replace("--", "-");
396 }
397 stem.trim_matches('-').chars().take(48).collect()
398}
399
400#[must_use]
401pub fn validate_resume_chunk_graph(
402 model: &ApplicationSemanticModel,
403 graph: &ResumeChunkGraph,
404) -> Vec<ResumeChunkIntegrityDiagnostic> {
405 let canonical = build_resume_chunk_graph(model);
406 let activation = build_resume_activation_plan(model);
407 let mut diagnostics = Vec::new();
408 let mut inclusions = BTreeSet::new();
409 for inclusion in &graph.inclusions {
410 if !inclusions.insert((inclusion.chunk_id.clone(), inclusion.program.clone())) {
411 diagnostics.push(integrity(
412 ResumeChunkIntegrityCode::DuplicateInclusion,
413 Some(inclusion.chunk_id.clone()),
414 "generated program was included more than once in one chunk",
415 ));
416 }
417 if graph.chunk(&inclusion.chunk_id).is_none() {
418 diagnostics.push(integrity(
419 ResumeChunkIntegrityCode::MissingProgram,
420 Some(inclusion.chunk_id.clone()),
421 "program inclusion references an unknown chunk",
422 ));
423 }
424 }
425 for chunk in &graph.chunks {
426 if !chunk.dependency_chunks.is_empty() {
427 diagnostics.push(integrity(
428 ResumeChunkIntegrityCode::DependencyCycle,
429 Some(chunk.id.clone()),
430 "Phase J v1 root chunks cannot depend on other lazy chunks",
431 ));
432 }
433 if chunk.programs.iter().any(|program| {
434 !graph
435 .inclusions
436 .iter()
437 .any(|inclusion| inclusion.chunk_id == chunk.id && inclusion.program == *program)
438 }) {
439 diagnostics.push(integrity(
440 ResumeChunkIntegrityCode::MissingProgram,
441 Some(chunk.id.clone()),
442 "chunk program has no reciprocal inclusion record",
443 ));
444 }
445 }
446 for decision in activation.decisions.iter().filter(|decision| {
447 decision.policy != ResumeActivationPolicy::None
448 && decision.policy != ResumeActivationPolicy::Eager
449 }) {
450 if graph.chunk_for_root(&decision.boundary).is_none() {
451 diagnostics.push(integrity(
452 ResumeChunkIntegrityCode::RootCorrespondence,
453 None,
454 "non-eager activation has no exact lazy root chunk",
455 ));
456 }
457 }
458 if graph.version != RESUME_CHUNK_GRAPH_VERSION || graph != &canonical {
459 diagnostics.push(integrity(
460 ResumeChunkIntegrityCode::OrderingOrOutputDrift,
461 None,
462 "chunk graph drifted from canonical roots, closure, module bytes, or order",
463 ));
464 }
465 diagnostics.sort_by(|left, right| {
466 (left.code, &left.chunk, left.message.as_str()).cmp(&(
467 right.code,
468 &right.chunk,
469 right.message.as_str(),
470 ))
471 });
472 diagnostics.dedup();
473 diagnostics
474}
475
476fn integrity(
477 code: ResumeChunkIntegrityCode,
478 chunk: Option<ResumeChunkId>,
479 message: &str,
480) -> ResumeChunkIntegrityDiagnostic {
481 ResumeChunkIntegrityDiagnostic {
482 code,
483 chunk,
484 message: message.to_string(),
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn creates_one_eager_and_one_isolated_chunk_per_interaction() {
494 let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
495 "src/Chunks.tsx",
496 r#"
497@component("x-chunk-child") class Child {
498 a = state(1); b = state(2);
499 @action() first() { this.a++; }
500 @action() second() { this.b++; }
501 render() { return <><button onClick={() => this.first()}>A</button><button onClick={() => this.second()}>B</button></>; }
502}
503@component("x-chunk-page") @route("/") class Page { render() { return <Child />; } }
504"#,
505 ));
506 let graph = build_resume_chunk_graph(&model);
507 assert!(validate_resume_chunk_graph(&model, &graph).is_empty());
508 assert_eq!(graph.chunks.len(), 3);
509 assert_eq!(
510 graph
511 .chunks
512 .iter()
513 .filter(|chunk| chunk.root_kind == ResumeChunkRootKind::Eager)
514 .count(),
515 1
516 );
517 let lazy = graph
518 .chunks
519 .iter()
520 .filter(|chunk| chunk.root_kind == ResumeChunkRootKind::Interaction)
521 .collect::<Vec<_>>();
522 assert_eq!(lazy.len(), 2);
523 assert!(lazy.iter().all(|chunk| {
524 chunk.programs.len() == 1
525 && chunk.dependency_chunks.is_empty()
526 && chunk.module.module_path.starts_with("event.")
527 }));
528 assert_ne!(lazy[0].programs, lazy[1].programs);
529 }
530
531 #[test]
532 fn chunk_output_is_deterministic_under_source_reversal() {
533 let first = presolve_parser::parse_file(
534 "src/A.tsx",
535 r#"@component("x-a") @route("/a") class A { @action() go() {} render() { return <button onClick={() => this.go()}>A</button>; } }"#,
536 );
537 let second = presolve_parser::parse_file(
538 "src/B.tsx",
539 r#"@component("x-b") @route("/b") class B { @action() go() {} render() { return <button onClick={() => this.go()}>B</button>; } }"#,
540 );
541 let forward = crate::build_application_semantic_model_for_unit(
542 &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
543 );
544 let reverse = crate::build_application_semantic_model_for_unit(
545 &crate::CompilationUnit::from_parsed_files(vec![second, first]),
546 );
547 assert_eq!(
548 build_resume_chunk_graph(&forward),
549 build_resume_chunk_graph(&reverse)
550 );
551 }
552
553 #[test]
554 fn reserves_the_complete_j5_integrity_range() {
555 assert_eq!(
556 [
557 ResumeChunkIntegrityCode::DuplicateInclusion,
558 ResumeChunkIntegrityCode::MissingProgram,
559 ResumeChunkIntegrityCode::DependencyCycle,
560 ResumeChunkIntegrityCode::RootCorrespondence,
561 ResumeChunkIntegrityCode::UnrelatedProgram,
562 ResumeChunkIntegrityCode::OrderingOrOutputDrift,
563 ]
564 .map(ResumeChunkIntegrityCode::code),
565 [
566 "PSASM1343",
567 "PSASM1344",
568 "PSASM1345",
569 "PSASM1346",
570 "PSASM1347",
571 "PSASM1348",
572 ]
573 );
574 }
575}