use crate::delivery::{DeliveryError, DeliveryOutcome, DeliverySink};
use crate::models::delivery_outbox::DeliveryOutbox;
pub const REACTOR_EVENT_KIND: &str = "reactor_event";
const ACCUMULATOR_RECIPIENT_PREFIX: &str = "accumulator:";
pub fn accumulator_recipient(accumulator_name: &str) -> String {
format!("{ACCUMULATOR_RECIPIENT_PREFIX}{accumulator_name}")
}
pub fn parse_accumulator_recipient(recipient: &str) -> Option<&str> {
recipient.strip_prefix(ACCUMULATOR_RECIPIENT_PREFIX)
}
pub struct AccumulatorDeliverySink {
registry: super::registry::EndpointRegistry,
}
impl AccumulatorDeliverySink {
pub fn new(registry: super::registry::EndpointRegistry) -> Self {
Self { registry }
}
}
#[async_trait::async_trait]
impl DeliverySink for AccumulatorDeliverySink {
async fn deliver(&self, row: &DeliveryOutbox) -> Result<DeliveryOutcome, DeliveryError> {
if row.kind != REACTOR_EVENT_KIND {
return Ok(DeliveryOutcome::NoRoute);
}
let Some(name) = parse_accumulator_recipient(&row.recipient) else {
return Ok(DeliveryOutcome::NoRoute);
};
let scope = match row.tenant_id.as_deref() {
Some(t) => super::registry::EndpointScope::tenant(t),
None => super::registry::EndpointScope::untenanted(),
};
match self
.registry
.send_to_accumulator(name, scope, row.payload.clone())
.await
{
Ok(n) if n > 0 => Ok(DeliveryOutcome::Delivered),
Ok(_) => Ok(DeliveryOutcome::NoRoute),
Err(super::registry::RegistryError::AccumulatorNotFound(_)) => {
Ok(DeliveryOutcome::NoRoute)
}
Err(e) => Err(DeliveryError::Sink(e.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InjectOutcome {
Local(usize),
Forwarded,
}
pub async fn inject_event(
registry: &super::registry::EndpointRegistry,
dal: Option<&crate::dal::unified::DAL>,
name: &str,
tenant_id: Option<&str>,
bytes: Vec<u8>,
) -> Result<InjectOutcome, super::registry::RegistryError> {
let scope = match tenant_id {
Some(t) => super::registry::EndpointScope::tenant(t),
None => super::registry::EndpointScope::untenanted(),
};
match registry
.send_to_accumulator(name, scope, bytes.clone())
.await
{
Ok(n) if n > 0 => Ok(InjectOutcome::Local(n)),
Ok(_) | Err(super::registry::RegistryError::AccumulatorNotFound(_)) => {
let Some(dal) = dal else {
return Err(super::registry::RegistryError::AccumulatorNotFound(
name.to_string(),
));
};
let row = crate::models::delivery_outbox::NewDeliveryOutbox {
recipient: accumulator_recipient(name),
kind: REACTOR_EVENT_KIND.to_string(),
tenant_id: tenant_id.map(|t| t.to_string()),
payload: bytes,
};
dal.delivery_outbox().enqueue(row).await.map_err(|e| {
super::registry::RegistryError::AccumulatorNotFound(format!(
"{name}: not hosted here and forwarding to the owning replica failed: {e}"
))
})?;
tracing::debug!(
accumulator = %name,
tenant = ?tenant_id,
"accumulator not hosted here; forwarded event to the owning replica via the outbox"
);
Ok(InjectOutcome::Forwarded)
}
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recipient_round_trips() {
let r = accumulator_recipient("sensor_window");
assert_eq!(r, "accumulator:sensor_window");
assert_eq!(parse_accumulator_recipient(&r), Some("sensor_window"));
}
#[test]
fn other_subsystems_recipients_are_not_ours() {
assert_eq!(parse_accumulator_recipient("agent:abc-123"), None);
assert_eq!(parse_accumulator_recipient("exec_events:xyz"), None);
assert_eq!(parse_accumulator_recipient(""), None);
}
#[test]
fn only_the_leading_prefix_is_stripped() {
let r = accumulator_recipient("accumulator:weird");
assert_eq!(parse_accumulator_recipient(&r), Some("accumulator:weird"));
}
}