use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use cala_types::tx_template::TxTemplateValues;
use crate::{
primitives::TxTemplateId,
tx_template::{error::TxTemplateError, TxTemplateEvent},
};
use super::repo::PostingRepo;
#[derive(Clone)]
pub(super) struct ResolvedTemplate {
pub id: TxTemplateId,
pub version: i32,
pub values: Arc<TxTemplateValues>,
}
#[derive(Clone)]
pub(super) struct TemplateCache {
inner: Arc<TemplateCacheInner>,
}
struct TemplateCacheInner {
repo: PostingRepo,
snapshot: RwLock<Arc<HashMap<String, ResolvedTemplate>>>,
}
impl TemplateCache {
pub(super) fn new(repo: PostingRepo) -> Self {
Self {
inner: Arc::new(TemplateCacheInner {
repo,
snapshot: RwLock::new(Arc::new(HashMap::new())),
}),
}
}
pub(super) async fn resolve_in_op(
&self,
op: &mut impl es_entity::AtomicOperation,
codes: &[String],
) -> Result<HashMap<String, ResolvedTemplate>, TxTemplateError> {
let snapshot = self.load();
let mut used = HashMap::new();
let mut missing = Vec::new();
for code in codes {
match snapshot.get(code) {
Some(resolved) => {
used.insert(code.clone(), resolved.clone());
}
None => missing.push(code.clone()),
}
}
if !missing.is_empty() {
used.extend(self.fetch_and_install(op, &missing).await?);
}
Ok(used)
}
pub(super) async fn refresh_in_op(
&self,
op: &mut impl es_entity::AtomicOperation,
codes: &[String],
) -> Result<HashMap<String, ResolvedTemplate>, TxTemplateError> {
self.fetch_and_install(op, codes).await
}
pub(super) fn assert_up_to_date(
used: &HashMap<String, ResolvedTemplate>,
observed: &HashMap<String, (TxTemplateId, i32)>,
) -> Result<(), Vec<String>> {
let stale: Vec<String> = used
.iter()
.filter(|(code, resolved)| {
observed.get(*code) != Some(&(resolved.id, resolved.version))
})
.map(|(code, _)| code.clone())
.collect();
if stale.is_empty() {
Ok(())
} else {
Err(stale)
}
}
async fn fetch_and_install(
&self,
op: &mut impl es_entity::AtomicOperation,
codes: &[String],
) -> Result<HashMap<String, ResolvedTemplate>, TxTemplateError> {
let mut fetched = self.inner.repo.resolve_templates_in_op(op, codes).await?;
let mut resolved = HashMap::with_capacity(codes.len());
for code in codes {
let Some((id, version, event)) = fetched.remove(code) else {
return Err(TxTemplateError::NotFound);
};
let event: TxTemplateEvent = serde_json::from_value(event)?;
resolved.insert(
code.clone(),
ResolvedTemplate {
id,
version,
values: Arc::new(event.into_values()),
},
);
}
self.install(&resolved);
Ok(resolved)
}
fn load(&self) -> Arc<HashMap<String, ResolvedTemplate>> {
self.inner
.snapshot
.read()
.expect("template cache poisoned")
.clone()
}
fn install(&self, resolved: &HashMap<String, ResolvedTemplate>) {
let mut guard = self
.inner
.snapshot
.write()
.expect("template cache poisoned");
let mut next = HashMap::clone(&guard);
for (code, template) in resolved {
next.insert(code.clone(), template.clone());
}
*guard = Arc::new(next);
}
}