Skip to main content

presolve_compiler/
context_resume.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    ContextValueSlotId, ContextValueSourceId, ExecutionBoundary, RuntimeContextRegistry,
5    RuntimeContextSourceKind, SourceProvenance,
6};
7
8/// Stable serialized-state identity distinct from the live runtime slot.
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct ContextResumeSlotId(String);
12
13impl ContextResumeSlotId {
14    #[must_use]
15    pub fn for_source(source: &ContextValueSourceId) -> Self {
16        let source = match source {
17            ContextValueSourceId::Provider(provider) => provider.as_str().to_string(),
18            ContextValueSourceId::ContextDefault(context) => {
19                format!("{}/default", context.as_str())
20            }
21        };
22        Self(format!("{source}/resume:context-slot"))
23    }
24
25    #[must_use]
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum ContextSlotResumeStatus {
34    Uninitialized,
35    Initialized,
36    Failed,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ContextResumeRecord {
41    pub source: ContextValueSourceId,
42    pub context: crate::ContextId,
43    pub runtime_slot: ContextValueSlotId,
44    pub resume_slot: ContextResumeSlotId,
45    pub semantic_type: crate::SemanticTypeId,
46    pub source_kind: RuntimeContextSourceKind,
47    pub initial_status: ContextSlotResumeStatus,
48    pub action_batches: Vec<crate::SemanticId>,
49    pub consumers: Vec<crate::ConsumerId>,
50    pub boundary: ExecutionBoundary,
51    pub provenance: SourceProvenance,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Default)]
55pub struct ContextResumePlan {
56    pub records: Vec<ContextResumeRecord>,
57}
58
59#[must_use]
60pub fn build_context_resume_plan(
61    registry: &RuntimeContextRegistry,
62    updates: &crate::ContextUpdatePlan,
63) -> ContextResumePlan {
64    ContextResumePlan {
65        records: registry
66            .sources
67            .iter()
68            .map(|source| ContextResumeRecord {
69                source: source.source.clone(),
70                context: source.context.clone(),
71                runtime_slot: source.slot.clone(),
72                resume_slot: ContextResumeSlotId::for_source(&source.source),
73                semantic_type: source.semantic_type.clone(),
74                source_kind: source.source_kind,
75                initial_status: ContextSlotResumeStatus::Uninitialized,
76                action_batches: updates
77                    .actions
78                    .iter()
79                    .filter(|action| action.invalidated_sources.contains(&source.source))
80                    .map(|action| action.action_batch.clone())
81                    .collect(),
82                consumers: registry
83                    .consumers
84                    .iter()
85                    .filter(|consumer| consumer.selected_source == source.source)
86                    .map(|consumer| consumer.consumer.clone())
87                    .collect(),
88                boundary: source.boundary,
89                provenance: source.provenance.clone(),
90            })
91            .collect(),
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use crate::{
98        build_application_semantic_model, build_resume_manifest, build_resume_plan,
99        ContextSlotResumeStatus,
100    };
101
102    #[test]
103    fn plans_distinct_uninitialized_resume_slots_for_runtime_context_sources() {
104        let model = build_application_semantic_model(&presolve_parser::parse_file(
105            "src/App.tsx",
106            r#"
107@component("x-app")
108class App extends Component {
109  @context() locale: string = "en";
110  @consume(App.locale) locale!: string;
111  render() { return <main />; }
112}
113"#,
114        ));
115        let plan = build_resume_plan(&model);
116        let record = plan
117            .contexts
118            .records
119            .first()
120            .expect("Context resume record");
121        assert_eq!(
122            record.initial_status,
123            ContextSlotResumeStatus::Uninitialized
124        );
125        assert_ne!(record.resume_slot.as_str(), record.runtime_slot.as_str());
126        let manifest = build_resume_manifest(&model);
127        assert_eq!(manifest.schema_version, 7);
128        assert_eq!(
129            manifest
130                .phase_i_component_resume_records
131                .iter()
132                .filter(|record| matches!(
133                    record,
134                    crate::ResumeManifestPhaseIComponentResumeRecord::ContextSlot { .. }
135                ))
136                .count(),
137            1
138        );
139    }
140}