kmp_adapter_embedded/adapter/
node_card.rs1use kmp_domain::{
2 AuthorNodeCard, NodeCard, NodeCardEvent, NodeCardStore, NodeCardWriteFuture, PortError,
3 node_card_policy,
4};
5
6use super::engine::{Key, ReadTx, Table, WriteTx};
7use super::serdes::{AggregateRecord, CardRecord, NodeRecord, decode, encode};
8use super::store::EmbeddedKernelStore;
9
10pub(super) fn read_batch(
16 tx: &dyn ReadTx,
17 node_ids: &[String],
18 language: &str,
19) -> Result<Vec<Option<NodeCard>>, PortError> {
20 node_ids
21 .iter()
22 .map(|id| {
23 tx.get(Table::Cards, Key::Str2(id, language))?
24 .map(|raw| decode::<CardRecord>("node card", &raw).map(Into::into))
25 .transpose()
26 })
27 .collect()
28}
29
30fn stored_card(
31 tx: &dyn WriteTx,
32 node_id: &str,
33 language: &str,
34) -> Result<Option<NodeCard>, PortError> {
35 tx.get(Table::Cards, Key::Str2(node_id, language))?
36 .map(|raw| decode::<CardRecord>("node card", &raw).map(Into::into))
37 .transpose()
38}
39
40impl NodeCardStore for EmbeddedKernelStore {
41 fn author_node_card(&self, command: AuthorNodeCard) -> NodeCardWriteFuture<'_> {
42 Box::pin(async move {
43 self.run(move |store| {
44 let mut tx = store.begin_write()?;
50 let node = tx
51 .get(Table::Nodes, Key::Str(&command.node_id))?
52 .map(|raw| decode::<NodeRecord>("card node", &raw)?.into_projection())
53 .transpose()?;
54 let descriptor =
58 super::node_body_descriptor::read_one(tx.as_ref(), &command.node_id)?;
59 let existing = stored_card(tx.as_ref(), &command.node_id, &command.language)?;
60
61 let admitted = match node_card_policy::admit(
62 &command,
63 node.as_ref(),
64 descriptor.as_ref(),
65 existing.as_ref(),
66 None,
67 ) {
68 Ok(card) => card,
69 Err(rejection) => return Ok(Err(rejection)),
72 };
73
74 append_event(tx.as_mut(), &command.about, &admitted, false)?;
75 project(tx.as_mut(), &admitted)?;
76 tx.commit()?;
77 Ok(Ok(admitted))
78 })
79 .await
80 })
81 }
82}
83
84#[cfg(test)]
85#[path = "node_card_tests.rs"]
86mod tests;
87
88pub(super) fn project(tx: &mut dyn WriteTx, card: &NodeCard) -> Result<(), PortError> {
90 if let Some(previous) = stored_card(tx, &card.node_id, &card.language)?
91 && previous != *card
92 && previous.card_revision.checked_add(1) != Some(card.card_revision)
93 {
94 return Err(PortError::InvalidState(
95 "non-contiguous card projection".into(),
96 ));
97 }
98 let bytes = encode("node card", &CardRecord::from(card.clone()))?;
99 let revision = version_key(card)?;
100 let key = Key::Str3(&card.node_id, &card.language, &revision);
101 if let Some(previous) = tx.get(Table::CardVersions, key)?
102 && previous != bytes
103 {
104 return Err(PortError::InvalidState(
105 "immutable card revision differs".into(),
106 ));
107 }
108 tx.insert(Table::CardVersions, key, &bytes)?;
109 tx.insert(
110 Table::Cards,
111 Key::Str2(&card.node_id, &card.language),
112 &bytes,
113 )
114}
115
116pub(super) fn append_event(
117 tx: &mut dyn WriteTx,
118 about: &str,
119 card: &NodeCard,
120 baseline: bool,
121) -> Result<(), PortError> {
122 let event = NodeCardEvent::record(about, card, baseline, std::time::SystemTime::now())?;
123 NodeCardEvent::card(&event)?;
124 let key = super::store::aggregate_key(about, NodeCardEvent::ROLE);
125 let revision = tx
126 .get(Table::Aggregates, Key::Str(&key))?
127 .map(|raw| decode::<AggregateRecord>("card stream head", &raw).map(|head| head.revision))
128 .transpose()?
129 .unwrap_or(0);
130 super::context_events::append_in_transaction(tx, event, revision)?;
131 Ok(())
132}
133
134pub(super) fn read_batch_at(
138 tx: &dyn ReadTx,
139 ids: &[String],
140 language: &str,
141 cut: Option<i128>,
142) -> Result<Vec<Option<NodeCard>>, PortError> {
143 let Some(cut) = cut else {
144 return read_batch(tx, ids, language);
145 };
146 let bound = history_key(cut, u64::MAX);
147 ids.iter()
148 .map(|id| {
149 let selected = tx
150 .last_str3_before(Table::CardVersions, id, language, &bound)?
151 .map(|raw| decode::<CardRecord>("card revision", &raw).map(Into::into))
152 .transpose()?;
153 match selected {
154 Some(card) => Ok(Some(card)),
155 None => tx
156 .get(Table::Cards, Key::Str2(id, language))?
157 .map(|raw| decode::<CardRecord>("node card", &raw).map(Into::into))
158 .transpose(),
159 }
160 })
161 .collect()
162}
163
164fn history_key(nanos: i128, revision: u64) -> String {
167 let ordered = (nanos as u128) ^ (1u128 << 127);
168 format!("{ordered:039}:{revision:020}")
169}
170
171pub(super) fn version_key(card: &NodeCard) -> Result<String, PortError> {
172 let nanos = kmp_domain::temporal_instant_nanos(&card.authored_at)
173 .ok_or_else(|| PortError::InvalidState("invalid card authorship instant".into()))?;
174 Ok(history_key(nanos, card.card_revision))
175}