weavatrix_memory/projection/memory/
mod.rs1mod apply;
2pub(crate) mod index;
3mod parts;
4pub(crate) mod state;
5
6use crate::{
7 domain::{Evidence, MemoryFact, MemoryNode, MemoryView, MemoryViewRef},
8 error::{MemoryError, Result},
9 id::{EntityId, FactId},
10 time::Timestamp,
11};
12use serde::{Deserialize, Serialize};
13use state::{NodeHistory, NodeRevision, Retraction, Supersession};
14use std::collections::HashSet;
15
16pub use state::MemoryProjection;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ProjectionClock {
20 pub valid_at: Timestamp,
21 pub known_at: Timestamp,
22}
23
24impl ProjectionClock {
25 #[must_use]
26 pub const fn new(valid_at: Timestamp, known_at: Timestamp) -> Self {
27 Self { valid_at, known_at }
28 }
29}
30
31impl MemoryProjection {
32 #[must_use]
33 pub const fn last_global_position(&self) -> Option<u64> {
34 self.last_global_position
35 }
36
37 #[must_use]
38 pub fn superseded_by(&self, fact: &FactId) -> Option<&FactId> {
39 self.supersessions
40 .get(fact)
41 .map(|change| &change.replacement)
42 }
43
44 pub(crate) fn visible_node(&self, id: &EntityId, known_at: Timestamp) -> Option<&MemoryNode> {
45 self.node_lookup
46 .get(id)
47 .and_then(|index| visible_revision(&self.nodes[*index], known_at))
48 }
49
50 pub(crate) fn fact(&self, id: &FactId) -> Option<&MemoryFact> {
51 self.fact_lookup.get(id).map(|index| &self.facts[*index])
52 }
53
54 pub(crate) fn all_facts(&self) -> &[MemoryFact] {
55 &self.facts
56 }
57
58 pub(crate) fn incident_fact_ids(&self, id: &EntityId) -> impl Iterator<Item = &FactId> + '_ {
59 self.node_lookup
60 .get(id)
61 .into_iter()
62 .flat_map(|index| {
63 let stable = &self.incident_facts
64 [self.incident_offsets[*index]..self.incident_offsets[*index + 1]];
65 stable
66 .iter()
67 .chain(self.incident_delta.get(index).into_iter().flatten())
68 })
69 .map(|index| &self.facts[*index].id)
70 }
71
72 #[must_use]
73 pub fn view(&self, clock: ProjectionClock) -> MemoryView {
74 self.view_ref(clock).into_owned()
75 }
76
77 #[must_use]
82 pub fn view_ref(&self, clock: ProjectionClock) -> MemoryViewRef<'_> {
83 let nodes = self
84 .nodes
85 .iter()
86 .filter_map(|revisions| visible_revision(revisions, clock.known_at))
87 .collect::<Vec<_>>();
88 let visible_ids = (nodes.len() != self.nodes.len()).then(|| {
89 nodes
90 .iter()
91 .map(|node| node.id.clone())
92 .collect::<HashSet<_>>()
93 });
94 let facts = self
95 .facts
96 .iter()
97 .filter(|fact| {
98 visible_ids.as_ref().is_none_or(|visible| {
99 visible.contains(&fact.source) && visible.contains(&fact.target)
100 }) && self.fact_is_active(fact, clock)
101 })
102 .collect();
103 MemoryViewRef { nodes, facts }
104 }
105
106 pub(crate) fn fact_is_active(&self, fact: &MemoryFact, clock: ProjectionClock) -> bool {
107 if fact.recorded_at > clock.known_at || fact.valid_from > clock.valid_at {
108 return false;
109 }
110 if fact
111 .valid_until
112 .is_some_and(|until| clock.valid_at >= until)
113 {
114 return false;
115 }
116 if self.supersessions.get(&fact.id).is_some_and(|change| {
117 change.recorded_at <= clock.known_at && change.valid_from <= clock.valid_at
118 }) {
119 return false;
120 }
121 !self.retractions.get(&fact.id).is_some_and(|change| {
122 change.recorded_at <= clock.known_at && change.valid_until <= clock.valid_at
123 })
124 }
125
126 fn insert_node(&mut self, revision: NodeRevision) -> Result<()> {
127 revision.node.validate()?;
128 if let Some(index) = self.node_lookup.get(&revision.node.id).copied() {
129 self.nodes[index].later.push(revision);
130 } else {
131 let index = self.nodes.len();
132 self.node_lookup.insert(revision.node.id.clone(), index);
133 self.nodes.push(NodeHistory::new(revision));
134 let offset = *self.incident_offsets.last().unwrap_or(&0);
135 self.incident_offsets.push(offset);
136 }
137 Ok(())
138 }
139
140 fn insert_fact(&mut self, fact: MemoryFact) -> Result<()> {
141 fact.validate()?;
142 let source = self.require_entity(&fact.source)?;
143 let target = self.require_entity(&fact.target)?;
144 if self.fact_lookup.contains_key(&fact.id) {
145 return Err(MemoryError::ConflictingFact {
146 id: fact.id.to_string(),
147 });
148 }
149 if let Some(prior) = &fact.supersedes {
150 self.apply_supersession(prior, &fact)?;
151 }
152 let index = self.facts.len();
153 self.fact_lookup.insert(fact.id.clone(), index);
154 self.incident_delta.entry(source).or_default().push(index);
155 if target != source {
156 self.incident_delta.entry(target).or_default().push(index);
157 }
158 self.facts.push(fact);
159 Ok(())
160 }
161
162 fn apply_supersession(&mut self, prior: &FactId, fact: &MemoryFact) -> Result<()> {
163 if !self.fact_lookup.contains_key(prior) {
164 return Err(MemoryError::MissingFact {
165 id: prior.to_string(),
166 });
167 }
168 if self.supersessions.contains_key(prior) {
169 return Err(MemoryError::ConflictingFact {
170 id: prior.to_string(),
171 });
172 }
173 self.supersessions.insert(
174 prior.clone(),
175 Supersession {
176 replacement: fact.id.clone(),
177 valid_from: fact.valid_from,
178 recorded_at: fact.recorded_at,
179 },
180 );
181 Ok(())
182 }
183
184 fn require_entity(&self, id: &EntityId) -> Result<usize> {
185 self.node_lookup
186 .get(id)
187 .copied()
188 .ok_or_else(|| MemoryError::MissingEntity { id: id.to_string() })
189 }
190
191 fn apply_retraction(
192 &mut self,
193 recorded_at: Timestamp,
194 fact_id: &FactId,
195 valid_until: Timestamp,
196 evidence: &[Evidence],
197 ) -> Result<()> {
198 let fact = self.fact(fact_id).ok_or_else(|| MemoryError::MissingFact {
199 id: fact_id.to_string(),
200 })?;
201 if valid_until <= fact.valid_from {
202 return Err(MemoryError::InvalidValue {
203 field: "retraction.valid_until",
204 reason: "must be later than the fact valid_from",
205 });
206 }
207 if evidence.is_empty() {
208 return Err(MemoryError::InvalidValue {
209 field: "retraction.evidence",
210 reason: "at least one evidence item is required",
211 });
212 }
213 evidence.iter().try_for_each(Evidence::validate)?;
214 self.retractions.insert(
215 fact_id.clone(),
216 Retraction {
217 valid_until,
218 recorded_at,
219 },
220 );
221 Ok(())
222 }
223}
224
225fn visible_revision(history: &NodeHistory, known_at: Timestamp) -> Option<&MemoryNode> {
226 core::iter::once(&history.first)
227 .chain(&history.later)
228 .filter(|revision| revision.recorded_at <= known_at)
229 .max_by_key(|revision| (revision.recorded_at, revision.position))
230 .map(|revision| &revision.node)
231}