Skip to main content

weavatrix_memory/projection/memory/
state.rs

1use super::index::IdIndex;
2use crate::{EntityId, FactId, MemoryError, MemoryFact, MemoryNode, Result, Timestamp};
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
4use std::collections::{BTreeMap, HashMap};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub(super) struct NodeRevision {
8    pub(super) node: MemoryNode,
9    pub(super) recorded_at: Timestamp,
10    pub(super) position: u64,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub(super) struct NodeHistory {
15    pub(super) first: NodeRevision,
16    pub(super) later: Vec<NodeRevision>,
17}
18
19impl NodeHistory {
20    pub(super) const fn new(first: NodeRevision) -> Self {
21        Self {
22            first,
23            later: Vec::new(),
24        }
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub(super) struct Supersession {
30    pub(super) replacement: FactId,
31    pub(super) valid_from: Timestamp,
32    pub(super) recorded_at: Timestamp,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub(super) struct Retraction {
37    pub(super) valid_until: Timestamp,
38    pub(super) recorded_at: Timestamp,
39}
40
41#[derive(Debug, Clone)]
42pub struct MemoryProjection {
43    pub(super) nodes: Vec<NodeHistory>,
44    pub(super) facts: Vec<MemoryFact>,
45    pub(super) node_lookup: IdIndex<EntityId>,
46    pub(super) fact_lookup: IdIndex<FactId>,
47    pub(super) incident_offsets: Vec<usize>,
48    pub(super) incident_facts: Vec<usize>,
49    pub(super) incident_delta: HashMap<usize, Vec<usize>>,
50    pub(super) supersessions: BTreeMap<FactId, Supersession>,
51    pub(super) retractions: BTreeMap<FactId, Retraction>,
52    pub(super) last_global_position: Option<u64>,
53}
54
55impl MemoryProjection {
56    pub(super) fn with_capacity(nodes: usize, facts: usize) -> Self {
57        Self {
58            nodes: Vec::with_capacity(nodes),
59            facts: Vec::with_capacity(facts),
60            node_lookup: IdIndex::with_capacity(nodes),
61            fact_lookup: IdIndex::with_capacity(facts),
62            incident_offsets: vec![0],
63            incident_facts: Vec::with_capacity(facts.saturating_mul(2)),
64            incident_delta: HashMap::new(),
65            supersessions: BTreeMap::new(),
66            retractions: BTreeMap::new(),
67            last_global_position: None,
68        }
69    }
70
71    pub(super) fn rebuild_indexes(&mut self) -> Result<()> {
72        self.node_lookup.reserve(self.nodes.len());
73        for (index, revisions) in self.nodes.iter().enumerate() {
74            for revision in core::iter::once(&revisions.first).chain(&revisions.later) {
75                revision.node.validate()?;
76                if revision.node.id != revisions.first.node.id {
77                    return Err(MemoryError::InvalidValue {
78                        field: "projection.nodes",
79                        reason: "revision identifiers must match",
80                    });
81                }
82            }
83            if self
84                .node_lookup
85                .insert(revisions.first.node.id.clone(), index)
86                .is_some()
87            {
88                return Err(MemoryError::InvalidValue {
89                    field: "projection.nodes",
90                    reason: "node identifiers must be unique",
91                });
92            }
93        }
94        self.fact_lookup.reserve(self.facts.len());
95        let mut endpoints = Vec::with_capacity(self.facts.len());
96        for (index, fact) in self.facts.iter().enumerate() {
97            fact.validate()?;
98            let source = self.entity_index(&fact.source)?;
99            let target = self.entity_index(&fact.target)?;
100            if self.fact_lookup.insert(fact.id.clone(), index).is_some() {
101                return Err(MemoryError::ConflictingFact {
102                    id: fact.id.to_string(),
103                });
104            }
105            endpoints.push((source, target));
106        }
107        self.set_incidents(&endpoints)?;
108        self.validate_changes()
109    }
110
111    pub(super) fn set_incidents(&mut self, endpoints: &[(usize, usize)]) -> Result<()> {
112        let mut offsets = vec![0_usize; self.nodes.len() + 1];
113        for &(source, target) in endpoints {
114            offsets[source + 1] = offsets[source + 1]
115                .checked_add(1)
116                .ok_or(MemoryError::CapacityOverflow)?;
117            if target != source {
118                offsets[target + 1] = offsets[target + 1]
119                    .checked_add(1)
120                    .ok_or(MemoryError::CapacityOverflow)?;
121            }
122        }
123        for index in 1..offsets.len() {
124            offsets[index] = offsets[index]
125                .checked_add(offsets[index - 1])
126                .ok_or(MemoryError::CapacityOverflow)?;
127        }
128        let mut cursor = offsets[..self.nodes.len()].to_vec();
129        let mut incidents = vec![0_usize; *offsets.last().unwrap_or(&0)];
130        for (fact, &(source, target)) in endpoints.iter().enumerate() {
131            incidents[cursor[source]] = fact;
132            cursor[source] += 1;
133            if target != source {
134                incidents[cursor[target]] = fact;
135                cursor[target] += 1;
136            }
137        }
138        self.incident_offsets = offsets;
139        self.incident_facts = incidents;
140        self.incident_delta.clear();
141        Ok(())
142    }
143
144    fn entity_index(&self, id: &EntityId) -> Result<usize> {
145        self.node_lookup
146            .get(id)
147            .copied()
148            .ok_or_else(|| MemoryError::MissingEntity { id: id.to_string() })
149    }
150
151    fn validate_changes(&self) -> Result<()> {
152        for (prior, change) in &self.supersessions {
153            let prior_fact = self
154                .fact_lookup
155                .get(prior)
156                .map(|index| &self.facts[*index])
157                .ok_or_else(|| MemoryError::MissingFact {
158                    id: prior.to_string(),
159                })?;
160            let replacement = self
161                .fact_lookup
162                .get(&change.replacement)
163                .map(|index| &self.facts[*index])
164                .ok_or_else(|| MemoryError::MissingFact {
165                    id: change.replacement.to_string(),
166                })?;
167            if replacement.supersedes.as_ref() != Some(&prior_fact.id)
168                || replacement.valid_from != change.valid_from
169                || replacement.recorded_at != change.recorded_at
170            {
171                return Err(MemoryError::InvalidValue {
172                    field: "projection.supersessions",
173                    reason: "supersession index disagrees with replacement fact",
174                });
175            }
176        }
177        for (fact_id, change) in &self.retractions {
178            let fact = self
179                .fact_lookup
180                .get(fact_id)
181                .map(|index| &self.facts[*index])
182                .ok_or_else(|| MemoryError::MissingFact {
183                    id: fact_id.to_string(),
184                })?;
185            if change.valid_until <= fact.valid_from {
186                return Err(MemoryError::InvalidValue {
187                    field: "projection.retractions",
188                    reason: "retraction must follow fact validity",
189                });
190            }
191        }
192        Ok(())
193    }
194}
195
196impl Default for MemoryProjection {
197    fn default() -> Self {
198        Self::with_capacity(0, 0)
199    }
200}
201
202impl PartialEq for MemoryProjection {
203    fn eq(&self, other: &Self) -> bool {
204        self.nodes == other.nodes
205            && self.facts == other.facts
206            && self.supersessions == other.supersessions
207            && self.retractions == other.retractions
208            && self.last_global_position == other.last_global_position
209    }
210}
211
212impl Eq for MemoryProjection {}
213
214#[derive(Serialize)]
215struct ProjectionRef<'a> {
216    nodes: &'a [NodeHistory],
217    facts: &'a [MemoryFact],
218    supersessions: &'a BTreeMap<FactId, Supersession>,
219    retractions: &'a BTreeMap<FactId, Retraction>,
220    last_global_position: Option<u64>,
221}
222
223#[derive(Deserialize)]
224struct ProjectionData {
225    nodes: Vec<NodeHistory>,
226    facts: Vec<MemoryFact>,
227    supersessions: BTreeMap<FactId, Supersession>,
228    retractions: BTreeMap<FactId, Retraction>,
229    last_global_position: Option<u64>,
230}
231
232impl Serialize for MemoryProjection {
233    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
234    where
235        S: Serializer,
236    {
237        ProjectionRef {
238            nodes: &self.nodes,
239            facts: &self.facts,
240            supersessions: &self.supersessions,
241            retractions: &self.retractions,
242            last_global_position: self.last_global_position,
243        }
244        .serialize(serializer)
245    }
246}
247
248impl<'de> Deserialize<'de> for MemoryProjection {
249    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
250    where
251        D: Deserializer<'de>,
252    {
253        let data = ProjectionData::deserialize(deserializer)?;
254        let node_count = data.nodes.len();
255        let fact_count = data.facts.len();
256        let mut projection = Self {
257            nodes: data.nodes,
258            facts: data.facts,
259            node_lookup: IdIndex::with_capacity(node_count),
260            fact_lookup: IdIndex::with_capacity(fact_count),
261            incident_offsets: Vec::new(),
262            incident_facts: Vec::new(),
263            incident_delta: HashMap::new(),
264            supersessions: data.supersessions,
265            retractions: data.retractions,
266            last_global_position: data.last_global_position,
267        };
268        projection.rebuild_indexes().map_err(D::Error::custom)?;
269        Ok(projection)
270    }
271}