weavatrix_memory/projection/memory/
parts.rs1use super::{
2 MemoryProjection,
3 state::{NodeHistory, NodeRevision},
4};
5use crate::{
6 domain::{MemoryFact, MemoryNode},
7 error::{MemoryError, Result},
8 time::Timestamp,
9};
10use std::thread;
11
12#[derive(Clone, Copy)]
13struct PreparedFact {
14 source: usize,
15 target: usize,
16 id_hash: u64,
17}
18
19impl MemoryProjection {
20 pub fn try_from_parts(
33 nodes: Vec<MemoryNode>,
34 facts: Vec<MemoryFact>,
35 known_at: Timestamp,
36 source_position: Option<u64>,
37 ) -> Result<Self> {
38 let mut projection = Self::with_capacity(nodes.len(), facts.len());
39 for (position, node) in nodes.into_iter().enumerate() {
40 node.validate()?;
41 let index = projection.nodes.len();
42 if projection
43 .node_lookup
44 .insert(node.id.clone(), index)
45 .is_some()
46 {
47 return Err(MemoryError::InvalidValue {
48 field: "nodes",
49 reason: "node identifiers must be unique",
50 });
51 }
52 projection.nodes.push(NodeHistory::new(NodeRevision {
53 node,
54 recorded_at: known_at,
55 position: u64::try_from(position).map_err(|_| MemoryError::CapacityOverflow)?,
56 }));
57 }
58 let prepared = prepare_facts(&projection, &facts, known_at)?;
59 for (index, (fact, prepared)) in facts.iter().zip(&prepared).enumerate() {
60 if projection
61 .fact_lookup
62 .insert_hashed(fact.id.clone(), index, prepared.id_hash)
63 .is_some()
64 {
65 return Err(MemoryError::ConflictingFact {
66 id: fact.id.to_string(),
67 });
68 }
69 }
70 for fact in &facts {
71 if let Some(prior) = &fact.supersedes {
72 projection.apply_supersession(prior, fact)?;
73 }
74 }
75 let endpoints = prepared
76 .into_iter()
77 .map(|fact| (fact.source, fact.target))
78 .collect::<Vec<_>>();
79 projection.facts = facts;
80 projection.set_incidents(&endpoints)?;
81 projection.last_global_position = source_position;
82 Ok(projection)
83 }
84}
85
86fn prepare_facts(
87 projection: &MemoryProjection,
88 facts: &[MemoryFact],
89 known_at: Timestamp,
90) -> Result<Vec<PreparedFact>> {
91 let parallelism = thread::available_parallelism().map_or(1, usize::from);
92 let workers = parallelism.min(facts.len().div_ceil(16_384)).max(1);
93 if workers == 1 {
94 return facts
95 .iter()
96 .map(|fact| prepare_fact(projection, fact, known_at))
97 .collect();
98 }
99 let chunk_size = facts.len().div_ceil(workers);
100 thread::scope(|scope| {
101 let handles = facts
102 .chunks(chunk_size)
103 .map(|chunk| {
104 scope.spawn(move || {
105 chunk
106 .iter()
107 .map(|fact| prepare_fact(projection, fact, known_at))
108 .collect::<Result<Vec<_>>>()
109 })
110 })
111 .collect::<Vec<_>>();
112 let mut prepared = Vec::with_capacity(facts.len());
113 for handle in handles {
114 let mut chunk = handle.join().map_err(|_| MemoryError::InvalidValue {
115 field: "facts",
116 reason: "parallel fact preparation panicked",
117 })??;
118 prepared.append(&mut chunk);
119 }
120 Ok(prepared)
121 })
122}
123
124fn prepare_fact(
125 projection: &MemoryProjection,
126 fact: &MemoryFact,
127 known_at: Timestamp,
128) -> Result<PreparedFact> {
129 if fact.recorded_at > known_at {
130 return Err(MemoryError::InvalidValue {
131 field: "facts",
132 reason: "fact recorded_at must not exceed known_at",
133 });
134 }
135 fact.validate()?;
136 Ok(PreparedFact {
137 source: projection.require_entity(&fact.source)?,
138 target: projection.require_entity(&fact.target)?,
139 id_hash: projection.fact_lookup.hash(&fact.id),
140 })
141}