1use serde::{Deserialize, Serialize};
2
3use crate::{
4 build_computed_instance_slot_registry, build_ordinary_template_instance_registry,
5 build_runtime_component_registry, build_state_instance_storage_registry,
6 lower_components_to_ir, semantic_type_text, ApplicationSemanticModel,
7 OptimizedComponentIrReport, OrdinaryTemplateBindingKind, OrdinaryTemplateTargetKind,
8 RuntimeComponentRegistry, SerializationCompatibility,
9};
10
11pub const RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION: u32 = 4;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct RuntimeComponentArtifact {
16 pub schema_version: u32,
17 pub component_definitions: Vec<SerializedComponentDefinition>,
18 pub instances: Vec<SerializedComponentInstance>,
19 pub initialization_batches: Vec<SerializedComponentBatch>,
20 pub slot_binding_programs: Vec<SerializedSlotBinding>,
21 pub instance_context_bindings: Vec<SerializedInstanceContextBinding>,
22 pub ordinary_template_targets: Vec<SerializedOrdinaryTemplateTarget>,
23 pub ordinary_template_bindings: Vec<SerializedOrdinaryTemplateBinding>,
24 pub ordinary_template_events: Vec<SerializedOrdinaryTemplateEvent>,
25 pub destruction: SerializedDestructionMetadata,
26 pub structural_programs: Vec<SerializedStructuralComponentProgram>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct SerializedComponentDefinition {
31 pub component: String,
32 pub template: String,
33 pub slots: Vec<String>,
34 pub boundary: String,
35}
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct SerializedComponentInstance {
38 pub instance: String,
39 pub component: String,
40 pub invocation: Option<String>,
41 pub parent: Option<String>,
42 pub depth: usize,
43 pub initialization_batch: usize,
44 pub ordinary_template_targets: Vec<String>,
45 pub ordinary_template_bindings: Vec<String>,
46 pub ordinary_template_events: Vec<String>,
47 pub state_slots: Vec<SerializedRuntimeStateSlot>,
48 #[serde(skip_serializing_if = "Vec::is_empty")]
49 pub computed_slots: Vec<SerializedRuntimeComputedSlot>,
50 pub context_prefix: String,
51 pub instruction_indices: Vec<usize>,
52 pub structural_region: Option<String>,
53}
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct SerializedRuntimeStateSlot {
56 pub slot_id: String,
57 pub state_id: String,
58 pub storage_id: String,
59 pub initial_value: crate::SerializableValue,
60 pub semantic_type: String,
61 pub serializable: bool,
62}
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct SerializedRuntimeComputedSlot {
65 pub computed_id: String,
66 pub cache_slot_id: String,
67 pub dirty_slot_id: String,
68 pub dirty_initial_value: bool,
69}
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct SerializedOrdinaryTemplateTarget {
72 pub id: String,
73 pub component_instance_id: String,
74 pub component_id: String,
75 pub template_entity_id: String,
76 pub kind: String,
77}
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct SerializedOrdinaryTemplateBinding {
80 pub id: String,
81 pub component_instance_id: String,
82 pub component_id: String,
83 pub declaration_binding_id: String,
84 pub target_id: String,
85 pub kind: String,
86 pub state_storage_ids: Vec<String>,
87 pub computed_ids: Vec<String>,
88 pub program_id: String,
89 pub expression: Option<String>,
90 pub attribute_name: Option<String>,
91}
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct SerializedOrdinaryTemplateEvent {
94 pub component_instance_id: String,
95 pub component_id: String,
96 pub target_id: String,
97 pub declaration_event_id: String,
98 pub event_type: String,
99 pub handler_method_id: String,
100 pub action_batch_id: Option<String>,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
102 pub arguments: Vec<crate::component_graph::SerializableValue>,
103 pub program_id: String,
104}
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct SerializedComponentBatch {
107 pub index: usize,
108 pub instances: Vec<String>,
109 pub context_sources: Vec<String>,
110 pub slot_bindings: Vec<String>,
111}
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct SerializedSlotBinding {
114 pub binding: String,
115 pub caller_instance: String,
116 pub callee_instance: String,
117 pub slot: String,
118 pub outlet: String,
119 pub fragment: String,
120 pub content_owner_instance: String,
121}
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct SerializedInstanceContextBinding {
124 pub consumer_instance: String,
125 pub selected_source: String,
126 pub provider_source: Option<String>,
127 pub runtime_slot: String,
128}
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct SerializedDestructionMetadata {
131 pub operation: String,
132 pub enabled: bool,
133}
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct SerializedStructuralComponentProgram {
136 pub region: String,
137 pub template_instances: Vec<String>,
138 pub destroy_order: Vec<String>,
139 pub create_order: Vec<String>,
140}
141
142const fn target_kind_text(kind: OrdinaryTemplateTargetKind) -> &'static str {
143 match kind {
144 OrdinaryTemplateTargetKind::Element => "element",
145 OrdinaryTemplateTargetKind::AttributeOrPropertyHost => "attribute_or_property_host",
146 OrdinaryTemplateTargetKind::EventHost => "event_host",
147 OrdinaryTemplateTargetKind::ConditionalBoundary => "conditional_boundary",
148 OrdinaryTemplateTargetKind::ListBoundary => "list_boundary",
149 OrdinaryTemplateTargetKind::FormControlHost => "form_control_host",
150 OrdinaryTemplateTargetKind::FormSubmissionHost => "form_submission_host",
151 }
152}
153
154const fn binding_kind_text(kind: OrdinaryTemplateBindingKind) -> &'static str {
155 match kind {
156 OrdinaryTemplateBindingKind::Text => "text",
157 OrdinaryTemplateBindingKind::Attribute => "attribute",
158 OrdinaryTemplateBindingKind::Property => "property",
159 OrdinaryTemplateBindingKind::Conditional => "conditional",
160 OrdinaryTemplateBindingKind::List => "list",
161 OrdinaryTemplateBindingKind::FormControl => "form_control",
162 }
163}
164
165#[must_use]
166#[allow(clippy::too_many_lines)]
167pub fn build_runtime_component_artifact(
168 model: &ApplicationSemanticModel,
169 optimized: &OptimizedComponentIrReport,
170) -> RuntimeComponentArtifact {
171 let ordinary = build_ordinary_template_instance_registry(model);
172 let ir = lower_components_to_ir(model);
173 let state_slots = build_state_instance_storage_registry(model, &ir);
174 let computed_slots = build_computed_instance_slot_registry(model, &ir);
175 let mut artifact = artifact_from_registry(&build_runtime_component_registry(model, optimized));
176 artifact.ordinary_template_targets = ordinary
177 .targets
178 .iter()
179 .map(|target| SerializedOrdinaryTemplateTarget {
180 id: target.target_id.to_string(),
181 component_instance_id: target.component_instance_id.to_string(),
182 component_id: target.component_id.to_string(),
183 template_entity_id: target.template_entity_id.to_string(),
184 kind: target_kind_text(target.target_kind).to_string(),
185 })
186 .collect();
187 artifact.ordinary_template_bindings = ordinary
188 .bindings
189 .iter()
190 .map(|binding| SerializedOrdinaryTemplateBinding {
191 id: binding.instance_binding_id.to_string(),
192 component_instance_id: binding.component_instance_id.to_string(),
193 component_id: binding.component_id.to_string(),
194 declaration_binding_id: binding.declaration_binding_id.to_string(),
195 target_id: binding.target_id.to_string(),
196 kind: binding_kind_text(binding.binding_kind).to_string(),
197 state_storage_ids: binding
198 .state_storage_ids
199 .iter()
200 .map(ToString::to_string)
201 .collect(),
202 computed_ids: binding
203 .computed_ids
204 .iter()
205 .map(ToString::to_string)
206 .collect(),
207 program_id: binding.existing_program_identity.to_string(),
208 expression: binding.expression.clone(),
209 attribute_name: binding.attribute_name.clone(),
210 })
211 .collect();
212 artifact.ordinary_template_events = ordinary
213 .events
214 .iter()
215 .map(|event| SerializedOrdinaryTemplateEvent {
216 component_instance_id: event.component_instance_id.to_string(),
217 component_id: event.component_id.to_string(),
218 target_id: event.target_id.to_string(),
219 declaration_event_id: event.declaration_event_id.to_string(),
220 event_type: event.event_type.clone(),
221 handler_method_id: event.handler_method_id.to_string(),
222 action_batch_id: event.action_batch_id.as_ref().map(ToString::to_string),
223 arguments: event.arguments.clone(),
224 program_id: event.existing_event_program_identity.to_string(),
225 })
226 .collect();
227 for instance in &mut artifact.instances {
228 instance.ordinary_template_targets = artifact
229 .ordinary_template_targets
230 .iter()
231 .filter(|target| target.component_instance_id == instance.instance)
232 .map(|target| target.id.clone())
233 .collect();
234 instance.ordinary_template_bindings = artifact
235 .ordinary_template_bindings
236 .iter()
237 .filter(|binding| binding.component_instance_id == instance.instance)
238 .map(|binding| binding.id.clone())
239 .collect();
240 instance.ordinary_template_events = artifact
241 .ordinary_template_events
242 .iter()
243 .filter(|event| event.component_instance_id == instance.instance)
244 .map(|event| event.declaration_event_id.clone())
245 .collect();
246 instance.state_slots = state_slots
247 .records
248 .iter()
249 .filter(|slot| slot.component_instance_id.to_string() == instance.instance)
250 .map(|slot| SerializedRuntimeStateSlot {
251 slot_id: slot.slot_id.to_string(),
252 state_id: slot.state_id.to_string(),
253 storage_id: slot.storage_id.to_string(),
254 initial_value: slot.initial_value.clone(),
255 semantic_type: semantic_type_text(&slot.semantic_type),
256 serializable: slot.serialization == SerializationCompatibility::Serializable,
257 })
258 .collect();
259 instance.computed_slots = computed_slots
260 .records
261 .iter()
262 .filter(|slot| slot.component_instance_id.to_string() == instance.instance)
263 .map(|slot| SerializedRuntimeComputedSlot {
264 computed_id: slot.computed_id.to_string(),
265 cache_slot_id: slot.cache_slot_id.to_string(),
266 dirty_slot_id: slot.dirty_slot_id.to_string(),
267 dirty_initial_value: slot.dirty_initial_value,
268 })
269 .collect();
270 }
271 let mut programs = std::collections::BTreeMap::<String, Vec<String>>::new();
272 for instance in model.component_instance_plan.instances.values() {
273 if instance.status == crate::ComponentInstanceStatus::StructuralTemplate {
274 if let Some(region) = &instance.structural_region {
275 programs
276 .entry(region.to_string())
277 .or_default()
278 .push(instance.id.to_string());
279 }
280 }
281 }
282 artifact.structural_programs = programs
283 .into_iter()
284 .map(
285 |(region, template_instances)| SerializedStructuralComponentProgram {
286 region,
287 create_order: template_instances.clone(),
288 destroy_order: template_instances.iter().rev().cloned().collect(),
289 template_instances,
290 },
291 )
292 .collect();
293 artifact
294}
295
296#[must_use]
297pub fn artifact_from_registry(registry: &RuntimeComponentRegistry) -> RuntimeComponentArtifact {
298 RuntimeComponentArtifact {
299 schema_version: RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION,
300 component_definitions: registry
301 .component_definitions
302 .iter()
303 .map(|r| SerializedComponentDefinition {
304 component: r.component.to_string(),
305 template: r.template.to_string(),
306 slots: r.declared_slots.iter().map(ToString::to_string).collect(),
307 boundary: "client".to_string(),
308 })
309 .collect(),
310 instances: registry
311 .instances
312 .iter()
313 .map(|r| SerializedComponentInstance {
314 instance: r.instance.to_string(),
315 component: r.component.to_string(),
316 invocation: r.invocation.as_ref().map(ToString::to_string),
317 parent: r.parent.as_ref().map(ToString::to_string),
318 depth: r.depth,
319 initialization_batch: r.initialization_batch,
320 ordinary_template_targets: Vec::new(),
321 ordinary_template_bindings: Vec::new(),
322 ordinary_template_events: Vec::new(),
323 state_slots: Vec::new(),
324 computed_slots: Vec::new(),
325 context_prefix: r.instance_context_prefix.clone(),
326 instruction_indices: r.optimized_instruction_indices.clone(),
327 structural_region: r.structural_region.as_ref().map(ToString::to_string),
328 })
329 .collect(),
330 initialization_batches: registry
331 .initialization_batches
332 .iter()
333 .map(|r| SerializedComponentBatch {
334 index: r.index,
335 instances: r.instances.iter().map(ToString::to_string).collect(),
336 context_sources: r.context_sources.iter().map(ToString::to_string).collect(),
337 slot_bindings: r.slot_bindings.iter().map(ToString::to_string).collect(),
338 })
339 .collect(),
340 slot_binding_programs: registry
341 .slot_bindings
342 .iter()
343 .map(|r| SerializedSlotBinding {
344 binding: r.binding.to_string(),
345 caller_instance: r.caller_instance.to_string(),
346 callee_instance: r.callee_instance.to_string(),
347 slot: r.slot.to_string(),
348 outlet: r.outlet.to_string(),
349 fragment: r.fragment.to_string(),
350 content_owner_instance: r.content_owner_instance.to_string(),
351 })
352 .collect(),
353 instance_context_bindings: registry
354 .instance_context_bindings
355 .iter()
356 .map(|r| SerializedInstanceContextBinding {
357 consumer_instance: r.consumer_instance.to_string(),
358 selected_source: r.selected_source.to_string(),
359 provider_source: r.provider_source.as_ref().map(ToString::to_string),
360 runtime_slot: r.runtime_slot.to_string(),
361 })
362 .collect(),
363 ordinary_template_targets: Vec::new(),
364 ordinary_template_bindings: Vec::new(),
365 ordinary_template_events: Vec::new(),
366 destruction: SerializedDestructionMetadata {
367 operation: "destroy_component_instance".to_string(),
368 enabled: true,
369 },
370 structural_programs: Vec::new(),
371 }
372}
373
374#[must_use]
378pub fn runtime_component_artifact_json(artifact: &RuntimeComponentArtifact) -> String {
379 serde_json::to_string_pretty(artifact).expect("component runtime artifact serializes") + "\n"
380}
381
382#[allow(clippy::too_many_lines)]
386pub fn validate_runtime_component_artifact(
387 artifact: &RuntimeComponentArtifact,
388) -> Result<(), String> {
389 if artifact.schema_version != RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION {
390 return Err("unsupported component runtime artifact schema version".to_string());
391 }
392 if artifact.structural_programs.iter().any(|program| {
393 program.create_order != program.template_instances
394 || program
395 .destroy_order
396 .iter()
397 .rev()
398 .cloned()
399 .collect::<Vec<_>>()
400 != program.template_instances
401 }) {
402 return Err("component artifact has invalid structural program ordering".to_string());
403 }
404 let instances = artifact
405 .instances
406 .iter()
407 .map(|r| r.instance.as_str())
408 .collect::<std::collections::BTreeSet<_>>();
409 let target_ids = artifact
410 .ordinary_template_targets
411 .iter()
412 .map(|target| target.id.as_str())
413 .collect::<std::collections::BTreeSet<_>>();
414 let binding_ids = artifact
415 .ordinary_template_bindings
416 .iter()
417 .map(|binding| binding.id.as_str())
418 .collect::<std::collections::BTreeSet<_>>();
419 let event_keys = artifact
420 .ordinary_template_events
421 .iter()
422 .map(|event| {
423 (
424 event.component_instance_id.as_str(),
425 event.target_id.as_str(),
426 event.event_type.as_str(),
427 )
428 })
429 .collect::<std::collections::BTreeSet<_>>();
430 let mut computed_cache_slots = std::collections::BTreeSet::new();
431 let mut computed_dirty_slots = std::collections::BTreeSet::new();
432 let mut computed_instance_pairs = std::collections::BTreeSet::new();
433 let mut state_slots = std::collections::BTreeSet::new();
434 let mut state_instance_pairs = std::collections::BTreeSet::new();
435 if target_ids.len() != artifact.ordinary_template_targets.len()
436 || binding_ids.len() != artifact.ordinary_template_bindings.len()
437 || event_keys.len() != artifact.ordinary_template_events.len()
438 || artifact.ordinary_template_targets.iter().any(|target| {
439 !instances.contains(target.component_instance_id.as_str())
440 || !target.id.starts_with(&format!(
441 "{}/template-target:",
442 target.component_instance_id
443 ))
444 })
445 || artifact.ordinary_template_bindings.iter().any(|binding| {
446 !instances.contains(binding.component_instance_id.as_str())
447 || !target_ids.contains(binding.target_id.as_str())
448 || !binding.id.starts_with(&format!(
449 "{}/template-binding:",
450 binding.component_instance_id
451 ))
452 })
453 || artifact.ordinary_template_events.iter().any(|event| {
454 !instances.contains(event.component_instance_id.as_str())
455 || !target_ids.contains(event.target_id.as_str())
456 || event.action_batch_id.is_none()
457 })
458 || artifact.instances.iter().any(|instance| {
459 instance
460 .ordinary_template_targets
461 .iter()
462 .any(|id| !target_ids.contains(id.as_str()))
463 || instance
464 .ordinary_template_bindings
465 .iter()
466 .any(|id| !binding_ids.contains(id.as_str()))
467 || instance.ordinary_template_targets
468 != artifact
469 .ordinary_template_targets
470 .iter()
471 .filter(|target| target.component_instance_id == instance.instance)
472 .map(|target| target.id.clone())
473 .collect::<Vec<_>>()
474 || instance.ordinary_template_bindings
475 != artifact
476 .ordinary_template_bindings
477 .iter()
478 .filter(|binding| binding.component_instance_id == instance.instance)
479 .map(|binding| binding.id.clone())
480 .collect::<Vec<_>>()
481 || instance.ordinary_template_events
482 != artifact
483 .ordinary_template_events
484 .iter()
485 .filter(|event| event.component_instance_id == instance.instance)
486 .map(|event| event.declaration_event_id.clone())
487 .collect::<Vec<_>>()
488 || instance.state_slots.iter().any(|slot| {
489 slot.slot_id != canonical_state_slot_text(&instance.instance, &slot.storage_id)
490 || slot.storage_id != format!("storage:{}", slot.state_id)
491 || !state_slots.insert(slot.slot_id.as_str())
492 || !state_instance_pairs
493 .insert((instance.instance.as_str(), slot.storage_id.as_str()))
494 })
495 || instance.computed_slots.iter().any(|slot| {
496 !slot
497 .cache_slot_id
498 .starts_with(&format!("{}/computed-cache:", instance.instance))
499 || !slot
500 .dirty_slot_id
501 .starts_with(&format!("{}/computed-dirty:", instance.instance))
502 || !computed_cache_slots.insert(slot.cache_slot_id.as_str())
503 || !computed_dirty_slots.insert(slot.dirty_slot_id.as_str())
504 || !computed_instance_pairs
505 .insert((instance.instance.as_str(), slot.computed_id.as_str()))
506 })
507 })
508 {
509 return Err("component artifact has invalid ordinary template projection".to_string());
510 }
511 if artifact.instances.iter().any(|r| {
512 r.parent
513 .as_deref()
514 .is_some_and(|parent| !instances.contains(parent))
515 }) {
516 return Err("component artifact has an unknown parent instance".to_string());
517 }
518 if artifact.slot_binding_programs.iter().any(|r| {
519 !instances.contains(r.caller_instance.as_str())
520 || !instances.contains(r.callee_instance.as_str())
521 }) {
522 return Err("component artifact has an unknown Slot-binding endpoint".to_string());
523 }
524 if artifact
525 .initialization_batches
526 .iter()
527 .enumerate()
528 .any(|(index, batch)| {
529 batch.index != index
530 || batch
531 .instances
532 .iter()
533 .any(|id| !instances.contains(id.as_str()))
534 })
535 {
536 return Err("component artifact has invalid initialization ordering".to_string());
537 }
538 Ok(())
539}
540
541fn canonical_state_slot_text(instance: &str, storage: &str) -> String {
542 let encoded = storage.bytes().fold(String::new(), |mut encoded, byte| {
543 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
544 encoded.push(char::from(byte));
545 } else {
546 use std::fmt::Write as _;
547 write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail");
548 }
549 encoded
550 });
551 format!("{instance}/state-slot:{encoded}")
552}
553
554#[cfg(test)]
555mod tests {
556 use crate::{
557 build_application_semantic_model, build_runtime_component_artifact,
558 runtime_component_artifact_json, validate_runtime_component_artifact,
559 };
560 #[test]
561 fn serializes_deterministically_and_rejects_unknown_endpoints() {
562 let model = build_application_semantic_model(&presolve_parser::parse_file(
563 "src/Artifact.tsx",
564 r#"@component("x-page") class Page extends Component { render() { return <main />; } }"#,
565 ));
566 let mut artifact =
567 build_runtime_component_artifact(&model, &model.component_ir_optimization);
568 assert!(validate_runtime_component_artifact(&artifact).is_ok());
569 assert_eq!(
570 runtime_component_artifact_json(&artifact),
571 runtime_component_artifact_json(&build_runtime_component_artifact(
572 &model,
573 &model.component_ir_optimization
574 ))
575 );
576 artifact.instances[0].parent = Some("missing".to_string());
577 assert!(validate_runtime_component_artifact(&artifact).is_err());
578 }
579
580 #[test]
581 fn projects_exact_computed_slots_for_each_repeated_instance() {
582 let model = build_application_semantic_model(&presolve_parser::parse_file(
583 "src/RepeatedComputedArtifact.tsx",
584 r#"@component("x-child") class Child { count = state(1); @computed() get doubled() { return this.count * 2; } render() { return <span>{this.doubled}</span>; } }
585@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
586 ));
587 let mut artifact =
588 build_runtime_component_artifact(&model, &model.component_ir_optimization);
589 let slots = artifact
590 .instances
591 .iter()
592 .flat_map(|instance| instance.computed_slots.iter())
593 .collect::<Vec<_>>();
594 assert_eq!(slots.len(), 2);
595 assert_ne!(slots[0].cache_slot_id, slots[1].cache_slot_id);
596 assert_ne!(slots[0].dirty_slot_id, slots[1].dirty_slot_id);
597 assert!(validate_runtime_component_artifact(&artifact).is_ok());
598 let computed_instances = artifact
599 .instances
600 .iter()
601 .enumerate()
602 .filter(|(_, instance)| !instance.computed_slots.is_empty())
603 .map(|(index, _)| index)
604 .collect::<Vec<_>>();
605 let duplicate = artifact.instances[computed_instances[0]].computed_slots[0].clone();
606 artifact.instances[computed_instances[1]].computed_slots[0].cache_slot_id =
607 duplicate.cache_slot_id;
608 assert!(validate_runtime_component_artifact(&artifact).is_err());
609 }
610
611 #[test]
612 fn projects_and_validates_exact_state_slots_for_repeated_instances() {
613 let model = build_application_semantic_model(&presolve_parser::parse_file(
614 "src/RepeatedStateArtifact.tsx",
615 r#"@component("x-child") class Child { count = state(1); render() { return <span>{this.count}</span>; } }
616@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
617 ));
618 let mut artifact =
619 build_runtime_component_artifact(&model, &model.component_ir_optimization);
620 let state_instances = artifact
621 .instances
622 .iter()
623 .enumerate()
624 .filter(|(_, instance)| !instance.state_slots.is_empty())
625 .map(|(index, _)| index)
626 .collect::<Vec<_>>();
627 assert_eq!(state_instances.len(), 2);
628 let first = &artifact.instances[state_instances[0]].state_slots[0];
629 let second = &artifact.instances[state_instances[1]].state_slots[0];
630 assert_eq!(first.storage_id, second.storage_id);
631 assert_ne!(first.slot_id, second.slot_id);
632 assert!(validate_runtime_component_artifact(&artifact).is_ok());
633
634 artifact.instances[state_instances[1]].state_slots[0].slot_id = first.slot_id.clone();
635 assert!(validate_runtime_component_artifact(&artifact).is_err());
636 }
637}