1use std::collections::BTreeSet;
4
5#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
6pub enum ProductionCleanupKind {
7 ActivationDispatch,
8 EventAndBindingIndex,
9 FormControlAndBindingIndex,
10 EffectSubscription,
11 ContextConsumerBinding,
12 SlotAndStructuralRegistry,
13 ComputedCache,
14 StateStorage,
15 FormInstanceStorage,
16 ContextProviderSlot,
17 ResumeBoundaryAndAnchor,
18 ComponentInstance,
19 DomAnchor,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct ProductionOwnedRuntimeRecord {
24 pub owner_id: String,
25 pub initialization_ordinal: u32,
26 pub kind: ProductionCleanupKind,
27 pub record_id: String,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct ProductionDestroyPlan {
32 pub detached_activation_ids: Vec<String>,
33 pub cleanup_records: Vec<ProductionOwnedRuntimeRecord>,
34}
35
36#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
37pub enum ProductionCleanupClosureViolation {
38 DuplicateRecord(String),
39 ForeignOwner(String),
40 MissingRecord(String),
41 OrderingMismatch,
42}
43
44#[must_use]
46pub fn build_production_destroy_plan(
47 destroyed_owner_ids: &[String],
48 records: &[ProductionOwnedRuntimeRecord],
49 pending_activation_ids: &[String],
50) -> ProductionDestroyPlan {
51 let owners = destroyed_owner_ids.iter().collect::<BTreeSet<_>>();
52 let mut cleanup_records = records
53 .iter()
54 .filter(|record| owners.contains(&record.owner_id))
55 .cloned()
56 .collect::<Vec<_>>();
57 cleanup_records.sort_by(|left, right| {
58 (right.initialization_ordinal, right.kind, &right.record_id).cmp(&(
59 left.initialization_ordinal,
60 left.kind,
61 &left.record_id,
62 ))
63 });
64 let mut detached_activation_ids = pending_activation_ids.to_vec();
65 detached_activation_ids.sort();
66 detached_activation_ids.dedup();
67 ProductionDestroyPlan {
68 detached_activation_ids,
69 cleanup_records,
70 }
71}
72
73#[must_use]
75pub fn validate_production_cleanup_closure(
76 destroyed_owner_ids: &[String],
77 required_records: &[ProductionOwnedRuntimeRecord],
78 plan: &ProductionDestroyPlan,
79) -> Vec<ProductionCleanupClosureViolation> {
80 let owners = destroyed_owner_ids.iter().collect::<BTreeSet<_>>();
81 let required = required_records
82 .iter()
83 .filter(|record| owners.contains(&record.owner_id))
84 .map(|record| record.record_id.clone())
85 .collect::<BTreeSet<_>>();
86 let mut actual = BTreeSet::new();
87 let mut violations = Vec::new();
88 for record in &plan.cleanup_records {
89 if !owners.contains(&record.owner_id) {
90 violations.push(ProductionCleanupClosureViolation::ForeignOwner(
91 record.record_id.clone(),
92 ));
93 }
94 if !actual.insert(record.record_id.clone()) {
95 violations.push(ProductionCleanupClosureViolation::DuplicateRecord(
96 record.record_id.clone(),
97 ));
98 }
99 }
100 violations.extend(
101 required
102 .difference(&actual)
103 .cloned()
104 .map(ProductionCleanupClosureViolation::MissingRecord),
105 );
106 if plan.cleanup_records.windows(2).any(|pair| {
107 (
108 pair[0].initialization_ordinal,
109 pair[0].kind,
110 &pair[0].record_id,
111 ) < (
112 pair[1].initialization_ordinal,
113 pair[1].kind,
114 &pair[1].record_id,
115 )
116 }) {
117 violations.push(ProductionCleanupClosureViolation::OrderingMismatch);
118 }
119 violations.sort();
120 violations.dedup();
121 violations
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 #[test]
128 fn k12_releases_only_destroyed_owners_in_reverse_initialization_order() {
129 let records = vec![
130 ProductionOwnedRuntimeRecord {
131 owner_id: "a".to_string(),
132 initialization_ordinal: 1,
133 kind: ProductionCleanupKind::StateStorage,
134 record_id: "state".to_string(),
135 },
136 ProductionOwnedRuntimeRecord {
137 owner_id: "a".to_string(),
138 initialization_ordinal: 2,
139 kind: ProductionCleanupKind::DomAnchor,
140 record_id: "anchor".to_string(),
141 },
142 ProductionOwnedRuntimeRecord {
143 owner_id: "b".to_string(),
144 initialization_ordinal: 3,
145 kind: ProductionCleanupKind::ComponentInstance,
146 record_id: "other".to_string(),
147 },
148 ];
149 let plan = build_production_destroy_plan(
150 &["a".to_string()],
151 &records,
152 &["activation:a".to_string(), "activation:a".to_string()],
153 );
154 assert_eq!(
155 plan.cleanup_records
156 .iter()
157 .map(|record| record.record_id.as_str())
158 .collect::<Vec<_>>(),
159 vec!["anchor", "state"]
160 );
161 assert_eq!(plan.detached_activation_ids, vec!["activation:a"]);
162 }
163
164 #[test]
165 fn k13_covers_forms_context_effect_and_resume_without_authored_cleanup() {
166 let records = [
167 (
168 ProductionCleanupKind::FormControlAndBindingIndex,
169 "form-control",
170 ),
171 (ProductionCleanupKind::EffectSubscription, "effect"),
172 (ProductionCleanupKind::ContextConsumerBinding, "consumer"),
173 (ProductionCleanupKind::FormInstanceStorage, "form-state"),
174 (ProductionCleanupKind::ContextProviderSlot, "provider"),
175 (ProductionCleanupKind::ResumeBoundaryAndAnchor, "resume"),
176 ]
177 .into_iter()
178 .enumerate()
179 .map(
180 |(ordinal, (kind, record_id))| ProductionOwnedRuntimeRecord {
181 owner_id: "destroyed".to_string(),
182 initialization_ordinal: u32::try_from(ordinal).expect("fixture ordinal"),
183 kind,
184 record_id: record_id.to_string(),
185 },
186 )
187 .collect::<Vec<_>>();
188 let plan = build_production_destroy_plan(
189 &["destroyed".to_string()],
190 &records,
191 &["activation:destroyed".to_string()],
192 );
193 assert!(
194 validate_production_cleanup_closure(&["destroyed".to_string()], &records, &plan)
195 .is_empty()
196 );
197 assert_eq!(
198 plan.cleanup_records.first().expect("record").record_id,
199 "resume"
200 );
201 }
202
203 #[test]
204 fn k16_one_hundred_cleanup_cycles_return_every_owned_registry_to_baseline() {
205 let kinds = [
206 ProductionCleanupKind::ActivationDispatch,
207 ProductionCleanupKind::EventAndBindingIndex,
208 ProductionCleanupKind::FormControlAndBindingIndex,
209 ProductionCleanupKind::EffectSubscription,
210 ProductionCleanupKind::ContextConsumerBinding,
211 ProductionCleanupKind::SlotAndStructuralRegistry,
212 ProductionCleanupKind::ComputedCache,
213 ProductionCleanupKind::StateStorage,
214 ProductionCleanupKind::FormInstanceStorage,
215 ProductionCleanupKind::ContextProviderSlot,
216 ProductionCleanupKind::ResumeBoundaryAndAnchor,
217 ProductionCleanupKind::ComponentInstance,
218 ProductionCleanupKind::DomAnchor,
219 ];
220 let mut instance_registry = BTreeSet::new();
221 let global_program_cache = BTreeSet::from(["runtime-bootstrap", "runtime-registries"]);
222 let baseline_instance_count = instance_registry.len();
223 let baseline_global_count = global_program_cache.len();
224
225 for cycle in 0_u32..100 {
226 let owner = format!("component:{cycle}");
227 let records = kinds
228 .iter()
229 .enumerate()
230 .map(|(ordinal, kind)| ProductionOwnedRuntimeRecord {
231 owner_id: owner.clone(),
232 initialization_ordinal: u32::try_from(ordinal).expect("fixture ordinal"),
233 kind: *kind,
234 record_id: format!("{owner}:{kind:?}"),
235 })
236 .collect::<Vec<_>>();
237 instance_registry.extend(records.iter().map(|record| record.record_id.clone()));
238 let plan = build_production_destroy_plan(
239 std::slice::from_ref(&owner),
240 &records,
241 &[format!("activation:{owner}")],
242 );
243 assert!(validate_production_cleanup_closure(
244 std::slice::from_ref(&owner),
245 &records,
246 &plan
247 )
248 .is_empty());
249 for record in plan.cleanup_records {
250 assert!(instance_registry.remove(&record.record_id));
251 }
252 assert_eq!(instance_registry.len(), baseline_instance_count);
253 assert_eq!(global_program_cache.len(), baseline_global_count);
254 }
255 }
256}