#[cfg(any(test, feature = "testing"))]
use std::{collections::HashSet, sync::Arc};
#[cfg(any(test, feature = "testing"))]
use tokio::sync::RwLock;
use crate::error::EngineError;
pub const MAX_INBOX_KEY_LEN: usize = 509;
#[allow(async_fn_in_trait)]
pub trait InboxStore: Send + Sync {
async fn accept(&self, key: &str) -> Result<bool, EngineError>;
}
#[cfg(any(test, feature = "testing"))]
#[derive(Debug, Default, Clone)]
pub struct InMemoryInboxStore {
seen: Arc<RwLock<HashSet<String>>>,
}
#[cfg(any(test, feature = "testing"))]
impl InMemoryInboxStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub async fn len(&self) -> usize {
self.seen.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.seen.read().await.is_empty()
}
}
#[cfg(any(test, feature = "testing"))]
impl InboxStore for InMemoryInboxStore {
async fn accept(&self, key: &str) -> Result<bool, EngineError> {
if key.len() > MAX_INBOX_KEY_LEN {
return Err(EngineError::inbox(format!(
"inbox key is {} bytes, exceeds maximum of {MAX_INBOX_KEY_LEN}",
key.len()
)));
}
Ok(self.seen.write().await.insert(key.to_owned()))
}
}
pub fn inbox_key(sender_party_id: &str, message_ref: &str) -> Result<String, &'static str> {
if sender_party_id.is_empty() {
return Err("inbox_key: sender_party_id must not be empty");
}
if message_ref.is_empty() {
return Err("inbox_key: message_ref must not be empty");
}
Ok(format!("{sender_party_id}:{message_ref}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn new_message_is_accepted() {
let inbox = InMemoryInboxStore::new();
assert!(inbox.accept("sender:ref-001").await.unwrap());
}
#[tokio::test]
async fn duplicate_message_is_rejected() {
let inbox = InMemoryInboxStore::new();
assert!(inbox.accept("sender:ref-001").await.unwrap());
assert!(
!inbox.accept("sender:ref-001").await.unwrap(),
"second accept should return false"
);
}
#[tokio::test]
async fn different_senders_same_ref_are_independent() {
let inbox = InMemoryInboxStore::new();
assert!(
inbox
.accept(&inbox_key("sender-A", "ref-001").unwrap())
.await
.unwrap()
);
assert!(
inbox
.accept(&inbox_key("sender-B", "ref-001").unwrap())
.await
.unwrap()
);
}
#[test]
fn inbox_key_rejects_empty_party_id() {
assert!(inbox_key("", "ref-001").is_err());
}
#[test]
fn inbox_key_rejects_empty_ref() {
assert!(inbox_key("4012345000023", "").is_err());
}
#[test]
fn inbox_key_formats_correctly() {
assert_eq!(
inbox_key("4012345000023", "MSG-001").unwrap(),
"4012345000023:MSG-001",
);
}
}