use super::*;
pub const IDEMPOTENCY_TTL: std::time::Duration = std::time::Duration::from_secs(60 * 60);
pub const MAX_IDEMPOTENCY_KEY_BYTES: usize = 128;
const IDEM_PREFIX: &[u8] = b"idem\0";
#[derive(Debug)]
pub enum PostOutcome {
Stored(u64, MessageMeta),
Duplicate(String),
}
fn idem_key(agent: &AgentId, thread: &ThreadId, key: &str) -> Vec<u8> {
let mut out = IDEM_PREFIX.to_vec();
for part in [agent.as_str(), thread.as_str(), key] {
out.extend_from_slice(part.as_bytes());
out.push(0);
}
out
}
pub fn valid_idempotency_key(key: &str) -> bool {
!key.is_empty() && key.len() <= MAX_IDEMPOTENCY_KEY_BYTES && key.bytes().all(|b| b.is_ascii_graphic())
}
impl CommsStore {
pub fn post_keyed(
&self,
thread: &ThreadId,
meta: MessageMeta,
body: MessageBody,
key: &str,
) -> Result<PostOutcome, CommsStoreError> {
let row_key = idem_key(&meta.from, thread, key);
let _guard = self.post_lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(value) = self.meta.get(&row_key)?
&& let Ok((id, ts)) = rmp_serde::from_slice::<(String, i64)>(&value)
&& now_micros().saturating_sub(ts) < i64::try_from(IDEMPOTENCY_TTL.as_micros()).unwrap_or(i64::MAX)
{
return Ok(PostOutcome::Duplicate(id));
}
let row = rmp_serde::to_vec(&(meta.id.clone(), now_micros()))?;
let (seq, meta) = self.post_with_row(thread, meta, body, Some((row_key, row)))?;
Ok(PostOutcome::Stored(seq, meta))
}
pub(super) fn prune_idempotency(&self) -> Result<usize, CommsStoreError> {
let cutoff = now_micros().saturating_sub(i64::try_from(IDEMPOTENCY_TTL.as_micros()).unwrap_or(i64::MAX));
let mut batch = self.db.batch();
let mut pruned = 0usize;
for guard in self.meta.prefix(IDEM_PREFIX) {
let (key, value) = guard.into_inner()?;
let expired = rmp_serde::from_slice::<(String, i64)>(&value).is_ok_and(|(_, ts)| ts < cutoff);
if expired {
batch.remove(&self.meta, key.to_vec());
pruned += 1;
}
}
if pruned > 0 {
batch.commit()?;
}
Ok(pruned)
}
}