use std::time::Duration;
use chrono::Utc;
use tracing::{info, warn};
use vti_common::audit::{AuditEvent, DirectoryFieldChangedData};
use vti_common::error::AppError;
use super::{cards, fields};
use crate::server::AppState;
pub const ACTOR: &str = "vtc:directory-retention";
const PASS_INTERVAL: Duration = Duration::from_secs(3600);
fn purged_key(field: &str) -> Vec<u8> {
format!("purged:{field}").into_bytes()
}
pub async fn purge_due(state: &AppState) -> Result<Vec<String>, AppError> {
let ks = state.directory.ks();
let mut done = Vec::new();
for key in fields::due_for_purge(ks, Utc::now()).await? {
if ks.get_raw(purged_key(&key)).await?.is_some() {
continue;
}
if let Some(w) = state.audit_writer.as_ref() {
w.write(
ACTOR,
None,
AuditEvent::DirectoryFieldChanged(DirectoryFieldChangedData {
key: key.clone(),
change: "purge".into(),
widens: false,
reason: None,
}),
)
.await?;
}
let cards_held = cards::purge_field(ks, &key).await?;
ks.insert_raw(purged_key(&key), Utc::now().to_rfc3339().into_bytes())
.await?;
info!(field = %key, cards_held, "erased a retired directory field's values");
done.push(key);
}
Ok(done)
}
pub fn spawn(
state: AppState,
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
loop {
if let Err(e) = purge_due(&state).await {
warn!(error = %e, "directory retention pass failed; it runs again within the hour");
}
tokio::select! {
_ = shutdown_rx.changed() => return,
_ = tokio::time::sleep(PASS_INTERVAL) => {}
}
}
})
}