use std::collections::BTreeMap;
use crate::{
serialization_compatibility, ApplicationSemanticModel, ComponentInstanceId,
ComponentInstanceStatus, IntermediateRepresentation, IrStorage, IrStorageId, SemanticId,
SemanticType, SerializableValue, SerializationCompatibility, SourceProvenance,
StateInstanceSlotId,
};
pub const STATE_INSTANCE_STORAGE_REGISTRY_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateInstanceStorageRecord {
pub slot_id: StateInstanceSlotId,
pub component_instance_id: ComponentInstanceId,
pub component_id: SemanticId,
pub state_id: SemanticId,
pub storage_id: IrStorageId,
pub initial_value: SerializableValue,
pub semantic_type: SemanticType,
pub serialization: SerializationCompatibility,
pub provenance: SourceProvenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateInstanceStorageRegistry {
pub version: u32,
pub records: Vec<StateInstanceStorageRecord>,
}
impl StateInstanceStorageRegistry {
#[must_use]
pub fn record(
&self,
component_instance_id: &ComponentInstanceId,
storage_id: &IrStorageId,
) -> Option<&StateInstanceStorageRecord> {
self.records.iter().find(|record| {
record.component_instance_id == *component_instance_id
&& record.storage_id == *storage_id
})
}
pub fn records_for_instance<'a>(
&'a self,
component_instance_id: &'a ComponentInstanceId,
) -> impl Iterator<Item = &'a StateInstanceStorageRecord> + 'a {
self.records
.iter()
.filter(move |record| record.component_instance_id == *component_instance_id)
}
}
#[must_use]
pub fn build_state_instance_storage_registry(
model: &ApplicationSemanticModel,
ir: &IntermediateRepresentation,
) -> StateInstanceStorageRegistry {
let storages = ir
.modules
.iter()
.flat_map(|module| &module.storages)
.map(|storage| (storage.semantic_origin.clone(), storage))
.collect::<BTreeMap<_, _>>();
let mut records = Vec::new();
for instance in model.component_instance_plan.instances.values() {
if instance.status != ComponentInstanceStatus::Planned {
continue;
}
let Some(component) = model
.components
.iter()
.find(|component| component.id == instance.component)
else {
continue;
};
let mut component_storages = component
.state_fields
.iter()
.filter_map(|state| {
canonical_state_storage_record(model, instance.id.clone(), &storages, state)
})
.collect::<Vec<_>>();
component_storages.sort_by(|left, right| left.storage_id.cmp(&right.storage_id));
records.extend(component_storages);
}
StateInstanceStorageRegistry {
version: STATE_INSTANCE_STORAGE_REGISTRY_VERSION,
records,
}
}
fn canonical_state_storage_record(
model: &ApplicationSemanticModel,
component_instance_id: ComponentInstanceId,
storages: &BTreeMap<SemanticId, &IrStorage>,
state: &crate::StateField,
) -> Option<StateInstanceStorageRecord> {
let storage = storages.get(&state.id)?;
let initial_value = storage.initial_value.clone()?;
let semantic_type = model.semantic_type_of(&state.id)?.clone();
let component_id = state.owner.entity_id()?.clone();
Some(StateInstanceStorageRecord {
slot_id: StateInstanceSlotId::for_component_instance_storage(
component_instance_id.clone(),
storage.id.clone(),
),
component_instance_id,
component_id,
state_id: state.id.clone(),
storage_id: storage.id.clone(),
initial_value,
serialization: serialization_compatibility(&semantic_type),
semantic_type,
provenance: storage.provenance.clone(),
})
}
pub fn validate_state_instance_storage_registry(
model: &ApplicationSemanticModel,
ir: &IntermediateRepresentation,
registry: &StateInstanceStorageRegistry,
) -> Result<(), String> {
if registry.version != STATE_INSTANCE_STORAGE_REGISTRY_VERSION {
return Err("unsupported State instance storage registry version".to_string());
}
if registry != &build_state_instance_storage_registry(model, ir) {
return Err("State instance storage registry drifted from canonical products".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repeated_instances_receive_distinct_slots_for_one_ir_storage() {
let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
"src/RepeatedState.tsx",
r#"@component("x-child") class Child { count = state(1); render() { return <button>{this.count}</button>; } }
@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
));
let ir = crate::lower_components_to_ir(&model);
let registry = build_state_instance_storage_registry(&model, &ir);
assert_eq!(registry.records.len(), 2);
assert_eq!(
registry.records[0].storage_id,
registry.records[1].storage_id
);
assert_ne!(registry.records[0].slot_id, registry.records[1].slot_id);
assert_eq!(
registry
.records_for_instance(®istry.records[0].component_instance_id)
.count(),
1
);
assert_eq!(
registry
.record(
®istry.records[1].component_instance_id,
®istry.records[1].storage_id
)
.map(|record| &record.slot_id),
Some(®istry.records[1].slot_id)
);
assert!(validate_state_instance_storage_registry(&model, &ir, ®istry).is_ok());
}
#[test]
fn registry_is_deterministic_under_reversed_compilation_input() {
let first = presolve_parser::parse_file(
"src/Child.tsx",
r#"@component("x-child") class Child { count = state(1); render() { return <b>{this.count}</b>; } }"#,
);
let second = presolve_parser::parse_file(
"src/Page.tsx",
r#"@component("x-page") class Page { render() { return <><Child /><Child /></>; } }"#,
);
let forward = crate::build_application_semantic_model_for_unit(
&crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
);
let reverse = crate::build_application_semantic_model_for_unit(
&crate::CompilationUnit::from_parsed_files(vec![second, first]),
);
assert_eq!(
build_state_instance_storage_registry(
&forward,
&crate::lower_components_to_ir(&forward)
),
build_state_instance_storage_registry(
&reverse,
&crate::lower_components_to_ir(&reverse)
)
);
}
}