Skip to main content

presolve_compiler/
runtime_resource_artifact.rs

1//! N6-C3 deterministic Resource declaration and activation artifact.
2//!
3//! This is a projection of already-resolved compiler products. Its
4//! execution-facing form contains the exact host-provided runtime module
5//! location consumed by the generated browser runtime.
6
7use std::collections::BTreeSet;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    semantic_type_text, ApplicationSemanticModel, ResourceEndpointResolutionOutcome,
13    ResourceLifecycleState, SemanticPackageRuntimeModuleKey, SemanticPackageRuntimeModuleTable,
14};
15
16pub const RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION: u32 = 1;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct RuntimeResourceArtifact {
21    pub schema_version: u32,
22    pub declarations: Vec<RuntimeResourceArtifactDeclaration>,
23    pub activations: Vec<RuntimeResourceArtifactActivation>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct RuntimeResourceArtifactDeclaration {
29    pub id: String,
30    pub owner_component: String,
31    pub key: String,
32    pub data_type: String,
33    pub error_type: String,
34    pub execution_boundary: String,
35    pub input_dependencies: Vec<String>,
36    pub retry_policy: String,
37    pub invalidation_policy: String,
38    pub endpoint: RuntimeResourceArtifactEndpoint,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct RuntimeResourceArtifactEndpoint {
44    pub package: String,
45    pub version: String,
46    pub integrity: String,
47    pub export: String,
48    pub type_signature: String,
49    pub runtime_module: String,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub runtime_location: Option<String>,
52    pub resume_policy: String,
53    pub cancellation: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct RuntimeResourceArtifactActivation {
59    pub id: String,
60    pub declaration: String,
61    pub component_instance: String,
62    pub state: String,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub generation: Option<u64>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum RuntimeResourceArtifactValidationError {
69    UnsupportedSchemaVersion {
70        actual: u32,
71    },
72    DuplicateDeclarationId {
73        id: String,
74    },
75    DuplicateActivationId {
76        id: String,
77    },
78    MissingEndpointCoordinate {
79        declaration: String,
80    },
81    InvalidLifecycleGeneration {
82        activation: String,
83    },
84    UnknownActivationDeclaration {
85        activation: String,
86        declaration: String,
87    },
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum RuntimeResourceArtifactBuildError {
92    MissingRuntimeModuleLocation { declaration: String },
93}
94
95/// Projects immutable Resource declaration and instance-activation records.
96///
97/// # Panics
98///
99/// Panics when an internal declaration has no resolved resource endpoint. That
100/// would violate the N6-C declaration-projection invariant.
101#[must_use]
102pub fn build_runtime_resource_artifact(
103    model: &ApplicationSemanticModel,
104) -> RuntimeResourceArtifact {
105    let declarations = model
106        .resource_declarations
107        .values()
108        .map(|declaration| {
109            let endpoint = model
110                .resource_endpoint_resolutions
111                .iter()
112                .find(|resolution| {
113                    resolution.owner_component == declaration.owner_component
114                        && resolution.field == declaration.name
115                })
116                .and_then(|resolution| match &resolution.outcome {
117                    ResourceEndpointResolutionOutcome::Resolved(endpoint) => Some(endpoint),
118                    _ => None,
119                })
120                .expect("projected Resource declaration must have resolved endpoint");
121            RuntimeResourceArtifactDeclaration {
122                id: declaration.id.as_str().to_string(),
123                owner_component: declaration.owner_component.as_str().to_string(),
124                key: declaration.key.clone(),
125                data_type: semantic_type_text(&declaration.data_type),
126                error_type: semantic_type_text(&declaration.error_type),
127                execution_boundary: format!("{:?}", declaration.execution_boundary),
128                input_dependencies: declaration
129                    .input_dependencies
130                    .iter()
131                    .map(|dependency| dependency.as_str().to_string())
132                    .collect(),
133                retry_policy: format!("{:?}", declaration.retry_policy),
134                invalidation_policy: format!("{:?}", declaration.invalidation_policy),
135                endpoint: RuntimeResourceArtifactEndpoint {
136                    package: endpoint.package.clone(),
137                    version: endpoint.version.clone(),
138                    integrity: endpoint.integrity.clone(),
139                    export: endpoint.export.clone(),
140                    type_signature: endpoint.type_signature.clone(),
141                    runtime_module: endpoint.runtime_module.clone(),
142                    runtime_location: None,
143                    resume_policy: endpoint.resume_policy.clone(),
144                    cancellation: format!("{:?}", endpoint.endpoint.cancellation),
145                },
146            }
147        })
148        .collect();
149    let activations = model
150        .resource_activations
151        .values()
152        .map(|activation| {
153            let (state, generation) = resource_lifecycle_artifact_state(activation.state);
154            RuntimeResourceArtifactActivation {
155                id: activation.id.as_str().to_string(),
156                declaration: activation.declaration.as_str().to_string(),
157                component_instance: activation.component_instance.as_str().to_string(),
158                state: state.to_string(),
159                generation,
160            }
161        })
162        .collect();
163    RuntimeResourceArtifact {
164        schema_version: RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION,
165        declarations,
166        activations,
167    }
168}
169
170/// Produces the execution-facing artifact only when every Resource endpoint
171/// has an explicit host-supplied location for its exact package coordinate.
172pub fn build_runtime_resource_artifact_with_modules(
173    model: &ApplicationSemanticModel,
174    modules: &SemanticPackageRuntimeModuleTable,
175) -> Result<RuntimeResourceArtifact, RuntimeResourceArtifactBuildError> {
176    let mut artifact = build_runtime_resource_artifact(model);
177    for declaration in &mut artifact.declarations {
178        let key = SemanticPackageRuntimeModuleKey {
179            package: declaration.endpoint.package.clone(),
180            version: declaration.endpoint.version.clone(),
181            integrity: declaration.endpoint.integrity.clone(),
182            runtime_module: declaration.endpoint.runtime_module.clone(),
183        };
184        declaration.endpoint.runtime_location = Some(
185            modules
186                .resolve(&key)
187                .ok_or_else(
188                    || RuntimeResourceArtifactBuildError::MissingRuntimeModuleLocation {
189                        declaration: declaration.id.clone(),
190                    },
191                )?
192                .to_string(),
193        );
194    }
195    Ok(artifact)
196}
197
198#[must_use]
199pub fn runtime_resource_artifact_json(artifact: &RuntimeResourceArtifact) -> String {
200    serde_json::to_string_pretty(artifact).expect("resource artifact should serialize") + "\n"
201}
202
203/// Validates the exact identity and endpoint prerequisites required by the
204/// Resource runtime boundary. Callers must reject the artifact on any error.
205#[must_use]
206pub fn validate_runtime_resource_artifact(
207    artifact: &RuntimeResourceArtifact,
208) -> Vec<RuntimeResourceArtifactValidationError> {
209    let mut errors = Vec::new();
210    if artifact.schema_version != RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION {
211        errors.push(
212            RuntimeResourceArtifactValidationError::UnsupportedSchemaVersion {
213                actual: artifact.schema_version,
214            },
215        );
216    }
217    let mut declarations = BTreeSet::new();
218    for declaration in &artifact.declarations {
219        if !declarations.insert(declaration.id.clone()) {
220            errors.push(
221                RuntimeResourceArtifactValidationError::DuplicateDeclarationId {
222                    id: declaration.id.clone(),
223                },
224            );
225        }
226        if declaration.endpoint.package.is_empty()
227            || declaration.endpoint.version.is_empty()
228            || declaration.endpoint.integrity.is_empty()
229            || declaration.endpoint.export.is_empty()
230            || declaration.endpoint.runtime_module.is_empty()
231        {
232            errors.push(
233                RuntimeResourceArtifactValidationError::MissingEndpointCoordinate {
234                    declaration: declaration.id.clone(),
235                },
236            );
237        }
238    }
239    let mut activations = BTreeSet::new();
240    for activation in &artifact.activations {
241        if !activations.insert(activation.id.clone()) {
242            errors.push(
243                RuntimeResourceArtifactValidationError::DuplicateActivationId {
244                    id: activation.id.clone(),
245                },
246            );
247        }
248        let generation_required = matches!(
249            activation.state.as_str(),
250            "pending" | "ready" | "failed" | "cancelled"
251        );
252        if generation_required != activation.generation.is_some() {
253            errors.push(
254                RuntimeResourceArtifactValidationError::InvalidLifecycleGeneration {
255                    activation: activation.id.clone(),
256                },
257            );
258        }
259        if !declarations.contains(&activation.declaration) {
260            errors.push(
261                RuntimeResourceArtifactValidationError::UnknownActivationDeclaration {
262                    activation: activation.id.clone(),
263                    declaration: activation.declaration.clone(),
264                },
265            );
266        }
267    }
268    errors
269}
270
271fn resource_lifecycle_artifact_state(state: ResourceLifecycleState) -> (&'static str, Option<u64>) {
272    match state {
273        ResourceLifecycleState::Idle => ("idle", None),
274        ResourceLifecycleState::Pending { generation } => ("pending", Some(generation)),
275        ResourceLifecycleState::Ready { generation } => ("ready", Some(generation)),
276        ResourceLifecycleState::Failed { generation } => ("failed", Some(generation)),
277        ResourceLifecycleState::Cancelled { generation } => ("cancelled", Some(generation)),
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use crate::{
284        build_application_semantic_model_for_unit_with_packages, build_runtime_resource_artifact,
285        build_runtime_resource_artifact_with_modules, parse_semantic_package_contract,
286        runtime_resource_artifact_json, validate_runtime_resource_artifact, CompilationUnit,
287        SemanticPackageResolutionTable, SemanticPackageRuntimeModuleKey,
288        SemanticPackageRuntimeModuleTable, RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION,
289    };
290
291    fn model() -> crate::ApplicationSemanticModel {
292        let unit = CompilationUnit::parse_sources([(
293            "src/Profile.tsx",
294            r#"
295import { loadProfile } from "profile-service";
296@component("x-profile")
297class Profile extends Component {
298  @resource("loadProfile") profile!: Resource<string, string>;
299  render() { return <div>Profile</div>; }
300}
301"#,
302        )]);
303        let contract = parse_semantic_package_contract(
304            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
305        )
306        .expect("resource contract");
307        let mut packages = SemanticPackageResolutionTable::default();
308        packages.insert("profile-service".into(), contract).unwrap();
309        build_application_semantic_model_for_unit_with_packages(&unit, &packages)
310    }
311
312    #[test]
313    fn projects_resolved_resource_declaration_and_idle_activation_deterministically() {
314        let model = model();
315        let artifact = build_runtime_resource_artifact(&model);
316        assert_eq!(
317            artifact.schema_version,
318            RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION
319        );
320        assert_eq!(artifact.declarations.len(), 1);
321        assert_eq!(artifact.declarations[0].endpoint.package, "profile-service");
322        assert_eq!(artifact.declarations[0].endpoint.export, "loadProfile");
323        assert_eq!(artifact.activations.len(), 1);
324        assert_eq!(artifact.activations[0].state, "idle");
325        assert!(validate_runtime_resource_artifact(&artifact).is_empty());
326        assert_eq!(
327            runtime_resource_artifact_json(&artifact),
328            runtime_resource_artifact_json(&build_runtime_resource_artifact(&model))
329        );
330    }
331
332    #[test]
333    fn rejects_malformed_resource_artifact_identity_and_lifecycle_records() {
334        let model = model();
335        let mut artifact = build_runtime_resource_artifact(&model);
336        artifact.schema_version = 9;
337        artifact.declarations[0].endpoint.integrity.clear();
338        artifact.activations[0].declaration = "resource:missing".to_string();
339        artifact.activations[0].state = "ready".to_string();
340
341        let errors = validate_runtime_resource_artifact(&artifact);
342        assert!(errors.iter().any(|error| matches!(
343            error,
344            crate::RuntimeResourceArtifactValidationError::UnsupportedSchemaVersion { actual: 9 }
345        )));
346        assert!(errors.iter().any(|error| matches!(
347            error,
348            crate::RuntimeResourceArtifactValidationError::MissingEndpointCoordinate { .. }
349        )));
350        assert!(errors.iter().any(|error| matches!(
351            error,
352            crate::RuntimeResourceArtifactValidationError::InvalidLifecycleGeneration { .. }
353        )));
354        assert!(errors.iter().any(|error| matches!(
355            error,
356            crate::RuntimeResourceArtifactValidationError::UnknownActivationDeclaration { .. }
357        )));
358    }
359
360    #[test]
361    fn requires_exact_runtime_module_location_for_execution_facing_artifact() {
362        let model = model();
363        let mut modules = SemanticPackageRuntimeModuleTable::default();
364        assert!(matches!(
365            build_runtime_resource_artifact_with_modules(&model, &modules),
366            Err(crate::RuntimeResourceArtifactBuildError::MissingRuntimeModuleLocation { .. })
367        ));
368        modules
369            .insert(
370                SemanticPackageRuntimeModuleKey {
371                    package: "profile-service".into(),
372                    version: "1.2.3".into(),
373                    integrity:
374                        "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
375                            .into(),
376                    runtime_module: "dist/load-profile.js".into(),
377                },
378                "./vendor/profile-service.js".into(),
379            )
380            .unwrap();
381        let artifact = build_runtime_resource_artifact_with_modules(&model, &modules).unwrap();
382        assert_eq!(
383            artifact.declarations[0]
384                .endpoint
385                .runtime_location
386                .as_deref(),
387            Some("./vendor/profile-service.js")
388        );
389    }
390}