use kmp_domain::{
AuthorNodeCard, NodeCard, NodeCardEvent, NodeCardStore, NodeCardWriteFuture, PortError,
node_card_policy,
};
use super::engine::{Key, ReadTx, Table, WriteTx};
use super::serdes::{AggregateRecord, CardRecord, NodeRecord, decode, encode};
use super::store::EmbeddedKernelStore;
pub(super) fn read_batch(
tx: &dyn ReadTx,
node_ids: &[String],
language: &str,
) -> Result<Vec<Option<NodeCard>>, PortError> {
node_ids
.iter()
.map(|id| {
tx.get(Table::Cards, Key::Str2(id, language))?
.map(|raw| decode::<CardRecord>("node card", &raw).map(Into::into))
.transpose()
})
.collect()
}
fn stored_card(
tx: &dyn WriteTx,
node_id: &str,
language: &str,
) -> Result<Option<NodeCard>, PortError> {
tx.get(Table::Cards, Key::Str2(node_id, language))?
.map(|raw| decode::<CardRecord>("node card", &raw).map(Into::into))
.transpose()
}
impl NodeCardStore for EmbeddedKernelStore {
fn author_node_card(&self, command: AuthorNodeCard) -> NodeCardWriteFuture<'_> {
Box::pin(async move {
self.run(move |store| {
let mut tx = store.begin_write()?;
let node = tx
.get(Table::Nodes, Key::Str(&command.node_id))?
.map(|raw| decode::<NodeRecord>("card node", &raw)?.into_projection())
.transpose()?;
let descriptor =
super::node_body_descriptor::read_one(tx.as_ref(), &command.node_id)?;
let existing = stored_card(tx.as_ref(), &command.node_id, &command.language)?;
let admitted = match node_card_policy::admit(
&command,
node.as_ref(),
descriptor.as_ref(),
existing.as_ref(),
None,
) {
Ok(card) => card,
Err(rejection) => return Ok(Err(rejection)),
};
append_event(tx.as_mut(), &command.about, &admitted, false)?;
project(tx.as_mut(), &admitted)?;
tx.commit()?;
Ok(Ok(admitted))
})
.await
})
}
}
#[cfg(test)]
#[path = "node_card_tests.rs"]
mod tests;
pub(super) fn project(tx: &mut dyn WriteTx, card: &NodeCard) -> Result<(), PortError> {
if let Some(previous) = stored_card(tx, &card.node_id, &card.language)?
&& previous != *card
&& previous.card_revision.checked_add(1) != Some(card.card_revision)
{
return Err(PortError::InvalidState(
"non-contiguous card projection".into(),
));
}
let bytes = encode("node card", &CardRecord::from(card.clone()))?;
let revision = version_key(card)?;
let key = Key::Str3(&card.node_id, &card.language, &revision);
if let Some(previous) = tx.get(Table::CardVersions, key)?
&& previous != bytes
{
return Err(PortError::InvalidState(
"immutable card revision differs".into(),
));
}
tx.insert(Table::CardVersions, key, &bytes)?;
tx.insert(
Table::Cards,
Key::Str2(&card.node_id, &card.language),
&bytes,
)
}
pub(super) fn append_event(
tx: &mut dyn WriteTx,
about: &str,
card: &NodeCard,
baseline: bool,
) -> Result<(), PortError> {
let event = NodeCardEvent::record(about, card, baseline, std::time::SystemTime::now())?;
NodeCardEvent::card(&event)?;
let key = super::store::aggregate_key(about, NodeCardEvent::ROLE);
let revision = tx
.get(Table::Aggregates, Key::Str(&key))?
.map(|raw| decode::<AggregateRecord>("card stream head", &raw).map(|head| head.revision))
.transpose()?
.unwrap_or(0);
super::context_events::append_in_transaction(tx, event, revision)?;
Ok(())
}
pub(super) fn read_batch_at(
tx: &dyn ReadTx,
ids: &[String],
language: &str,
cut: Option<i128>,
) -> Result<Vec<Option<NodeCard>>, PortError> {
let Some(cut) = cut else {
return read_batch(tx, ids, language);
};
let bound = history_key(cut, u64::MAX);
ids.iter()
.map(|id| {
let selected = tx
.last_str3_before(Table::CardVersions, id, language, &bound)?
.map(|raw| decode::<CardRecord>("card revision", &raw).map(Into::into))
.transpose()?;
match selected {
Some(card) => Ok(Some(card)),
None => tx
.get(Table::Cards, Key::Str2(id, language))?
.map(|raw| decode::<CardRecord>("node card", &raw).map(Into::into))
.transpose(),
}
})
.collect()
}
fn history_key(nanos: i128, revision: u64) -> String {
let ordered = (nanos as u128) ^ (1u128 << 127);
format!("{ordered:039}:{revision:020}")
}
pub(super) fn version_key(card: &NodeCard) -> Result<String, PortError> {
let nanos = kmp_domain::temporal_instant_nanos(&card.authored_at)
.ok_or_else(|| PortError::InvalidState("invalid card authorship instant".into()))?;
Ok(history_key(nanos, card.card_revision))
}